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