blob: 1fce3e50f15d8ec23bcae05bbc9d56fc6f4fd613 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.audio;
import static android.car.media.CarAudioManager.PRIMARY_AUDIO_ZONE;
import static android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
import static com.android.car.audio.CarAudioService.CAR_DEFAULT_AUDIO_ATTRIBUTE;
import static com.android.car.audio.CarAudioUtils.isMicrophoneInputDevice;
import static java.util.Locale.ROOT;
import android.annotation.NonNull;
import android.car.builtin.media.AudioManagerHelper;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.media.audiopolicy.AudioProductStrategy;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.Xml;
import com.android.car.audio.CarAudioContext.AudioContext;
import com.android.internal.util.Preconditions;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* A helper class loads all audio zones from the configuration XML file.
*/
/* package */ final class CarAudioZonesHelper {
private static final String NAMESPACE = null;
private static final String TAG_ROOT = "carAudioConfiguration";
private static final String TAG_OEM_CONTEXTS = "oemContexts";
private static final String TAG_OEM_CONTEXT = "oemContext";
private static final String TAG_AUDIO_ATTRIBUTES = "audioAttributes";
private static final String TAG_AUDIO_ATTRIBUTE = "audioAttribute";
private static final String TAG_USAGE = "usage";
private static final String ATTR_USAGE_VALUE = "value";
private static final String ATTR_NAME = "name";
private static final String ATTR_CONTENT_TYPE = "contentType";
private static final String ATTR_USAGE = "usage";
private static final String ATTR_BUNDLE = "bundle";
private static final String ATTR_TAGS = "tags";
private static final String TAG_AUDIO_ZONES = "zones";
private static final String TAG_AUDIO_ZONE = "zone";
private static final String TAG_AUDIO_ZONE_CONFIGS = "zoneConfigs";
private static final String TAG_AUDIO_ZONE_CONFIG = "zoneConfig";
private static final String TAG_VOLUME_GROUPS = "volumeGroups";
private static final String TAG_VOLUME_GROUP = "group";
private static final String TAG_AUDIO_DEVICE = "device";
private static final String TAG_CONTEXT = "context";
private static final String ATTR_VERSION = "version";
private static final String ATTR_IS_PRIMARY = "isPrimary";
private static final String ATTR_IS_CONFIG_DEFAULT = "isDefault";
private static final String ATTR_ZONE_NAME = "name";
private static final String ATTR_CONFIG_NAME = "name";
private static final String ATTR_DEVICE_ADDRESS = "address";
private static final String ATTR_CONTEXT_NAME = "context";
private static final String ATTR_ZONE_ID = "audioZoneId";
private static final String ATTR_OCCUPANT_ZONE_ID = "occupantZoneId";
private static final String TAG_INPUT_DEVICES = "inputDevices";
private static final String TAG_INPUT_DEVICE = "inputDevice";
private static final String TAG_MIRRORING_DEVICES = "mirroringDevices";
private static final String TAG_MIRRORING_DEVICE = "mirroringDevice";
private static final int INVALID_VERSION = -1;
private static final int SUPPORTED_VERSION_1 = 1;
private static final int SUPPORTED_VERSION_2 = 2;
private static final int SUPPORTED_VERSION_3 = 3;
private static final SparseIntArray SUPPORTED_VERSIONS;
static {
SUPPORTED_VERSIONS = new SparseIntArray(3);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_1, SUPPORTED_VERSION_1);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_2, SUPPORTED_VERSION_2);
SUPPORTED_VERSIONS.put(SUPPORTED_VERSION_3, SUPPORTED_VERSION_3);
}
private final AudioManager mAudioManager;
private final CarAudioSettings mCarAudioSettings;
private final List<CarAudioContextInfo> mCarAudioContextInfos = new ArrayList<>();
private final Map<String, CarAudioDeviceInfo> mAddressToCarAudioDeviceInfo;
private final Map<String, AudioDeviceInfo> mAddressToInputAudioDeviceInfoForAllInputDevices;
private final InputStream mInputStream;
private final SparseIntArray mZoneIdToOccupantZoneIdMapping;
private final Set<Integer> mAudioZoneIds;
private final Set<String> mAssignedInputAudioDevices;
private final Set<String> mAudioZoneConfigNames;
private final boolean mUseCarVolumeGroupMute;
private final boolean mUseCoreAudioVolume;
private final boolean mUseCoreAudioRouting;
private final List<CarAudioDeviceInfo> mMirroringDevices = new ArrayList<>();
private final ArrayMap<String, Integer> mContextNameToId = new ArrayMap<>();
private CarAudioContext mCarAudioContext;
private int mNextSecondaryZoneId;
private int mCurrentVersion;
/**
* <p><b>Note: <b/> CarAudioZonesHelper is expected to be used from a single thread. This
* should be the same thread that originally called new CarAudioZonesHelper.
*/
CarAudioZonesHelper(AudioManager audioManager,
@NonNull CarAudioSettings carAudioSettings,
@NonNull InputStream inputStream,
@NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos,
@NonNull AudioDeviceInfo[] inputDeviceInfo, boolean useCarVolumeGroupMute,
boolean useCoreAudioVolume, boolean useCoreAudioRouting) {
mAudioManager = Objects.requireNonNull(audioManager,
"Audio manager cannot be null");
mCarAudioSettings = Objects.requireNonNull(carAudioSettings);
mInputStream = Objects.requireNonNull(inputStream);
Objects.requireNonNull(carAudioDeviceInfos);
Objects.requireNonNull(inputDeviceInfo);
mAddressToCarAudioDeviceInfo = CarAudioZonesHelper.generateAddressToInfoMap(
carAudioDeviceInfos);
mAddressToInputAudioDeviceInfoForAllInputDevices =
CarAudioZonesHelper.generateAddressToInputAudioDeviceInfoMap(inputDeviceInfo);
mNextSecondaryZoneId = PRIMARY_AUDIO_ZONE + 1;
mZoneIdToOccupantZoneIdMapping = new SparseIntArray();
mAudioZoneIds = new ArraySet<>();
mAssignedInputAudioDevices = new ArraySet<>();
mAudioZoneConfigNames = new ArraySet<>();
mUseCarVolumeGroupMute = useCarVolumeGroupMute;
mUseCoreAudioVolume = useCoreAudioVolume;
mUseCoreAudioRouting = useCoreAudioRouting;
}
SparseIntArray getCarAudioZoneIdToOccupantZoneIdMapping() {
return mZoneIdToOccupantZoneIdMapping;
}
SparseArray<CarAudioZone> loadAudioZones() throws IOException, XmlPullParserException {
return parseCarAudioZones(mInputStream);
}
private static Map<String, CarAudioDeviceInfo> generateAddressToInfoMap(
List<CarAudioDeviceInfo> carAudioDeviceInfos) {
return carAudioDeviceInfos.stream()
.filter(info -> !TextUtils.isEmpty(info.getAddress()))
.collect(Collectors.toMap(CarAudioDeviceInfo::getAddress, info -> info));
}
private static Map<String, AudioDeviceInfo> generateAddressToInputAudioDeviceInfoMap(
@NonNull AudioDeviceInfo[] inputAudioDeviceInfos) {
Map<String, AudioDeviceInfo> deviceAddressToInputDeviceMap =
new ArrayMap<>(inputAudioDeviceInfos.length);
for (int i = 0; i < inputAudioDeviceInfos.length; ++i) {
AudioDeviceInfo device = inputAudioDeviceInfos[i];
if (device.isSource()) {
deviceAddressToInputDeviceMap.put(device.getAddress(), device);
}
}
return deviceAddressToInputDeviceMap;
}
private SparseArray<CarAudioZone> parseCarAudioZones(InputStream stream)
throws XmlPullParserException, IOException {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null);
parser.setInput(stream, null);
// Ensure <carAudioConfiguration> is the root
parser.nextTag();
parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT);
// Version check
final int versionNumber = Integer.parseInt(
parser.getAttributeValue(NAMESPACE, ATTR_VERSION));
if (SUPPORTED_VERSIONS.get(versionNumber, INVALID_VERSION) == INVALID_VERSION) {
throw new IllegalArgumentException("Latest Supported version:"
+ SUPPORTED_VERSION_3 + " , got version:" + versionNumber);
}
mCurrentVersion = versionNumber;
// Get all zones configured under <zones> tag
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_OEM_CONTEXTS.equals(parser.getName())) {
parseCarAudioContexts(parser);
} else if (TAG_MIRRORING_DEVICES.equals(parser.getName())) {
parseMirroringDevices(parser);
} else if (TAG_AUDIO_ZONES.equals(parser.getName())) {
loadCarAudioContexts();
return parseAudioZones(parser);
} else {
skip(parser);
}
}
throw new MissingResourceException(TAG_AUDIO_ZONES + " is missing from configuration",
"", TAG_AUDIO_ZONES);
}
private void parseMirroringDevices(XmlPullParser parser)
throws XmlPullParserException, IOException {
if (isVersionLessThanThree()) {
throw new IllegalStateException(
TAG_MIRRORING_DEVICES + " are not supported in car_audio_configuration.xml"
+ " version " + mCurrentVersion + ". Must be at least version "
+ SUPPORTED_VERSION_3);
}
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (TAG_MIRRORING_DEVICE.equals(parser.getName())) {
parseMirroringDevice(parser);
}
skip(parser);
}
}
private void parseMirroringDevice(XmlPullParser parser) {
String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
validateOutputDeviceExist(address);
CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
if (mMirroringDevices.contains(info)) {
throw new IllegalArgumentException(TAG_MIRRORING_DEVICE + " " + address
+ " repeats, " + TAG_MIRRORING_DEVICES + " can not repeat.");
}
mMirroringDevices.add(info);
}
private void loadCarAudioContexts() {
if (isVersionLessThanThree() || mCarAudioContextInfos.isEmpty()) {
mCarAudioContextInfos.addAll(CarAudioContext.getAllContextsInfo());
}
for (int index = 0; index < mCarAudioContextInfos.size(); index++) {
CarAudioContextInfo info = mCarAudioContextInfos.get(index);
mContextNameToId.put(info.getName().toLowerCase(ROOT), info.getId());
}
mCarAudioContext = new CarAudioContext(mCarAudioContextInfos, mUseCoreAudioRouting);
}
private void parseCarAudioContexts(XmlPullParser parser)
throws XmlPullParserException, IOException {
int contextId = 0;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (TAG_OEM_CONTEXT.equals(parser.getName())) {
parseCarAudioContext(parser, contextId);
contextId++;
} else {
skip(parser);
}
}
}
private void parseCarAudioContext(XmlPullParser parser, int contextId)
throws XmlPullParserException, IOException {
String contextName = parser.getAttributeValue(NAMESPACE, ATTR_NAME);
CarAudioContextInfo context = null;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (TAG_AUDIO_ATTRIBUTES.equals(parser.getName())) {
List<AudioAttributes> attributes = parseAudioAttributes(parser);
if (mUseCoreAudioRouting) {
contextId = CoreAudioHelper.getStrategyForAudioAttributes(attributes.get(0));
if (contextId == CoreAudioHelper.INVALID_STRATEGY) {
throw new RuntimeException("Cannot find strategy id for context: "
+ contextName);
}
}
validateCarAudioContextAttributes(contextId, attributes, contextName);
context = new CarAudioContextInfo(attributes.toArray(new AudioAttributes[0]),
contextName, contextId);
mCarAudioContextInfos.add(context);
} else {
skip(parser);
}
}
}
private void validateCarAudioContextAttributes(int contextId, List<AudioAttributes> attributes,
String contextName) {
if (!mUseCoreAudioRouting) {
return;
}
AudioProductStrategy strategy = CoreAudioHelper.getStrategy(contextId);
Preconditions.checkNotNull(strategy, "No strategy for context id = %d", contextId);
for (int index = 0; index < attributes.size(); index++) {
AudioAttributes aa = attributes.get(index);
if (!strategy.supportsAudioAttributes(aa)
&& !CoreAudioHelper.isDefaultStrategy(strategy.getId())) {
throw new RuntimeException(" Invalid attributes " + aa + " for context: "
+ contextName);
}
}
}
private List<AudioAttributes> parseAudioAttributes(XmlPullParser parser)
throws XmlPullParserException, IOException {
List<AudioAttributes> supportedAttributes = new ArrayList<>();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (TAG_USAGE.equals(parser.getName())) {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
parseUsage(parser, attributesBuilder, ATTR_USAGE_VALUE);
AudioAttributes attributes = attributesBuilder.build();
supportedAttributes.add(attributes);
} else if (TAG_AUDIO_ATTRIBUTE.equals(parser.getName())) {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
parseUsage(parser, attributesBuilder, ATTR_USAGE);
parseContentType(parser, attributesBuilder);
parseTags(parser, attributesBuilder);
AudioAttributes attributes = attributesBuilder.build();
supportedAttributes.add(attributes);
}
// Always skip to upper level since we're at the lowest.
skip(parser);
}
return supportedAttributes;
}
private void parseUsage(XmlPullParser parser, AudioAttributes.Builder builder, String attrValue)
throws XmlPullParserException, IOException {
String usageLiteral = parser.getAttributeValue(NAMESPACE, attrValue);
int usage = AudioManagerHelper.xsdStringToUsage(usageLiteral);
// TODO (b/248106031): Remove once AUDIO_USAGE_NOTIFICATION_EVENT is fixed in core
if (Objects.equals(usageLiteral, "AUDIO_USAGE_NOTIFICATION_EVENT")) {
usage = USAGE_NOTIFICATION_EVENT;
}
if (AudioAttributes.isSystemUsage(usage)) {
builder.setSystemUsage(usage);
} else {
builder.setUsage(usage);
}
}
private void parseContentType(XmlPullParser parser, AudioAttributes.Builder builder)
throws XmlPullParserException, IOException {
String contentTypeLiteral = parser.getAttributeValue(NAMESPACE, ATTR_CONTENT_TYPE);
int contentType = AudioManagerHelper.xsdStringToContentType(contentTypeLiteral);
builder.setContentType(contentType);
}
private void parseTags(XmlPullParser parser, AudioAttributes.Builder builder)
throws XmlPullParserException, IOException {
String tagsLiteral = parser.getAttributeValue(NAMESPACE, ATTR_TAGS);
if (tagsLiteral != null) {
AudioManagerHelper.addTagToAudioAttributes(builder, tagsLiteral);
}
}
private SparseArray<CarAudioZone> parseAudioZones(XmlPullParser parser)
throws XmlPullParserException, IOException {
SparseArray<CarAudioZone> carAudioZones = new SparseArray<>();
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_AUDIO_ZONE.equals(parser.getName())) {
CarAudioZone zone = parseAudioZone(parser);
verifyOnlyOnePrimaryZone(zone, carAudioZones);
carAudioZones.put(zone.getId(), zone);
} else {
skip(parser);
}
}
verifyPrimaryZonePresent(carAudioZones);
addRemainingMicrophonesToPrimaryZone(carAudioZones);
return carAudioZones;
}
private void addRemainingMicrophonesToPrimaryZone(SparseArray<CarAudioZone> carAudioZones) {
CarAudioZone primaryAudioZone = carAudioZones.get(PRIMARY_AUDIO_ZONE);
for (AudioDeviceInfo info : mAddressToInputAudioDeviceInfoForAllInputDevices.values()) {
if (!mAssignedInputAudioDevices.contains(info.getAddress())
&& isMicrophoneInputDevice(info)) {
primaryAudioZone.addInputAudioDevice(new AudioDeviceAttributes(info));
}
}
}
private void verifyOnlyOnePrimaryZone(CarAudioZone newZone, SparseArray<CarAudioZone> zones) {
if (newZone.getId() == PRIMARY_AUDIO_ZONE && zones.contains(PRIMARY_AUDIO_ZONE)) {
throw new RuntimeException("More than one zone parsed with primary audio zone ID: "
+ PRIMARY_AUDIO_ZONE);
}
}
private void verifyPrimaryZonePresent(SparseArray<CarAudioZone> zones) {
if (!zones.contains(PRIMARY_AUDIO_ZONE)) {
throw new RuntimeException("Primary audio zone is required");
}
}
private CarAudioZone parseAudioZone(XmlPullParser parser)
throws XmlPullParserException, IOException {
final boolean isPrimary = Boolean.parseBoolean(
parser.getAttributeValue(NAMESPACE, ATTR_IS_PRIMARY));
final String zoneName = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_NAME);
final int audioZoneId = getZoneId(isPrimary, parser);
parseOccupantZoneId(audioZoneId, parser);
final CarAudioZone zone = new CarAudioZone(mCarAudioContext, zoneName, audioZoneId);
if (isVersionLessThanThree()) {
final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder(
zoneName, audioZoneId, /* zoneConfigId= */ 0, /* isDefault= */ true);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// Expect at least one <volumeGroups> in one audio zone
if (TAG_VOLUME_GROUPS.equals(parser.getName())) {
parseVolumeGroups(parser, zoneConfigBuilder, audioZoneId);
} else if (TAG_INPUT_DEVICES.equals(parser.getName())) {
parseInputAudioDevices(parser, zone);
} else {
skip(parser);
}
}
zone.addZoneConfig(zoneConfigBuilder.build());
return zone;
}
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// Expect at least one <zoneConfigs> in one audio zone
if (TAG_AUDIO_ZONE_CONFIGS.equals(parser.getName())) {
parseZoneConfigs(parser, zone);
} else if (TAG_INPUT_DEVICES.equals(parser.getName())) {
parseInputAudioDevices(parser, zone);
} else {
skip(parser);
}
}
return zone;
}
private int getZoneId(boolean isPrimary, XmlPullParser parser) {
String audioZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_ZONE_ID);
if (isVersionOne()) {
Preconditions.checkArgument(audioZoneIdString == null,
"Invalid audio attribute %s"
+ ", Please update car audio configurations file "
+ "to version to 2 to use it.", ATTR_ZONE_ID);
return isPrimary ? PRIMARY_AUDIO_ZONE
: getNextSecondaryZoneId();
}
// Primary zone does not need to define it
if (isPrimary && audioZoneIdString == null) {
return PRIMARY_AUDIO_ZONE;
}
Objects.requireNonNull(audioZoneIdString,
"Requires audioZoneId for all audio zones.");
int zoneId = parsePositiveIntAttribute(ATTR_ZONE_ID, audioZoneIdString);
//Verify that primary zone id is PRIMARY_AUDIO_ZONE
if (isPrimary) {
Preconditions.checkArgument(zoneId == PRIMARY_AUDIO_ZONE,
"Primary zone %s must be %d or it can be left empty.",
ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
} else {
Preconditions.checkArgument(zoneId != PRIMARY_AUDIO_ZONE,
"%s can only be %d for primary zone.",
ATTR_ZONE_ID, PRIMARY_AUDIO_ZONE);
}
validateAudioZoneIdIsUnique(zoneId);
return zoneId;
}
private void parseOccupantZoneId(int audioZoneId, XmlPullParser parser) {
String occupantZoneIdString = parser.getAttributeValue(NAMESPACE, ATTR_OCCUPANT_ZONE_ID);
if (isVersionOne()) {
Preconditions.checkArgument(occupantZoneIdString == null,
"Invalid audio attribute %s"
+ ", Please update car audio configurations file "
+ "to version to 2 to use it.", ATTR_OCCUPANT_ZONE_ID);
return;
}
//Occupant id not required for all zones
if (occupantZoneIdString == null) {
return;
}
int occupantZoneId = parsePositiveIntAttribute(ATTR_OCCUPANT_ZONE_ID, occupantZoneIdString);
validateOccupantZoneIdIsUnique(occupantZoneId);
mZoneIdToOccupantZoneIdMapping.put(audioZoneId, occupantZoneId);
}
private int parsePositiveIntAttribute(String attribute, String integerString) {
try {
return Integer.parseUnsignedInt(integerString);
} catch (NumberFormatException | IndexOutOfBoundsException e) {
throw new IllegalArgumentException(attribute + " must be a positive integer, but was \""
+ integerString + "\" instead.", e);
}
}
private void parseInputAudioDevices(XmlPullParser parser, CarAudioZone zone)
throws IOException, XmlPullParserException {
if (isVersionOne()) {
throw new IllegalStateException(
TAG_INPUT_DEVICES + " are not supported in car_audio_configuration.xml version "
+ SUPPORTED_VERSION_1);
}
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_INPUT_DEVICE.equals(parser.getName())) {
String audioDeviceAddress =
parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
validateInputAudioDeviceAddress(audioDeviceAddress);
AudioDeviceInfo info =
mAddressToInputAudioDeviceInfoForAllInputDevices.get(audioDeviceAddress);
Preconditions.checkArgument(info != null,
"%s %s of %s does not exist, add input device to"
+ " audio_policy_configuration.xml.",
ATTR_DEVICE_ADDRESS, audioDeviceAddress, TAG_INPUT_DEVICE);
zone.addInputAudioDevice(new AudioDeviceAttributes(info));
}
skip(parser);
}
}
private void validateInputAudioDeviceAddress(String audioDeviceAddress) {
Objects.requireNonNull(audioDeviceAddress, () ->
TAG_INPUT_DEVICE + " " + ATTR_DEVICE_ADDRESS + " attribute must be present.");
Preconditions.checkArgument(!audioDeviceAddress.isEmpty(),
"%s %s attribute can not be empty.",
TAG_INPUT_DEVICE, ATTR_DEVICE_ADDRESS);
if (mAssignedInputAudioDevices.contains(audioDeviceAddress)) {
throw new IllegalArgumentException(TAG_INPUT_DEVICE + " " + audioDeviceAddress
+ " repeats, " + TAG_INPUT_DEVICES + " can not repeat.");
}
mAssignedInputAudioDevices.add(audioDeviceAddress);
}
private void validateOccupantZoneIdIsUnique(int occupantZoneId) {
if (mZoneIdToOccupantZoneIdMapping.indexOfValue(occupantZoneId) > -1) {
throw new IllegalArgumentException(ATTR_OCCUPANT_ZONE_ID + " " + occupantZoneId
+ " is already associated with a zone");
}
}
private void validateAudioZoneIdIsUnique(int audioZoneId) {
if (mAudioZoneIds.contains(audioZoneId)) {
throw new IllegalArgumentException(ATTR_ZONE_ID + " " + audioZoneId
+ " is already associated with a zone");
}
mAudioZoneIds.add(audioZoneId);
}
private void parseZoneConfigs(XmlPullParser parser, CarAudioZone zone)
throws XmlPullParserException, IOException {
int zoneConfigId = 0;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_AUDIO_ZONE_CONFIG.equals(parser.getName())) {
if (zone.getId() == PRIMARY_AUDIO_ZONE && zoneConfigId > 0) {
throw new IllegalArgumentException(
"Primary zone cannot have multiple zone configurations");
}
parseZoneConfig(parser, zone, zoneConfigId);
zoneConfigId++;
} else {
skip(parser);
}
}
}
private void parseZoneConfig(XmlPullParser parser, CarAudioZone zone, int zoneConfigId)
throws XmlPullParserException, IOException {
final boolean isDefault = Boolean.parseBoolean(
parser.getAttributeValue(NAMESPACE, ATTR_IS_CONFIG_DEFAULT));
final String zoneConfigName = parser.getAttributeValue(NAMESPACE, ATTR_CONFIG_NAME);
validateAudioZoneConfigName(zoneConfigName);
final CarAudioZoneConfig.Builder zoneConfigBuilder = new CarAudioZoneConfig.Builder(
zoneConfigName, zone.getId(), zoneConfigId, isDefault);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
// Expect at least one <volumeGroups> in one audio zone config
if (TAG_VOLUME_GROUPS.equals(parser.getName())) {
parseVolumeGroups(parser, zoneConfigBuilder, zone.getId());
} else {
skip(parser);
}
}
zone.addZoneConfig(zoneConfigBuilder.build());
}
private void validateAudioZoneConfigName(String configName) {
Objects.requireNonNull(configName, TAG_AUDIO_ZONE_CONFIG + " " + ATTR_CONFIG_NAME
+ " attribute must be present.");
Preconditions.checkArgument(!configName.isEmpty(),
"%s %s attribute can not be empty.",
TAG_AUDIO_ZONE_CONFIG, ATTR_CONFIG_NAME);
if (mAudioZoneConfigNames.contains(configName)) {
throw new IllegalArgumentException(ATTR_CONFIG_NAME + " " + configName
+ " repeats, " + ATTR_CONFIG_NAME + " can not repeat.");
}
mAudioZoneConfigNames.add(configName);
}
private void parseVolumeGroups(XmlPullParser parser,
CarAudioZoneConfig.Builder zoneConfigBuilder, int audioZoneId)
throws XmlPullParserException, IOException {
int groupId = 0;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_VOLUME_GROUP.equals(parser.getName())) {
String groupName = parser.getAttributeValue(NAMESPACE, ATTR_NAME);
Preconditions.checkArgument(!mUseCoreAudioVolume || groupName != null,
"%s %s attribute can not be empty when relying on core volume groups",
TAG_VOLUME_GROUP, ATTR_NAME);
if (groupName == null) {
groupName = String.valueOf(groupId);
}
zoneConfigBuilder.addVolumeGroup(parseVolumeGroup(parser, audioZoneId, groupId,
groupName));
groupId++;
} else {
skip(parser);
}
}
}
private CarVolumeGroup parseVolumeGroup(XmlPullParser parser, int zoneId, int groupId,
String groupName) throws XmlPullParserException, IOException {
CarVolumeGroupFactory groupFactory = new CarVolumeGroupFactory(mAudioManager,
mCarAudioSettings, mCarAudioContext, zoneId, groupId, groupName,
mUseCarVolumeGroupMute);
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_AUDIO_DEVICE.equals(parser.getName())) {
String address = parser.getAttributeValue(NAMESPACE, ATTR_DEVICE_ADDRESS);
validateOutputDeviceExist(address);
parseVolumeGroupContexts(parser, groupFactory, address);
} else {
skip(parser);
}
}
return groupFactory.getCarVolumeGroup(mUseCoreAudioVolume);
}
private void validateOutputDeviceExist(String address) {
if (!mAddressToCarAudioDeviceInfo.containsKey(address)) {
throw new IllegalStateException(String.format(
"Output device address %s does not belong to any configured output device.",
address));
}
}
private void parseVolumeGroupContexts(
XmlPullParser parser, CarVolumeGroupFactory groupFactory, String address)
throws XmlPullParserException, IOException {
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) continue;
if (TAG_CONTEXT.equals(parser.getName())) {
@AudioContext int carAudioContextId = parseCarAudioContextId(
parser.getAttributeValue(NAMESPACE, ATTR_CONTEXT_NAME));
validateCarAudioContextSupport(carAudioContextId);
CarAudioDeviceInfo info = mAddressToCarAudioDeviceInfo.get(address);
groupFactory.setDeviceInfoForContext(carAudioContextId, info);
// If V1, default new contexts to same device as DEFAULT_AUDIO_USAGE
if (isVersionOne() && carAudioContextId == mCarAudioContext
.getContextForAudioAttribute(CAR_DEFAULT_AUDIO_ATTRIBUTE)) {
groupFactory.setNonLegacyContexts(info);
}
}
// Always skip to upper level since we're at the lowest.
skip(parser);
}
}
private boolean isVersionLessThanThree() {
return mCurrentVersion < SUPPORTED_VERSION_3;
}
private boolean isVersionOne() {
return mCurrentVersion == SUPPORTED_VERSION_1;
}
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
private @AudioContext int parseCarAudioContextId(String context) {
return mContextNameToId.getOrDefault(context.toLowerCase(ROOT),
CarAudioContext.getInvalidContext());
}
private void validateCarAudioContextSupport(@AudioContext int audioContext) {
if (isVersionOne() && CarAudioContext.getCarSystemContextIds().contains(audioContext)) {
throw new IllegalArgumentException(String.format(
"Non-legacy audio contexts such as %s are not supported in "
+ "car_audio_configuration.xml version %d",
mCarAudioContext.toString(audioContext), SUPPORTED_VERSION_1));
}
}
private int getNextSecondaryZoneId() {
int zoneId = mNextSecondaryZoneId;
mNextSecondaryZoneId += 1;
return zoneId;
}
public CarAudioContext getCarAudioContext() {
return mCarAudioContext;
}
public List<CarAudioDeviceInfo> getMirrorDeviceInfos() {
return mMirroringDevices;
}
}