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 com.android.car.audio.CarAudioZonesHelper.LEGACY_CONTEXTS; 19 20 import android.annotation.NonNull; 21 import android.annotation.XmlRes; 22 import android.car.media.CarAudioManager; 23 import android.content.Context; 24 import android.content.res.TypedArray; 25 import android.content.res.XmlResourceParser; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.util.SparseArray; 29 import android.util.SparseIntArray; 30 import android.util.Xml; 31 32 import com.android.car.CarLog; 33 import com.android.car.R; 34 import com.android.car.audio.hal.AudioControlWrapperV1; 35 36 import org.xmlpull.v1.XmlPullParserException; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.List; 41 import java.util.Objects; 42 43 /** 44 * A helper class loads volume groups from car_volume_groups.xml configuration into one zone. 45 * 46 * @deprecated This is replaced by {@link CarAudioZonesHelper}. 47 */ 48 @Deprecated 49 class CarAudioZonesHelperLegacy { 50 private static final String TAG_VOLUME_GROUPS = "volumeGroups"; 51 private static final String TAG_GROUP = "group"; 52 private static final String TAG_CONTEXT = "context"; 53 54 private static final int NO_BUS_FOR_CONTEXT = -1; 55 56 private final Context mContext; 57 private final @XmlRes int mXmlConfiguration; 58 private final SparseIntArray mLegacyAudioContextToBus; 59 private final SparseArray<CarAudioDeviceInfo> mBusToCarAudioDeviceInfo; 60 private final CarAudioSettings mCarAudioSettings; 61 CarAudioZonesHelperLegacy(@onNull Context context, @XmlRes int xmlConfiguration, @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, @NonNull AudioControlWrapperV1 audioControlWrapper, @NonNull CarAudioSettings carAudioSettings)62 CarAudioZonesHelperLegacy(@NonNull Context context, @XmlRes int xmlConfiguration, 63 @NonNull List<CarAudioDeviceInfo> carAudioDeviceInfos, 64 @NonNull AudioControlWrapperV1 audioControlWrapper, 65 @NonNull CarAudioSettings carAudioSettings) { 66 Objects.requireNonNull(context); 67 Objects.requireNonNull(carAudioDeviceInfos); 68 Objects.requireNonNull(audioControlWrapper); 69 mCarAudioSettings = Objects.requireNonNull(carAudioSettings); 70 mContext = context; 71 mXmlConfiguration = xmlConfiguration; 72 mBusToCarAudioDeviceInfo = 73 generateBusToCarAudioDeviceInfo(carAudioDeviceInfos); 74 75 mLegacyAudioContextToBus = 76 loadBusesForLegacyContexts(audioControlWrapper); 77 } 78 79 /* Loads mapping from {@link CarAudioContext} values to bus numbers 80 * <p>Queries {@code IAudioControl#getBusForContext} for all 81 * {@code CArAudioZoneHelper.LEGACY_CONTEXTS}. Legacy 82 * contexts are those defined as part of 83 * {@code android.hardware.automotive.audiocontrol.V1_0.ContextNumber} 84 * 85 * @param audioControl wrapper for IAudioControl HAL interface. 86 * @return SparseIntArray mapping from {@link CarAudioContext} to bus number. 87 */ loadBusesForLegacyContexts( @onNull AudioControlWrapperV1 audioControlWrapper)88 private static SparseIntArray loadBusesForLegacyContexts( 89 @NonNull AudioControlWrapperV1 audioControlWrapper) { 90 SparseIntArray contextToBus = new SparseIntArray(); 91 92 for (int legacyContext : LEGACY_CONTEXTS) { 93 int bus = audioControlWrapper.getBusForContext(legacyContext); 94 validateBusNumber(legacyContext, bus); 95 contextToBus.put(legacyContext, bus); 96 } 97 return contextToBus; 98 } 99 validateBusNumber(int legacyContext, int bus)100 private static void validateBusNumber(int legacyContext, int bus) { 101 if (bus == NO_BUS_FOR_CONTEXT) { 102 throw new IllegalArgumentException( 103 String.format("Invalid bus %d was associated with context %s", bus, 104 CarAudioContext.toString(legacyContext))); 105 } 106 } 107 generateBusToCarAudioDeviceInfo( List<CarAudioDeviceInfo> carAudioDeviceInfos)108 private static SparseArray<CarAudioDeviceInfo> generateBusToCarAudioDeviceInfo( 109 List<CarAudioDeviceInfo> carAudioDeviceInfos) { 110 SparseArray<CarAudioDeviceInfo> busToCarAudioDeviceInfo = new SparseArray<>(); 111 112 for (CarAudioDeviceInfo carAudioDeviceInfo : carAudioDeviceInfos) { 113 int busNumber = parseDeviceAddress(carAudioDeviceInfo.getAddress()); 114 if (busNumber >= 0) { 115 if (busToCarAudioDeviceInfo.get(busNumber) != null) { 116 throw new IllegalArgumentException("Two addresses map to same bus number: " 117 + carAudioDeviceInfo.getAddress() 118 + " and " 119 + busToCarAudioDeviceInfo.get(busNumber).getAddress()); 120 } 121 busToCarAudioDeviceInfo.put(busNumber, carAudioDeviceInfo); 122 } 123 } 124 125 return busToCarAudioDeviceInfo; 126 } 127 loadAudioZones()128 CarAudioZone[] loadAudioZones() { 129 final CarAudioZone zone = new CarAudioZone(CarAudioManager.PRIMARY_AUDIO_ZONE, 130 "Primary zone"); 131 for (CarVolumeGroup group : loadVolumeGroups()) { 132 zone.addVolumeGroup(group); 133 bindContextsForVolumeGroup(group); 134 } 135 return new CarAudioZone[]{zone}; 136 } 137 bindContextsForVolumeGroup(CarVolumeGroup group)138 private void bindContextsForVolumeGroup(CarVolumeGroup group) { 139 for (int legacyAudioContext : group.getContexts()) { 140 int busNumber = mLegacyAudioContextToBus.get(legacyAudioContext); 141 CarAudioDeviceInfo info = mBusToCarAudioDeviceInfo.get(busNumber); 142 group.bind(legacyAudioContext, info); 143 144 if (legacyAudioContext == CarAudioService.DEFAULT_AUDIO_CONTEXT) { 145 CarAudioZonesHelper.bindNonLegacyContexts(group, info); 146 } 147 } 148 } 149 150 /** 151 * @return all {@link CarVolumeGroup} read from configuration. 152 */ loadVolumeGroups()153 private List<CarVolumeGroup> loadVolumeGroups() { 154 List<CarVolumeGroup> carVolumeGroups = new ArrayList<>(); 155 try (XmlResourceParser parser = mContext.getResources().getXml(mXmlConfiguration)) { 156 AttributeSet attrs = Xml.asAttributeSet(parser); 157 int type; 158 // Traverse to the first start tag 159 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 160 && type != XmlResourceParser.START_TAG) { 161 // ignored 162 } 163 164 if (!TAG_VOLUME_GROUPS.equals(parser.getName())) { 165 throw new IllegalArgumentException( 166 "Meta-data does not start with volumeGroups tag"); 167 } 168 int outerDepth = parser.getDepth(); 169 int id = 0; 170 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 171 && (type != XmlResourceParser.END_TAG || parser.getDepth() > outerDepth)) { 172 if (type == XmlResourceParser.END_TAG) { 173 continue; 174 } 175 if (TAG_GROUP.equals(parser.getName())) { 176 carVolumeGroups.add(parseVolumeGroup(id, attrs, parser)); 177 id++; 178 } 179 } 180 } catch (Exception e) { 181 Log.e(CarLog.TAG_AUDIO, "Error parsing volume groups configuration", e); 182 } 183 return carVolumeGroups; 184 } 185 parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser)186 private CarVolumeGroup parseVolumeGroup(int id, AttributeSet attrs, XmlResourceParser parser) 187 throws XmlPullParserException, IOException { 188 List<Integer> contexts = new ArrayList<>(); 189 int type; 190 int innerDepth = parser.getDepth(); 191 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 192 && (type != XmlResourceParser.END_TAG || parser.getDepth() > innerDepth)) { 193 if (type == XmlResourceParser.END_TAG) { 194 continue; 195 } 196 if (TAG_CONTEXT.equals(parser.getName())) { 197 TypedArray c = mContext.getResources().obtainAttributes( 198 attrs, R.styleable.volumeGroups_context); 199 contexts.add(c.getInt(R.styleable.volumeGroups_context_context, -1)); 200 c.recycle(); 201 } 202 } 203 204 return new CarVolumeGroup(mCarAudioSettings, CarAudioManager.PRIMARY_AUDIO_ZONE, id, 205 contexts.stream().mapToInt(i -> i).filter(i -> i >= 0).toArray()); 206 } 207 208 /** 209 * Parse device address. Expected format is BUS%d_%s, address, usage hint 210 * 211 * @return valid address (from 0 to positive) or -1 for invalid address. 212 */ parseDeviceAddress(String address)213 private static int parseDeviceAddress(String address) { 214 String[] words = address.split("_"); 215 int addressParsed = -1; 216 if (words[0].toLowerCase().startsWith("bus")) { 217 try { 218 addressParsed = Integer.parseInt(words[0].substring(3)); 219 } catch (NumberFormatException e) { 220 //ignore 221 } 222 } 223 if (addressParsed < 0) { 224 return -1; 225 } 226 return addressParsed; 227 } 228 } 229