• 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.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
19 import static android.media.AudioDeviceInfo.TYPE_AUX_LINE;
20 import static android.media.AudioDeviceInfo.TYPE_BLE_BROADCAST;
21 import static android.media.AudioDeviceInfo.TYPE_BLE_HEADSET;
22 import static android.media.AudioDeviceInfo.TYPE_BLE_SPEAKER;
23 import static android.media.AudioDeviceInfo.TYPE_BLUETOOTH_A2DP;
24 import static android.media.AudioDeviceInfo.TYPE_BUILTIN_SPEAKER;
25 import static android.media.AudioDeviceInfo.TYPE_BUS;
26 import static android.media.AudioDeviceInfo.TYPE_HDMI;
27 import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY;
28 import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
29 import static android.media.AudioDeviceInfo.TYPE_USB_HEADSET;
30 import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADPHONES;
31 import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET;
32 
33 import static com.android.car.audio.CarAudioService.CAR_DEFAULT_AUDIO_ATTRIBUTE;
34 import static com.android.car.audio.CarAudioService.TAG;
35 import static com.android.car.audio.CarAudioUtils.ACTIVATION_VOLUME_INVOCATION_TYPE;
36 import static com.android.car.audio.CarAudioUtils.ACTIVATION_VOLUME_PERCENTAGE_MAX;
37 import static com.android.car.audio.CarAudioUtils.ACTIVATION_VOLUME_PERCENTAGE_MIN;
38 import static com.android.car.audio.CarAudioUtils.DEFAULT_ACTIVATION_VOLUME;
39 import static com.android.car.audio.CarAudioUtils.generateAddressToCarAudioDeviceInfoMap;
40 import static com.android.car.audio.CarAudioUtils.generateAddressToInputAudioDeviceInfoMap;
41 import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
42 
43 import static java.util.Locale.ROOT;
44 
45 import android.car.builtin.util.Slogf;
46 import android.car.feature.Flags;
47 import android.car.oem.CarAudioFadeConfiguration;
48 import android.media.AudioAttributes;
49 import android.media.AudioDeviceAttributes;
50 import android.media.AudioDeviceInfo;
51 import android.media.audiopolicy.AudioProductStrategy;
52 import android.text.TextUtils;
53 import android.util.ArrayMap;
54 import android.util.ArraySet;
55 import android.util.SparseArray;
56 import android.util.SparseIntArray;
57 import android.util.Xml;
58 
59 import com.android.car.audio.CarAudioContext.AudioContext;
60 import com.android.car.internal.util.ConstantDebugUtils;
61 import com.android.car.internal.util.DebugUtils;
62 import com.android.car.internal.util.LocalLog;
63 import com.android.internal.util.Preconditions;
64 
65 import org.xmlpull.v1.XmlPullParser;
66 import org.xmlpull.v1.XmlPullParserException;
67 
68 import java.io.IOException;
69 import java.io.InputStream;
70 import java.util.ArrayList;
71 import java.util.List;
72 import java.util.Map;
73 import java.util.MissingResourceException;
74 import java.util.Objects;
75 import java.util.Optional;
76 import java.util.Set;
77 
78 /**
79  * A helper class loads all audio zones from the configuration XML file.
80  */
81 /* package */ final class CarAudioZonesHelperImpl implements CarAudioZonesHelper {
82     private static final String NAMESPACE = null;
83     private static final String TAG_ROOT = "carAudioConfiguration";
84 
85     private static final String TAG_OEM_CONTEXTS = "oemContexts";
86     private static final String TAG_OEM_CONTEXT = "oemContext";
87     private static final String OEM_CONTEXT_NAME = "name";
88 
89     private static final String TAG_DEVICE_CONFIGURATIONS = "deviceConfigurations";
90     private static final String TAG_DEVICE_CONFIG = "deviceConfiguration";
91     private static final String TAG_DEVICE_CONFIG_NAME = "name";
92     private static final String TAG_DEVICE_CONFIG_VALUE = "value";
93     private static final String DEVICE_CONFIG_CORE_VOLUME = "useCoreAudioVolume";
94     private static final String DEVICE_CONFIG_CORE_ROUTING = "useCoreAudioRouting";
95     private static final String DEVICE_CONFIG_DUCKING_SIGNALS = "useHalDuckingSignals";
96     private static final String DEVICE_CONFIG_GROUP_MUTING = "useCarVolumeGroupMuting";
97 
98     private static final String TAG_AUDIO_ZONES = "zones";
99     private static final String TAG_AUDIO_ZONE = "zone";
100     private static final String TAG_AUDIO_ZONE_CONFIGS = "zoneConfigs";
101     private static final String TAG_AUDIO_ZONE_CONFIG = "zoneConfig";
102     private static final String TAG_VOLUME_GROUPS = "volumeGroups";
103     private static final String VOLUME_GROUP_NAME = "name";
104     private static final String TAG_VOLUME_GROUP = "group";
105     private static final String TAG_AUDIO_DEVICE = "device";
106     private static final String TAG_CONTEXT = "context";
107     private static final String ATTR_ACTIVATION_VOLUME_CONFIG = "activationConfig";
108     private static final String ATTR_VERSION = "version";
109     private static final String ATTR_IS_PRIMARY = "isPrimary";
110     private static final String ATTR_IS_CONFIG_DEFAULT = "isDefault";
111     private static final String ATTR_ZONE_NAME = "name";
112     private static final String ATTR_CONFIG_NAME = "name";
113     private static final String ATTR_DEVICE_ADDRESS = "address";
114     private static final String ATTR_DEVICE_TYPE = "type";
115     private static final String ATTR_CONTEXT_NAME = "context";
116     private static final String ATTR_ZONE_ID = "audioZoneId";
117     private static final String ATTR_OCCUPANT_ZONE_ID = "occupantZoneId";
118     private static final String TAG_INPUT_DEVICES = "inputDevices";
119     private static final String TAG_INPUT_DEVICE = "inputDevice";
120     private static final String TAG_MIRRORING_DEVICES = "mirroringDevices";
121     private static final String TAG_MIRRORING_DEVICE = "mirroringDevice";
122     private static final String TAG_ACTIVATION_VOLUME_CONFIGS = "activationVolumeConfigs";
123     private static final String TAG_ACTIVATION_VOLUME_CONFIG = "activationVolumeConfig";
124     private static final String TAG_ACTIVATION_VOLUME_CONFIG_ENTRY = "activationVolumeConfigEntry";
125     private static final String ATTR_ACTIVATION_VOLUME_CONFIG_NAME = "name";
126     private static final String ATTR_ACTIVATION_VOLUME_INVOCATION_TYPE = "invocationType";
127     private static final String ATTR_MAX_ACTIVATION_VOLUME_PERCENTAGE =
128             "maxActivationVolumePercentage";
129     private static final String ATTR_MIN_ACTIVATION_VOLUME_PERCENTAGE =
130             "minActivationVolumePercentage";
131     private static final String ACTIVATION_VOLUME_INVOCATION_TYPE_ON_BOOT = "onBoot";
132     private static final String ACTIVATION_VOLUME_INVOCATION_TYPE_ON_SOURCE_CHANGED =
133             "onSourceChanged";
134     private static final String ACTIVATION_VOLUME_INVOCATION_TYPE_ON_PLAYBACK_CHANGED =
135             "onPlaybackChanged";
136     private static final String TAG_APPLY_FADE_CONFIGS = "applyFadeConfigs";
137     private static final String FADE_CONFIG = "fadeConfig";
138     private static final String FADE_CONFIG_NAME = "name";
139     private static final String FADE_CONFIG_IS_DEFAULT = "isDefault";
140     private static final int INVALID_VERSION = -1;
141     private static final int SUPPORTED_VERSION_1 = 1;
142     private static final int SUPPORTED_VERSION_2 = 2;
143     private static final int SUPPORTED_VERSION_3 = 3;
144     private static final int SUPPORTED_VERSION_4 = 4;
145     private static final SparseIntArray SUPPORTED_VERSIONS;
146 
147     static {
148         SUPPORTED_VERSIONS = new SparseIntArray(4);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1)149         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2)150         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_3, SUPPORTED_VERSION_3)151         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_3, SUPPORTED_VERSION_3);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_4, SUPPORTED_VERSION_4)152         SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_4, SUPPORTED_VERSION_4);
153     }
154 
155     private final AudioManagerWrapper mAudioManager;
156     private final CarAudioSettings mCarAudioSettings;
157     private final List<CarAudioContextInfo> mCarAudioContextInfos = new ArrayList<>();
158     private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
159     private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices;
160     private final InputStream mInputStream;
161     private final SparseIntArray mZoneIdToOccupantZoneIdMapping;
162     private final Set<Integer> mAudioZoneIds;
163     private final Set<String> mAssignedInputAudioDevices;
164     private final Set<String> mAudioZoneConfigNames;
165     private final boolean mUseFadeManagerConfiguration;
166     private final CarAudioFadeConfigurationHelper mCarAudioFadeConfigurationHelper;
167     private final List<CarAudioDeviceInfo> mMirroringDevices = new ArrayList<>();
168     private final Map<String, CarActivationVolumeConfig> mConfigNameToActivationVolumeConfig =
169             new ArrayMap<>();
170 
171     private final ArrayMap<String, String> mDeviceConfigNameToValue = new ArrayMap<>();
172     private boolean mUseCoreAudioVolume;
173     private boolean mUseCoreAudioRouting;
174     private boolean mUseCarVolumeGroupMute;
175     private Optional<Boolean> mUseHalDuckingSignals = Optional.empty();
176     private final ArrayMap<String, Integer> mContextNameToId = new ArrayMap<>();
177     private final LocalLog mCarServiceLocalLog;
178     private CarAudioContext mCarAudioContext;
179     private int mNextSecondaryZoneId;
180     private int mCurrentVersion;
181 
182     /**
183      * <p><b>Note: </b> CarAudioZonesHelper is expected to be used from a single thread. This
184      * should be the same thread that originally called new CarAudioZonesHelper.
185      */
CarAudioZonesHelperImpl(AudioManagerWrapper audioManager, CarAudioSettings carAudioSettings, InputStream inputStream, List<CarAudioDeviceInfo> carAudioDeviceInfos, AudioDeviceInfo[] inputDeviceInfo, LocalLog serviceLog, boolean useCarVolumeGroupMute, boolean useCoreAudioVolume, boolean useCoreAudioRouting, boolean useFadeManagerConfiguration, CarAudioFadeConfigurationHelper carAudioFadeConfigurationHelper)186     CarAudioZonesHelperImpl(AudioManagerWrapper audioManager, CarAudioSettings carAudioSettings,
187                             InputStream inputStream, List<CarAudioDeviceInfo> carAudioDeviceInfos,
188                             AudioDeviceInfo[] inputDeviceInfo, LocalLog serviceLog,
189                             boolean useCarVolumeGroupMute, boolean useCoreAudioVolume,
190                             boolean useCoreAudioRouting, boolean useFadeManagerConfiguration,
191                             CarAudioFadeConfigurationHelper carAudioFadeConfigurationHelper) {
192         mAudioManager = Objects.requireNonNull(audioManager,
193                 "Audio manager cannot be null");
194         mCarAudioSettings = Objects.requireNonNull(carAudioSettings);
195         mInputStream = Objects.requireNonNull(inputStream);
196         Objects.requireNonNull(carAudioDeviceInfos);
197         Objects.requireNonNull(inputDeviceInfo);
198         mAddressToCarAudioDeviceInfo = generateAddressToCarAudioDeviceInfoMap(carAudioDeviceInfos);
199         mCarServiceLocalLog = Objects.requireNonNull(serviceLog,
200                 "Car audio service local log cannot be null");
201         mAddressToInputAudioDeviceInfoForAllInputDevices =
202                 generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo);
203         mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1;
204         mZoneIdToOccupantZoneIdMapping = new SparseIntArray();
205         mAudioZoneIds = new ArraySet<>();
206         mAssignedInputAudioDevices = new ArraySet<>();
207         mAudioZoneConfigNames = new ArraySet<>();
208         mUseCarVolumeGroupMute = useCarVolumeGroupMute;
209         mUseCoreAudioVolume = useCoreAudioVolume;
210         mUseCoreAudioRouting = useCoreAudioRouting;
211         mUseFadeManagerConfiguration = useFadeManagerConfiguration;
212         mCarAudioFadeConfigurationHelper = carAudioFadeConfigurationHelper;
213     }
214 
215     /**
216      * see {@code CarAudioZonesHelperI#getCarAudioZoneIdToOccupantZoneIdMapping} for details
217      */
218     @Override
getCarAudioZoneIdToOccupantZoneIdMapping()219     public SparseIntArray getCarAudioZoneIdToOccupantZoneIdMapping() {
220         return mZoneIdToOccupantZoneIdMapping;
221     }
222 
223     /**
224      * see {@code CarAudioZonesHelperI#loadAudioZones} for details
225      */
226     @Override
loadAudioZones()227     public SparseArray<CarAudioZone> loadAudioZones() throws IOException, XmlPullParserException {
228         return parseCarAudioZones(mInputStream);
229     }
230 
231     /**
232      * Returns the updated use core volume device configuration
233      *
234      * <p><b>Note</b> The value will depend on the device configuration obtained from the car audio
235      * configuration file. With value obtained from the configuration file having higher priority
236      * over the passed in value (which was obtained from the legacy RRO). If the value is not
237      * defined in the audio configuration file, the original passed in value will be returned.
238      *
239      * @return {@code true} if core volume management should be used, {@code false} otherwise.
240      */
241     @Override
useCoreAudioVolume()242     public boolean useCoreAudioVolume() {
243         return mUseCoreAudioVolume;
244     }
245 
246     /**
247      * Returns the updated use core routing device configuration
248      *
249      * <p><b>Note</b> The value will depend on the device configuration obtained from the car audio
250      * configuration file. With value obtained from the configuration file having higher priority
251      * over the passed in value (which was obtained from the legacy RRO). If the value is not
252      * defined in the audio configuration file, the original passed in value will be returned.
253      *
254      * @return {@code true} if core routing management should be used, {@code false} otherwise.
255      */
256     @Override
useCoreAudioRouting()257     public boolean useCoreAudioRouting() {
258         return mUseCoreAudioRouting;
259     }
260 
261     /**
262      * Returns the updated use volume group muting device configuration
263      *
264      * <p><b>Note</b> The value will depend on the device configuration obtained from the car audio
265      * configuration file. With value obtained from the configuration file having higher priority
266      * over the passed in value (which was obtained from the legacy RRO). If the value is not
267      * defined in the audio configuration file, the original passed in value will be returned.
268      *
269      * @return {@code true} if volume group muting should be used, {@code false} otherwise.
270      */
271     @Override
useVolumeGroupMuting()272     public boolean useVolumeGroupMuting() {
273         return mUseCarVolumeGroupMute;
274     }
275 
276     /**
277      * Returns the updated use HAL ducking device configuration
278      *
279      * <p><b>Note</b> The value will depend on the device configuration obtained from the car audio
280      * configuration file. With value obtained from the configuration file having higher priority
281      * over the passed in default value (which was obtained from the legacy RRO). If the value is
282      * not defined in the audio configuration file, the passed in value will be returned.
283      *
284      * @param defaultUseHalDuckingSignal default value that should be returned if the config was
285      *                                   not present in the audio configuration file.
286      *
287      * @return {@code true} if volume group muting should be used, {@code false} otherwise.
288      */
289     @Override
useHalDuckingSignalOrDefault(boolean defaultUseHalDuckingSignal)290     public boolean useHalDuckingSignalOrDefault(boolean defaultUseHalDuckingSignal) {
291         return mUseHalDuckingSignals.orElse(defaultUseHalDuckingSignal);
292     }
293 
parseCarAudioZones(InputStream stream)294     private SparseArray<CarAudioZone> parseCarAudioZones(InputStream stream)
295             throws XmlPullParserException, IOException {
296         XmlPullParser parser = Xml.newPullParser();
297         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null);
298         parser.setInput(stream, null);
299 
300         // Ensure <carAudioConfiguration> is the root
301         parser.nextTag();
302         parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT);
303 
304         // Version check
305         final int versionNumber = Integer.parseInt(
306                 parser.getAttributeValue(NAMESPACE, ATTR_VERSION));
307 
308         if (SUPPORTED_VERSIONS.get(versionNumber, INVALID_VERSION) == INVALID_VERSION) {
309             throw new IllegalArgumentException("Latest Supported version:"
310                     + SUPPORTED_VERSION_4 + " , got version:" + versionNumber);
311         }
312 
313         mCurrentVersion = versionNumber;
314         // Get all zones configured under <zones> tag
315         while (parser.next() != XmlPullParser.END_TAG) {
316             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
317             if (Flags.audioVendorFreezeImprovements()
318                     && Objects.equals(parser.getName(), TAG_DEVICE_CONFIGURATIONS)) {
319                 parseDeviceConfigurations(parser);
320             } else if (Objects.equals(parser.getName(), TAG_OEM_CONTEXTS)) {
321                 parseCarAudioContexts(parser);
322             } else if (Objects.equals(parser.getName(), TAG_MIRRORING_DEVICES)) {
323                 parseMirroringDevices(parser);
324             } else if (Objects.equals(parser.getName(), TAG_ACTIVATION_VOLUME_CONFIGS)) {
325                 parseActivationVolumeConfigs(parser);
326             } else if (Objects.equals(parser.getName(), TAG_AUDIO_ZONES)) {
327                 loadCarAudioContexts();
328                 return parseAudioZones(parser);
329             } else {
330                 CarAudioParserUtils.skip(parser);
331             }
332         }
333         throw new MissingResourceException(TAG_AUDIO_ZONES + " is missing from configuration",
334                 "", TAG_AUDIO_ZONES);
335     }
336 
parseMirroringDevices(XmlPullParser parser)337     private void parseMirroringDevices(XmlPullParser parser)
338             throws XmlPullParserException, IOException {
339         if (isVersionLessThanThree()) {
340             throw new IllegalStateException(
341                     TAG_MIRRORING_DEVICES + " are not supported in car_audio_configuration.xml"
342                             + " version " + mCurrentVersion + ". Must be at least version "
343                             + SUPPORTED_VERSION_3);
344         }
345 
346         while (parser.next() != XmlPullParser.END_TAG) {
347             if (parser.getEventType() != XmlPullParser.START_TAG) {
348                 continue;
349             }
350             if (Objects.equals(parser.getName(), TAG_MIRRORING_DEVICE)) {
351                 parseMirroringDevice(parser);
352             }
353             CarAudioParserUtils.skip(parser);
354         }
355     }
356 
parseMirroringDevice(XmlPullParser parser)357     private void parseMirroringDevice(XmlPullParser parser) {
358         String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
359         validateOutputAudioDevice(address, TYPE_BUS);
360         CarAudioDeviceInfo info = getCarAudioDeviceInfo(address, TYPE_BUS);
361         if (mMirroringDevices.contains(info)) {
362             throw new IllegalArgumentException(TAG_MIRRORING_DEVICE + " " + address
363                     + " repeats, " + TAG_MIRRORING_DEVICES + " can not repeat.");
364         }
365         mMirroringDevices.add(info);
366     }
367 
loadCarAudioContexts()368     private void loadCarAudioContexts() {
369         if (isVersionLessThanThree() || mCarAudioContextInfos.isEmpty()) {
370             mCarAudioContextInfos.addAll(CarAudioContext.getAllContextsInfo());
371         }
372         for (int index = 0; index < mCarAudioContextInfos.size(); index++) {
373             CarAudioContextInfo info = mCarAudioContextInfos.get(index);
374             mContextNameToId.put(info.getName().toLowerCase(ROOT), info.getId());
375         }
376         mCarAudioContext = new CarAudioContext(mCarAudioContextInfos, mUseCoreAudioRouting);
377     }
378 
parseCarAudioContexts(XmlPullParser parser)379     private void parseCarAudioContexts(XmlPullParser parser)
380             throws XmlPullParserException, IOException {
381         int contextId = CarAudioContext.getInvalidContext() + 1;
382 
383         while (parser.next() != XmlPullParser.END_TAG) {
384             if (parser.getEventType() != XmlPullParser.START_TAG) {
385                 continue;
386             }
387             if (Objects.equals(parser.getName(), TAG_OEM_CONTEXT)) {
388                 parseCarAudioContext(parser, contextId);
389                 contextId++;
390             } else {
391                 CarAudioParserUtils.skip(parser);
392             }
393         }
394     }
395 
parseCarAudioContext(XmlPullParser parser, int contextId)396     private void parseCarAudioContext(XmlPullParser parser, int contextId)
397             throws XmlPullParserException, IOException {
398         String contextName = parser.getAttributeValue(NAMESPACE, OEM_CONTEXT_NAME);
399         CarAudioContextInfo context = null;
400         while (parser.next() != XmlPullParser.END_TAG) {
401             if (parser.getEventType() != XmlPullParser.START_TAG) {
402                 continue;
403             }
404             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES)) {
405                 List<AudioAttributes> attributes =
406                         CarAudioParserUtils.parseAudioAttributes(parser, contextName);
407                 if (mUseCoreAudioRouting) {
408                     contextId = CoreAudioHelper.getStrategyForContextName(contextName);
409                     if (contextId == CoreAudioHelper.INVALID_STRATEGY) {
410                         throw new IllegalArgumentException(CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES
411                                 + ": Cannot find strategy id for context: "
412                                 + contextName + " and attributes \"" + attributes.get(0) + "\" .");
413                     }
414                 }
415                 validateCarAudioContextAttributes(contextId, attributes, contextName);
416                 context = new CarAudioContextInfo(attributes.toArray(new AudioAttributes[0]),
417                         contextName, contextId);
418                 mCarAudioContextInfos.add(context);
419             } else {
420                 CarAudioParserUtils.skip(parser);
421             }
422         }
423     }
424 
validateCarAudioContextAttributes(int contextId, List<AudioAttributes> attributes, String contextName)425     private void validateCarAudioContextAttributes(int contextId, List<AudioAttributes> attributes,
426             String contextName) {
427         if (!mUseCoreAudioRouting) {
428             return;
429         }
430         AudioProductStrategy strategy = CoreAudioHelper.getStrategy(contextId);
431         Preconditions.checkNotNull(strategy, "No strategy for context id = %d", contextId);
432         for (int index = 0; index < attributes.size(); index++) {
433             AudioAttributes aa = attributes.get(index);
434             if (!strategy.supportsAudioAttributes(aa)
435                     && !CoreAudioHelper.isDefaultStrategy(strategy.getId())) {
436                 throw new IllegalArgumentException("Invalid attributes " + aa + " for context: "
437                         + contextName);
438             }
439         }
440     }
441 
parseDeviceConfigurations(XmlPullParser parser)442     private void parseDeviceConfigurations(XmlPullParser parser)
443             throws XmlPullParserException, IOException {
444         while (parser.next() != XmlPullParser.END_TAG) {
445             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
446             if (Objects.equals(parser.getName(), TAG_DEVICE_CONFIG)) {
447                 parseAudioConfig(parser);
448             } else {
449                 CarAudioParserUtils.skip(parser);
450             }
451         }
452         mUseCoreAudioVolume = parseUseCoreAudioVolume();
453         mUseCoreAudioRouting = parseUseCoreAudioRouting();
454         mUseCarVolumeGroupMute = parseUseCarVolumeGroupMuting();
455         mUseHalDuckingSignals = parseUseHalDuckingSignals();
456     }
457 
parseAudioConfig(XmlPullParser parser)458     private void parseAudioConfig(XmlPullParser parser)
459             throws XmlPullParserException, IOException {
460         String name = parser.getAttributeValue(NAMESPACE, TAG_DEVICE_CONFIG_NAME);
461         String value = parser.getAttributeValue(NAMESPACE, TAG_DEVICE_CONFIG_VALUE);
462         if (!TextUtils.isEmpty(name) && !TextUtils.isEmpty(value)
463                 && !mDeviceConfigNameToValue.containsKey(name)) {
464             mDeviceConfigNameToValue.put(name, value);
465         } else if (mDeviceConfigNameToValue.containsKey(name)) {
466             // If the name already exist consistently keep the previous value
467             mCarServiceLocalLog.log("Device configuration " + name
468                     + " has a duplicate definition with a new value of " + value
469                     + ", keeping the previously assigned value of "
470                     + mDeviceConfigNameToValue.get(name));
471         } else {
472             Slogf.w(TAG, "Device configuration with empty name or value was encountered: [name="
473                     + name + ", value= " + value + "]");
474         }
475         while (parser.next() != XmlPullParser.END_TAG) {
476             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
477             CarAudioParserUtils.skip(parser);
478         }
479     }
480 
parseUseHalDuckingSignals()481     private Optional<Boolean> parseUseHalDuckingSignals() {
482         String useHalDuckingSignals = mDeviceConfigNameToValue.get(DEVICE_CONFIG_DUCKING_SIGNALS);
483         return parseOptionalBoolean(useHalDuckingSignals, DEVICE_CONFIG_DUCKING_SIGNALS);
484     }
485 
parseUseCarVolumeGroupMuting()486     private boolean parseUseCarVolumeGroupMuting() {
487         String useHalGroupMuting = mDeviceConfigNameToValue.get(DEVICE_CONFIG_GROUP_MUTING);
488         return parseBoolean(useHalGroupMuting, DEVICE_CONFIG_GROUP_MUTING, mUseCarVolumeGroupMute);
489     }
490 
parseUseCoreAudioVolume()491     private boolean parseUseCoreAudioVolume() {
492         String useCoreVolumeString = mDeviceConfigNameToValue.get(DEVICE_CONFIG_CORE_VOLUME);
493         return parseBoolean(useCoreVolumeString, DEVICE_CONFIG_CORE_VOLUME, mUseCoreAudioVolume);
494     }
495 
parseUseCoreAudioRouting()496     private boolean parseUseCoreAudioRouting() {
497         String useCoreVolumeString = mDeviceConfigNameToValue.get(DEVICE_CONFIG_CORE_ROUTING);
498         return parseBoolean(useCoreVolumeString, DEVICE_CONFIG_CORE_ROUTING,
499                 mUseCoreAudioRouting);
500     }
501 
parseBoolean(String booleanString, String configName, boolean defaultValue)502     private boolean parseBoolean(String booleanString, String configName, boolean defaultValue) {
503         Optional<Boolean> parsedValue = parseOptionalBoolean(booleanString, configName);
504 
505         return parsedValue.orElse(defaultValue);
506     }
507 
parseOptionalBoolean(String booleanString, String configName)508     private Optional<Boolean> parseOptionalBoolean(String booleanString, String configName) {
509         if (TextUtils.isEmpty(booleanString)) {
510             return Optional.empty();
511         }
512 
513         // Need to check for "true" or "false", since Boolean parse will return false for anything
514         // not "true" and we are specifically checking for "true" or "false".
515         if (!booleanString.equalsIgnoreCase("true")
516                 && !booleanString.equalsIgnoreCase("false")) {
517             mCarServiceLocalLog.log(configName + " was declared with the value of "
518                     + booleanString + " but should be either true or false");
519             return Optional.empty();
520         }
521 
522         mCarServiceLocalLog.log("Found " + configName + " = " + booleanString
523                 + " in car_audio_configuration.xml file");
524         return Optional.of(Boolean.parseBoolean(booleanString));
525     }
526 
parseAudioZones(XmlPullParser parser)527     private SparseArray<CarAudioZone> parseAudioZones(XmlPullParser parser)
528             throws XmlPullParserException, IOException {
529         SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
530 
531         while (parser.next() != XmlPullParser.END_TAG) {
532             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
533             if (Objects.equals(parser.getName(), TAG_AUDIO_ZONE)) {
534                 CarAudioZone zone = parseAudioZone(parser);
535                 carAudioZones.put(zone.getId(), zone);
536             } else {
537                 CarAudioParserUtils.skip(parser);
538             }
539         }
540 
541         verifyPrimaryZonePresent(carAudioZones);
542         addRemainingMicrophonesToPrimaryZone(carAudioZones);
543         return carAudioZones;
544     }
545 
addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones)546     private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) {
547         CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
548         for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) {
549             if (!mAssignedInputAudioDevices.contains(info.getAddress())
550                     && isMicrophoneInputDevice(info)) {
551                 primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
552             }
553         }
554     }
555 
verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones)556     private void verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones) {
557         if (!zones.contains(PRIMARY_AUDIO_ZONE)) {
558             throw new RuntimeException("Primary audio zone is required");
559         }
560     }
561 
parseAudioZone(XmlPullParser parser)562     private CarAudioZone parseAudioZone(XmlPullParser parser)
563             throws XmlPullParserException, IOException {
564         final boolean isPrimary = Boolean.parseBoolean(
565                 parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY));
566         final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME);
567         final int audioZoneId = getZoneId(isPrimary, parser);
568         parseOccupantZoneId(audioZoneId, parser);
569         final CarAudioZone zone = new CarAudioZone(mCarAudioContext, zoneName, audioZoneId);
570         if (isVersionLessThanThree()) {
571             final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder(
572                     zoneName, audioZoneId, /* zoneConfigId= */ 0, /* isDefault= */ true);
573             while (parser.next() != XmlPullParser.END_TAG) {
574                 if (parser.getEventType() != XmlPullParser.START_TAG) continue;
575                 // Expect at least one <volumeGroups> in one audio zone
576                 if (Objects.equals(parser.getName(), TAG_VOLUME_GROUPS)) {
577                     parseVolumeGroups(parser, zoneConfigBuilder);
578                 } else if (Objects.equals(parser.getName(), TAG_INPUT_DEVICES)) {
579                     parseInputAudioDevices(parser, zone);
580                 } else {
581                     CarAudioParserUtils.skip(parser);
582                 }
583             }
584             zone.addZoneConfig(zoneConfigBuilder.build());
585             return zone;
586         }
587         while (parser.next() != XmlPullParser.END_TAG) {
588             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
589             // Expect at least one <zoneConfigs> in one audio zone
590             if (Objects.equals(parser.getName(), TAG_AUDIO_ZONE_CONFIGS)) {
591                 parseZoneConfigs(parser, zone);
592             } else if (Objects.equals(parser.getName(), TAG_INPUT_DEVICES)) {
593                 parseInputAudioDevices(parser, zone);
594             } else {
595                 CarAudioParserUtils.skip(parser);
596             }
597         }
598         return zone;
599     }
600 
getZoneId(boolean isPrimary, XmlPullParser parser)601     private int getZoneId(boolean isPrimary, XmlPullParser parser) {
602         String audioZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_ID);
603         if (isVersionOne()) {
604             Preconditions.checkArgument(audioZoneIdString == null,
605                     "Invalid audio attribute %s"
606                             + ", Please update car audio configurations file "
607                             + "to version to 2 to use it.", ATTR_ZONE_ID);
608             return isPrimary ? PRIMARY_AUDIO_ZONE
609                     : getNextSecondaryZoneId();
610         }
611         // Primary zone does not need to define it
612         if (isPrimary && audioZoneIdString == null) {
613             return PRIMARY_AUDIO_ZONE;
614         }
615         Objects.requireNonNull(audioZoneIdString,
616                 "Requires audioZoneId for all audio zones.");
617         int zoneId = CarAudioParserUtils.parsePositiveIntAttribute(ATTR_ZONE_ID, audioZoneIdString);
618         //Verify that primary zone id is PRIMARY_AUDIO_ZONE
619         if (isPrimary) {
620             Preconditions.checkArgument(zoneId == PRIMARY_AUDIO_ZONE,
621                     "Primary zone %s must be %d or it can be left empty.",
622                     ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
623         } else {
624             Preconditions.checkArgument(zoneId != PRIMARY_AUDIO_ZONE,
625                     "%s can only be %d for primary zone.",
626                     ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
627         }
628         validateAudioZoneIdIsUnique(zoneId);
629         return zoneId;
630     }
631 
parseOccupantZoneId(int audioZoneId, XmlPullParser parser)632     private void parseOccupantZoneId(int audioZoneId, XmlPullParser parser) {
633         String occupantZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_OCCUPANT_ZONE_ID);
634         if (isVersionOne()) {
635             Preconditions.checkArgument(occupantZoneIdString == null,
636                     "Invalid audio attribute %s"
637                             + ", Please update car audio configurations file "
638                             + "to version to 2 to use it.", ATTR_OCCUPANT_ZONE_ID);
639             return;
640         }
641         //Occupant id not required for all zones
642         if (occupantZoneIdString == null) {
643             return;
644         }
645         int occupantZoneId = CarAudioParserUtils.parsePositiveIntAttribute(ATTR_OCCUPANT_ZONE_ID,
646                 occupantZoneIdString);
647         validateOccupantZoneIdIsUnique(occupantZoneId);
648         mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId);
649     }
650 
parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)651     private void parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)
652             throws IOException, XmlPullParserException {
653         if (isVersionOne()) {
654             throw new IllegalStateException(
655                     TAG_INPUT_DEVICES + " are not supported in car_audio_configuration.xml version "
656                             + SUPPORTED_VERSION_1);
657         }
658         while (parser.next() != XmlPullParser.END_TAG) {
659             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
660             if (Objects.equals(parser.getName(), TAG_INPUT_DEVICE)) {
661                 String audioDeviceAddress =
662                         parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
663                 validateInputAudioDeviceAddress(audioDeviceAddress);
664                 AudioDeviceInfo info =
665                         mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress);
666                 Preconditions.checkArgument(info != null,
667                         "%s %s of %s does not exist, add input device to"
668                                 + " audio_policy_configuration.xml.",
669                         ATTR_DEVICE_ADDRESS, audioDeviceAddress, TAG_INPUT_DEVICE);
670                 zone.addInputAudioDevice(new AudioDeviceAttributes(info));
671             }
672             CarAudioParserUtils.skip(parser);
673         }
674     }
675 
validateInputAudioDeviceAddress(String audioDeviceAddress)676     private void validateInputAudioDeviceAddress(String audioDeviceAddress) {
677         Objects.requireNonNull(audioDeviceAddress, () ->
678                 TAG_INPUT_DEVICE + " " + ATTR_DEVICE_ADDRESS + " attribute must be present.");
679         Preconditions.checkArgument(!audioDeviceAddress.isEmpty(),
680                 "%s %s attribute can not be empty.",
681                 TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS);
682         if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) {
683             throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress
684                     + " repeats, " + TAG_INPUT_DEVICES + " can not repeat.");
685         }
686         mAssignedInputAudioDevices.add(audioDeviceAddress);
687     }
688 
validateOccupantZoneIdIsUnique(int occupantZoneId)689     private void validateOccupantZoneIdIsUnique(int occupantZoneId) {
690         if (mZoneIdToOccupantZoneIdMapping.indexOfValue(occupantZoneId) > -1) {
691             throw new IllegalArgumentException(ATTR_OCCUPANT_ZONE_ID + " " + occupantZoneId
692                     + " is already associated with a zone");
693         }
694     }
695 
validateAudioZoneIdIsUnique(int audioZoneId)696     private void validateAudioZoneIdIsUnique(int audioZoneId) {
697         if (mAudioZoneIds.contains(audioZoneId)) {
698             throw new IllegalArgumentException(ATTR_ZONE_ID + " " + audioZoneId
699                     + " is already associated with a zone");
700         }
701         mAudioZoneIds.add(audioZoneId);
702     }
703 
parseZoneConfigs(XmlPullParser parser, CarAudioZone zone)704     private void parseZoneConfigs(XmlPullParser parser, CarAudioZone zone)
705             throws XmlPullParserException, IOException {
706         int zoneConfigId = 0;
707         while (parser.next() != XmlPullParser.END_TAG) {
708             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
709             if (Objects.equals(parser.getName(), TAG_AUDIO_ZONE_CONFIG)) {
710                 if (isVersionLessThanFour() && zone.getId() == PRIMARY_AUDIO_ZONE
711                         && zoneConfigId > 0) {
712                     throw new IllegalArgumentException(
713                             "Primary zone cannot have multiple zone configurations");
714                 }
715                 parseZoneConfig(parser, zone, zoneConfigId);
716                 zoneConfigId++;
717             } else {
718                 CarAudioParserUtils.skip(parser);
719             }
720         }
721     }
722 
parseZoneConfig(XmlPullParser parser, CarAudioZone zone, int zoneConfigId)723     private void parseZoneConfig(XmlPullParser parser, CarAudioZone zone, int zoneConfigId)
724             throws XmlPullParserException, IOException {
725         final boolean isDefault = Boolean.parseBoolean(
726                 parser.getAttributeValue(NAMESPACE, ATTR_IS_CONFIG_DEFAULT));
727         final String zoneConfigName = parser.getAttributeValue(NAMESPACE, ATTR_CONFIG_NAME);
728         validateAudioZoneConfigName(zoneConfigName);
729         final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder(
730                 zoneConfigName, zone.getId(), zoneConfigId, isDefault);
731         boolean valid = true;
732         zoneConfigBuilder.setFadeManagerConfigurationEnabled(mUseFadeManagerConfiguration);
733         while (parser.next() != XmlPullParser.END_TAG) {
734             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
735             // Expect at least one <volumeGroups> in one audio zone config
736             if (Objects.equals(parser.getName(), TAG_VOLUME_GROUPS)) {
737                 if (!parseVolumeGroups(parser, zoneConfigBuilder)) {
738                     valid = false;
739                 }
740             }  else if (Objects.equals(parser.getName(), TAG_APPLY_FADE_CONFIGS)) {
741                 parseApplyFadeConfigs(parser, zoneConfigBuilder);
742             } else {
743                 CarAudioParserUtils.skip(parser);
744             }
745         }
746         // If the configuration is not valid we can skip the config
747         if (!valid) {
748             String message = "Skipped configuration " + zoneConfigName + " in zone " + zone.getId();
749             Slogf.e(TAG, message);
750             mCarServiceLocalLog.log(message);
751             return;
752         }
753         zone.addZoneConfig(zoneConfigBuilder.build());
754     }
755 
validateAudioZoneConfigName(String configName)756     private void validateAudioZoneConfigName(String configName) {
757         Objects.requireNonNull(configName, TAG_AUDIO_ZONE_CONFIG + " " + ATTR_CONFIG_NAME
758                         + " attribute must be present.");
759         Preconditions.checkArgument(!configName.isEmpty(),
760                 "%s %s attribute can not be empty.",
761                 TAG_AUDIO_ZONE_CONFIG, ATTR_CONFIG_NAME);
762         if (mAudioZoneConfigNames.contains(configName)) {
763             throw new IllegalArgumentException(ATTR_CONFIG_NAME + " " + configName
764                             + " repeats, " + ATTR_CONFIG_NAME + " can not repeat.");
765         }
766         mAudioZoneConfigNames.add(configName);
767     }
768 
parseVolumeGroups(XmlPullParser parser, CarAudioZoneConfig.Builder zoneConfigBuilder)769     private boolean parseVolumeGroups(XmlPullParser parser,
770             CarAudioZoneConfig.Builder zoneConfigBuilder)
771             throws XmlPullParserException, IOException {
772         int groupId = 0;
773         boolean valid = true;
774         while (parser.next() != XmlPullParser.END_TAG) {
775             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
776             if (Objects.equals(parser.getName(), TAG_VOLUME_GROUP)) {
777                 String groupName = parser.getAttributeValue(NAMESPACE, VOLUME_GROUP_NAME);
778                 groupName = verifyGroupNameAndCreateIfNeeded(zoneConfigBuilder, groupName, groupId);
779 
780                 CarActivationVolumeConfig activationVolumeConfig =
781                         getActivationVolumeConfig(parser);
782 
783                 CarVolumeGroupFactory factory = new CarVolumeGroupFactory(mAudioManager,
784                         mCarAudioSettings, mCarAudioContext, zoneConfigBuilder.getZoneId(),
785                         zoneConfigBuilder.getZoneConfigId(), groupId, groupName,
786                         mUseCarVolumeGroupMute, activationVolumeConfig);
787 
788                 if (!parseVolumeGroup(parser, factory)) {
789                     valid = false;
790                 }
791                 if (!valid) {
792                     continue;
793                 }
794                 zoneConfigBuilder.addVolumeGroup(factory.getCarVolumeGroup(mUseCoreAudioVolume));
795                 groupId++;
796             } else {
797                 CarAudioParserUtils.skip(parser);
798             }
799         }
800         return valid;
801     }
802 
verifyGroupNameAndCreateIfNeeded( CarAudioZoneConfig.Builder zoneConfigBuilder, String groupName, int groupId)803     private String verifyGroupNameAndCreateIfNeeded(
804             CarAudioZoneConfig.Builder zoneConfigBuilder, String groupName, int groupId) {
805         verifyGroupName(groupName);
806         if (groupName == null) {
807             groupName = "config " + zoneConfigBuilder.getZoneConfigId() + " group " + groupId;
808         }
809         return groupName;
810     }
811 
verifyGroupName(String groupName)812     private void verifyGroupName(String groupName) {
813         if (!Flags.audioVendorFreezeImprovements()) {
814             Preconditions.checkArgument(!mUseCoreAudioVolume || groupName != null,
815                     "%s %s attribute can not be empty when relying on core volume groups",
816                     TAG_VOLUME_GROUP, VOLUME_GROUP_NAME);
817             return;
818         }
819         if (!mUseCoreAudioVolume || groupName != null) {
820             return;
821         }
822         mCarServiceLocalLog.log("Found " + TAG_VOLUME_GROUP + " " + VOLUME_GROUP_NAME
823                 + " empty while relying on core volume groups,"
824                 + " resetting use core volume to false");
825         mUseCoreAudioVolume = false;
826     }
827 
getActivationVolumeConfig(XmlPullParser parser)828     private CarActivationVolumeConfig getActivationVolumeConfig(XmlPullParser parser) {
829         String activationVolumeConfigName = parser.getAttributeValue(NAMESPACE,
830                 ATTR_ACTIVATION_VOLUME_CONFIG);
831         if (activationVolumeConfigName != null) {
832             if (isVersionLessThanFour()) {
833                 throw new IllegalArgumentException(TAG_VOLUME_GROUP + " "
834                         + ATTR_ACTIVATION_VOLUME_CONFIG
835                         + " attribute not supported for versions less than "
836                         + SUPPORTED_VERSION_4 + ", but current version is "
837                         + mCurrentVersion);
838             }
839             if (!mConfigNameToActivationVolumeConfig
840                     .containsKey(activationVolumeConfigName)) {
841                 throw new IllegalArgumentException(TAG_ACTIVATION_VOLUME_CONFIG + " with "
842                         + ATTR_ACTIVATION_VOLUME_CONFIG_NAME + " attribute of "
843                         + activationVolumeConfigName + " does not exist");
844             }
845             if (Flags.carAudioMinMaxActivationVolume()) {
846                 return mConfigNameToActivationVolumeConfig.get(activationVolumeConfigName);
847             }
848         }
849         if (!Flags.carAudioMinMaxActivationVolume()) {
850             mCarServiceLocalLog.log("Found " + TAG_VOLUME_GROUP + " "
851                     + ATTR_ACTIVATION_VOLUME_CONFIG
852                     + " attribute while min/max activation volume is disabled");
853         }
854         return DEFAULT_ACTIVATION_VOLUME;
855     }
856 
parseActivationVolumeConfigs(XmlPullParser parser)857     private void parseActivationVolumeConfigs(XmlPullParser parser)
858             throws XmlPullParserException, IOException {
859         while (parser.next() != XmlPullParser.END_TAG) {
860             if (parser.getEventType() != XmlPullParser.START_TAG) {
861                 continue;
862             }
863             if (Objects.equals(parser.getName(), TAG_ACTIVATION_VOLUME_CONFIG)) {
864                 parseActivationVolumeConfig(parser);
865             } else {
866                 CarAudioParserUtils.skip(parser);
867             }
868         }
869     }
870 
parseActivationVolumeConfig(XmlPullParser parser)871     private void parseActivationVolumeConfig(XmlPullParser parser)
872             throws XmlPullParserException, IOException {
873         String activationConfigName = parser.getAttributeValue(NAMESPACE,
874                 ATTR_ACTIVATION_VOLUME_CONFIG_NAME);
875         Objects.requireNonNull(activationConfigName, TAG_ACTIVATION_VOLUME_CONFIG + " "
876                 + ATTR_ACTIVATION_VOLUME_CONFIG_NAME + " attribute must be present.");
877         if (mConfigNameToActivationVolumeConfig.containsKey(activationConfigName)) {
878             throw new IllegalArgumentException(ATTR_ACTIVATION_VOLUME_CONFIG_NAME + " "
879                     + activationConfigName + " repeats, " + ATTR_ACTIVATION_VOLUME_CONFIG_NAME
880                     + " can not repeat.");
881         }
882         int configEntryCount = 0;
883         while (parser.next() != XmlPullParser.END_TAG) {
884             if (parser.getEventType() != XmlPullParser.START_TAG) {
885                 continue;
886             }
887             if (Objects.equals(parser.getName(), TAG_ACTIVATION_VOLUME_CONFIG_ENTRY)) {
888                 if (configEntryCount == 0) {
889                     CarActivationVolumeConfig config = parseActivationVolumeConfigEntry(parser);
890                     mConfigNameToActivationVolumeConfig.put(activationConfigName, config);
891                     configEntryCount++;
892                 } else {
893                     throw new IllegalArgumentException("Multiple "
894                             + TAG_ACTIVATION_VOLUME_CONFIG_ENTRY + "s in one "
895                             + TAG_ACTIVATION_VOLUME_CONFIG + " is not supported");
896                 }
897             }
898             CarAudioParserUtils.skip(parser);
899         }
900     }
901 
parseActivationVolumeConfigEntry(XmlPullParser parser)902     private CarActivationVolumeConfig parseActivationVolumeConfigEntry(XmlPullParser parser) {
903         int activationVolumeInvocationTypes = parseVolumeGroupActivationVolumeTypes(parser);
904         int minActivationVolumePercentage = parseVolumeGroupActivationVolumePercentage(parser,
905                 /* isMax= */ false);
906         int maxActivationVolumePercentage = parseVolumeGroupActivationVolumePercentage(parser,
907                 /* isMax= */ true);
908         validateMinMaxActivationVolume(maxActivationVolumePercentage,
909                 minActivationVolumePercentage);
910         return new CarActivationVolumeConfig(activationVolumeInvocationTypes,
911                 minActivationVolumePercentage, maxActivationVolumePercentage);
912     }
913 
parseVolumeGroupActivationVolumePercentage(XmlPullParser parser, boolean isMax)914     private int parseVolumeGroupActivationVolumePercentage(XmlPullParser parser, boolean isMax) {
915         int defaultValue = isMax ? ACTIVATION_VOLUME_PERCENTAGE_MAX
916                 : ACTIVATION_VOLUME_PERCENTAGE_MIN;
917         String attr = isMax ? ATTR_MAX_ACTIVATION_VOLUME_PERCENTAGE
918                 : ATTR_MIN_ACTIVATION_VOLUME_PERCENTAGE;
919         String activationPercentageString = parser.getAttributeValue(NAMESPACE, attr);
920         if (activationPercentageString == null) {
921             return defaultValue;
922         }
923         return CarAudioParserUtils.parsePositiveIntAttribute(attr, activationPercentageString);
924     }
925 
parseVolumeGroupActivationVolumeTypes(XmlPullParser parser)926     private int parseVolumeGroupActivationVolumeTypes(XmlPullParser parser) {
927         String activationPercentageString = parser.getAttributeValue(NAMESPACE,
928                 ATTR_ACTIVATION_VOLUME_INVOCATION_TYPE);
929         if (activationPercentageString == null) {
930             return ACTIVATION_VOLUME_INVOCATION_TYPE;
931         }
932         if (Objects.equals(activationPercentageString, ACTIVATION_VOLUME_INVOCATION_TYPE_ON_BOOT)) {
933             return CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_BOOT;
934         } else if (Objects.equals(activationPercentageString,
935                 ACTIVATION_VOLUME_INVOCATION_TYPE_ON_SOURCE_CHANGED)) {
936             return CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_BOOT
937                     | CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_SOURCE_CHANGED;
938         } else if (Objects.equals(activationPercentageString,
939                 ACTIVATION_VOLUME_INVOCATION_TYPE_ON_PLAYBACK_CHANGED)) {
940             return CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_BOOT
941                     | CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_SOURCE_CHANGED
942                     | CarActivationVolumeConfig.ACTIVATION_VOLUME_ON_PLAYBACK_CHANGED;
943         } else {
944             throw new IllegalArgumentException(" Value " + activationPercentageString
945                     + " is invalid for " + TAG_VOLUME_GROUP + " "
946                     + ATTR_ACTIVATION_VOLUME_INVOCATION_TYPE);
947         }
948     }
949 
parseVolumeGroup(XmlPullParser parser, CarVolumeGroupFactory groupFactory)950     private boolean parseVolumeGroup(XmlPullParser parser, CarVolumeGroupFactory groupFactory)
951             throws XmlPullParserException, IOException {
952         boolean valid = true;
953         while (parser.next() != XmlPullParser.END_TAG) {
954             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
955             if (Objects.equals(parser.getName(), TAG_AUDIO_DEVICE)) {
956                 if (!parseVolumeGroupDeviceToContextMapping(parser, groupFactory)) {
957                     valid = false;
958                 }
959             } else {
960                 CarAudioParserUtils.skip(parser);
961             }
962         }
963         return valid;
964     }
965 
parseVolumeGroupDeviceToContextMapping(XmlPullParser parser, CarVolumeGroupFactory groupFactory)966     private boolean parseVolumeGroupDeviceToContextMapping(XmlPullParser parser,
967             CarVolumeGroupFactory groupFactory) throws XmlPullParserException, IOException {
968         String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
969         int type = parseAudioDeviceType(parser);
970         boolean valid = validateOutputAudioDevice(address, type);
971         parseVolumeGroupContexts(parser, groupFactory, address, type);
972         return valid;
973     }
974 
parseAudioDeviceType(XmlPullParser parser)975     private int parseAudioDeviceType(XmlPullParser parser) {
976         String typeString = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_TYPE);
977         if (typeString == null) {
978             return TYPE_BUS;
979         }
980         if (isVersionLessThanFour()) {
981             throw new IllegalArgumentException("Audio device type " + typeString
982                     + " not supported for versions less than " + SUPPORTED_VERSION_4
983                     + ", but current version is " + mCurrentVersion);
984         }
985         return ConstantDebugUtils.toValue(AudioDeviceInfo.class, typeString);
986     }
987 
parseApplyFadeConfigs(XmlPullParser parser, CarAudioZoneConfig.Builder zoneConfigBuilder)988     private void parseApplyFadeConfigs(XmlPullParser parser,
989             CarAudioZoneConfig.Builder zoneConfigBuilder)
990             throws XmlPullParserException, IOException {
991         Preconditions.checkArgument(!isVersionLessThanFour(),
992                 "Fade configurations not available in versions < 4");
993         // ignore fade configurations when feature is not enabled
994         if (!mUseFadeManagerConfiguration) {
995             String message = "Skipped applying fade configs since feature flag is disabled";
996             Slogf.i(TAG, message);
997             mCarServiceLocalLog.log(message);
998             CarAudioParserUtils.skip(parser);
999             return;
1000         }
1001 
1002         Objects.requireNonNull(mCarAudioFadeConfigurationHelper,
1003                 "Car audio fade configuration helper can not be null when parsing fade configs");
1004         while (parser.next() != XmlPullParser.END_TAG) {
1005             if (parser.getEventType() != XmlPullParser.START_TAG) {
1006                 continue;
1007             }
1008             if (Objects.equals(parser.getName(), FADE_CONFIG)) {
1009                 parseFadeConfig(parser, zoneConfigBuilder);
1010             } else {
1011                 CarAudioParserUtils.skip(parser);
1012             }
1013         }
1014     }
1015 
parseFadeConfig(XmlPullParser parser, CarAudioZoneConfig.Builder zoneConfigBuilder)1016     private void parseFadeConfig(XmlPullParser parser,
1017             CarAudioZoneConfig.Builder zoneConfigBuilder)
1018             throws XmlPullParserException, IOException {
1019         String name = parser.getAttributeValue(NAMESPACE, FADE_CONFIG_NAME);
1020         boolean isDefault = Boolean.parseBoolean(
1021                 parser.getAttributeValue(NAMESPACE, FADE_CONFIG_IS_DEFAULT));
1022         validateFadeConfig(name);
1023         CarAudioFadeConfiguration afc =
1024                 mCarAudioFadeConfigurationHelper.getCarAudioFadeConfiguration(name);
1025         if (isDefault) {
1026             zoneConfigBuilder.setDefaultCarAudioFadeConfiguration(afc);
1027             CarAudioParserUtils.skip(parser);
1028         } else {
1029             parseTransientFadeConfigs(parser, zoneConfigBuilder, afc);
1030         }
1031     }
1032 
parseTransientFadeConfigs(XmlPullParser parser, CarAudioZoneConfig.Builder zoneConfigBuilder, CarAudioFadeConfiguration carAudioFadeConfiguration)1033     private void parseTransientFadeConfigs(XmlPullParser parser,
1034             CarAudioZoneConfig.Builder zoneConfigBuilder,
1035             CarAudioFadeConfiguration carAudioFadeConfiguration)
1036             throws XmlPullParserException, IOException {
1037         boolean hasAudioAttributes = false;
1038         while (parser.next() != XmlPullParser.END_TAG) {
1039             if (parser.getEventType() != XmlPullParser.START_TAG) {
1040                 continue;
1041             }
1042             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES)) {
1043                 List<AudioAttributes> attributes =
1044                         CarAudioParserUtils.parseAudioAttributes(parser,
1045                                 CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES);
1046                 for (int index = 0; index < attributes.size(); index++) {
1047                     hasAudioAttributes = true;
1048                     zoneConfigBuilder.setCarAudioFadeConfigurationForAudioAttributes(
1049                             attributes.get(index), carAudioFadeConfiguration);
1050                 }
1051             } else {
1052                 CarAudioParserUtils.skip(parser);
1053             }
1054         }
1055         Preconditions.checkArgument(hasAudioAttributes,
1056                 "Transient fade configs must have valid audio attributes");
1057     }
1058 
validateFadeConfig(String name)1059     private void validateFadeConfig(String name) {
1060         Objects.requireNonNull(name, "Fade config name cannot be null");
1061         Preconditions.checkArgument(!name.isEmpty(), "Fade config name cannot be empty");
1062         Preconditions.checkArgument(mCarAudioFadeConfigurationHelper.isConfigAvailable(name),
1063                 "No config available for name:%s", name);
1064     }
1065 
isVersionLessThanFour()1066     private boolean isVersionLessThanFour() {
1067         return mCurrentVersion < SUPPORTED_VERSION_4;
1068     }
1069 
isVersionFourOrGreater()1070     private boolean isVersionFourOrGreater() {
1071         return mCurrentVersion >= SUPPORTED_VERSION_4;
1072     }
1073 
validateMinMaxActivationVolume(int maxActivationVolume, int minActivationVolume)1074     private void validateMinMaxActivationVolume(int maxActivationVolume,
1075                                                 int minActivationVolume) {
1076         if (!Flags.carAudioMinMaxActivationVolume()) {
1077             return;
1078         }
1079         Preconditions.checkArgument(maxActivationVolume >= ACTIVATION_VOLUME_PERCENTAGE_MIN
1080                         && maxActivationVolume <= ACTIVATION_VOLUME_PERCENTAGE_MAX,
1081                 "%s %s attribute is %s but can not be outside the range (%s,%s)",
1082                 TAG_VOLUME_GROUP, ATTR_MAX_ACTIVATION_VOLUME_PERCENTAGE, maxActivationVolume,
1083                 ACTIVATION_VOLUME_PERCENTAGE_MIN, ACTIVATION_VOLUME_PERCENTAGE_MAX);
1084         Preconditions.checkArgument(minActivationVolume >= ACTIVATION_VOLUME_PERCENTAGE_MIN
1085                         && minActivationVolume <= ACTIVATION_VOLUME_PERCENTAGE_MAX,
1086                 "%s %s attribute is %s but can not be outside the range (%s,%s)",
1087                 TAG_VOLUME_GROUP, ATTR_MIN_ACTIVATION_VOLUME_PERCENTAGE, minActivationVolume,
1088                 ACTIVATION_VOLUME_PERCENTAGE_MIN, ACTIVATION_VOLUME_PERCENTAGE_MAX);
1089         Preconditions.checkArgument(minActivationVolume < maxActivationVolume,
1090                 "%s %s is %s but can not be larger than or equal to %s attribute %s",
1091                 TAG_VOLUME_GROUP, ATTR_MIN_ACTIVATION_VOLUME_PERCENTAGE, minActivationVolume,
1092                 ATTR_MAX_ACTIVATION_VOLUME_PERCENTAGE, maxActivationVolume);
1093     }
1094 
1095     private boolean validateOutputAudioDevice(String address, int type) {
1096         if (!Flags.carAudioDynamicDevices() && TextUtils.isEmpty(address)) {
1097             // If the version is four or greater, we can only return that the output device is not
1098             // valid since we can not crash. The configuration will only skip reading configuration.
1099             if (isVersionFourOrGreater()) {
1100                 mCarServiceLocalLog.log("Found invalid device while dynamic device is disabled,"
1101                         + " device address is empty for device type "
1102                         + DebugUtils.constantToString(AudioDeviceInfo.class,
1103                         /* prefix= */ "TYPE_", type));
1104                 return false;
1105             }
1106             throw new IllegalStateException("Output device address must be specified");
1107         }
1108 
1109         if (!isValidAudioDeviceTypeOut(type)) {
1110             // If the version is four or greater, we can only return that the output device is not
1111             // valid since we can not crash. The configuration will only skip reading configuration.
1112             if (isVersionFourOrGreater() && !Flags.carAudioDynamicDevices()) {
1113                 mCarServiceLocalLog.log("Found invalid device type while dynamic device is"
1114                         + " disabled, device address " + address + " and device type "
1115                         + DebugUtils.constantToString(AudioDeviceInfo.class,
1116                         /* prefix= */ "TYPE_", type));
1117                 return false;
1118             }
1119             throw new IllegalStateException("Output device type " + DebugUtils.constantToString(
1120                     AudioDeviceInfo.class, /* prefix= */ "TYPE_", type) + " is not valid");
1121         }
1122 
1123         boolean requiresDeviceAddress = type == TYPE_BUS;
1124         if (requiresDeviceAddress && !mAddressToCarAudioDeviceInfo.containsKey(address)) {
1125             throw new IllegalStateException("Output device address " + address
1126                     + " does not belong to any configured output device.");
1127         }
1128         if (CarAudioUtils.isDynamicDeviceType(type) && !mUseCoreAudioVolume) {
1129             mCarServiceLocalLog.log("Found invalid device setup,"
1130                     + " dynamic device " + DebugUtils.constantToString(AudioDeviceInfo.class,
1131                     /* prefix= */ "TYPE_", type) + " requires core audio volume management"
1132                     + " but audioUseCoreVolume is false.");
1133             return false;
1134         }
1135         return true;
1136     }
1137 
1138     private void parseVolumeGroupContexts(
1139             XmlPullParser parser, CarVolumeGroupFactory groupFactory, String address, int type)
1140             throws XmlPullParserException, IOException {
1141         while (parser.next() != XmlPullParser.END_TAG) {
1142             if (parser.getEventType() != XmlPullParser.START_TAG) continue;
1143             if (Objects.equals(parser.getName(), TAG_CONTEXT)) {
1144                 @AudioContext int carAudioContextId = parseCarAudioContextId(
1145                         parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME));
1146                 validateCarAudioContextSupport(carAudioContextId);
1147                 CarAudioDeviceInfo info = getCarAudioDeviceInfo(address, type);
1148                 groupFactory.setDeviceInfoForContext(carAudioContextId, info);
1149 
1150                 // If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE
1151                 if (isVersionOne() && carAudioContextId == mCarAudioContext
1152                         .getContextForAudioAttribute(CAR_DEFAULT_AUDIO_ATTRIBUTE)) {
1153                     groupFactory.setNonLegacyContexts(info);
1154                 }
1155             }
1156             // Always skip to upper level since we're at the lowest.
1157             CarAudioParserUtils.skip(parser);
1158         }
1159     }
1160 
1161     private CarAudioDeviceInfo getCarAudioDeviceInfo(String address, int type) {
1162         if (type == TYPE_BUS) {
1163             return mAddressToCarAudioDeviceInfo.get(address);
1164         }
1165 
1166         String newAddress = address == null ? "" : address;
1167         return new CarAudioDeviceInfo(mAudioManager,
1168                 new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, type, newAddress));
1169     }
1170 
1171     private boolean isVersionLessThanThree() {
1172         return mCurrentVersion < SUPPORTED_VERSION_3;
1173     }
1174 
1175     private boolean isVersionOne() {
1176         return mCurrentVersion == SUPPORTED_VERSION_1;
1177     }
1178 
1179     private @AudioContext int parseCarAudioContextId(String context) {
1180         return mContextNameToId.getOrDefault(context.toLowerCase(ROOT),
1181                 CarAudioContext.getInvalidContext());
1182     }
1183 
1184     private void validateCarAudioContextSupport(@AudioContext int audioContext) {
1185         if (isVersionOne() && CarAudioContext.getCarSystemContextIds().contains(audioContext)) {
1186             throw new IllegalArgumentException("Non-legacy audio contexts such as "
1187                     + mCarAudioContext.toString(audioContext) + " are not supported in "
1188                     + "car_audio_configuration.xml version " + SUPPORTED_VERSION_1);
1189         }
1190     }
1191 
1192     private int getNextSecondaryZoneId() {
1193         int zoneId = mNextSecondaryZoneId;
1194         mNextSecondaryZoneId += 1;
1195         return zoneId;
1196     }
1197 
1198     public CarAudioContext getCarAudioContext() {
1199         return mCarAudioContext;
1200     }
1201 
1202     public List<CarAudioDeviceInfo> getMirrorDeviceInfos() {
1203         return mMirroringDevices;
1204     }
1205 
1206     /**
1207      * Car audio service supports a subset of the output devices, including most dynamic
1208      * devices, built in speaker, and bus devices.
1209      */
1210     private static boolean isValidAudioDeviceTypeOut(int type) {
1211         if (!Flags.carAudioDynamicDevices()) {
1212             return type == TYPE_BUS;
1213         }
1214 
1215         switch (type) {
1216             case TYPE_BUILTIN_SPEAKER:
1217             case TYPE_WIRED_HEADSET:
1218             case TYPE_WIRED_HEADPHONES:
1219             case TYPE_BLUETOOTH_A2DP:
1220             case TYPE_HDMI:
1221             case TYPE_USB_ACCESSORY:
1222             case TYPE_USB_DEVICE:
1223             case TYPE_USB_HEADSET:
1224             case TYPE_AUX_LINE:
1225             case TYPE_BUS:
1226             case TYPE_BLE_HEADSET:
1227             case TYPE_BLE_SPEAKER:
1228             case TYPE_BLE_BROADCAST:
1229                 return true;
1230             default:
1231                 return false;
1232         }
1233     }
1234 }
1235