1 /*
2  * Copyright (C) 2024 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.internal.ExcludeFromCodeCoverageGeneratedReport.DUMP_INFO;
19 
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.car.oem.CarAudioFadeConfiguration;
23 import android.media.AudioAttributes;
24 import android.media.FadeManagerConfiguration;
25 import android.util.ArrayMap;
26 import android.util.Xml;
27 import android.util.proto.ProtoOutputStream;
28 
29 import com.android.car.CarLog;
30 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
31 import com.android.car.internal.util.IndentingPrintWriter;
32 import com.android.car.internal.util.IntArray;
33 import com.android.internal.util.Preconditions;
34 
35 import org.xmlpull.v1.XmlPullParser;
36 import org.xmlpull.v1.XmlPullParserException;
37 
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.util.MissingResourceException;
41 import java.util.Objects;
42 import java.util.Set;
43 
44 /**
45  * A class to load {@link android.media.FadeManagerConfiguration} from the configuration XML file.
46  */
47 final class CarAudioFadeConfigurationHelper {
48     private static final String TAG = CarLog.tagFor(CarAudioFadeConfigurationHelper.class);
49 
50     private static final String NAMESPACE = null;
51     private static final String TAG_ROOT = "carAudioFadeConfiguration";
52     private static final String TAG_CONFIGS = "configs";
53     private static final String TAG_CONFIG = "config";
54     private static final String CONFIG_NAME = "name";
55     private static final String CONFIG_DEFAULT_FADE_OUT_DURATION = "defaultFadeOutDurationInMillis";
56     private static final String CONFIG_DEFAULT_FADE_IN_DURATION = "defaultFadeInDurationInMillis";
57     private static final String CONFIG_FADE_STATE = "fadeState";
58     private static final String CONFIG_FADEABLE_USAGES = "fadeableUsages";
59     private static final String CONFIG_UNFADEABLE_CONTENT_TYPES = "unfadeableContentTypes";
60     private static final String CONFIG_UNFADEABLE_AUDIO_ATTRIBUTES = "unfadeableAudioAttributes";
61     private static final String CONFIG_FADE_OUT_CONFIGURATIONS = "fadeOutConfigurations";
62     private static final String CONFIG_FADE_IN_CONFIGURATIONS = "fadeInConfigurations";
63     private static final String CONFIG_FADE_CONFIGURATION = "fadeConfiguration";
64     private static final String FADE_DURATION = "fadeDurationMillis";
65     private static final String FADE_STATE_VALUE = "value";
66     private static final String VERSION = "version";
67     private static final int INVALID = -1;
68     private static final int SUPPORTED_VERSION_1 = 1;
69 
70     private static final IntArray SUPPORTED_VERSIONS = IntArray.wrap(new int[] {
71             SUPPORTED_VERSION_1,
72     });
73 
74     private final ArrayMap<String, CarAudioFadeConfiguration> mNameToCarAudioFadeConfigurationMap =
75             new ArrayMap<>();
76 
CarAudioFadeConfigurationHelper(@onNull InputStream stream)77     CarAudioFadeConfigurationHelper(@NonNull InputStream stream)
78             throws XmlPullParserException, IOException {
79         Objects.requireNonNull(stream, "Car audio fade config input stream can not be null");
80         parseFadeManagerConfigFile(stream);
81     }
82 
83     @Nullable
getCarAudioFadeConfiguration(String configName)84     public CarAudioFadeConfiguration getCarAudioFadeConfiguration(String configName) {
85         if (!isConfigAvailable(configName)) {
86             return null;
87         }
88 
89         return mNameToCarAudioFadeConfigurationMap.get(configName);
90     }
91 
isConfigAvailable(String configName)92     public boolean isConfigAvailable(String configName) {
93         return mNameToCarAudioFadeConfigurationMap.containsKey(configName);
94     }
95 
96     @NonNull
getAllConfigNames()97     public Set<String> getAllConfigNames() {
98         return mNameToCarAudioFadeConfigurationMap.keySet();
99     }
100 
parseFadeManagerConfigFile(InputStream stream)101     private void parseFadeManagerConfigFile(InputStream stream)
102             throws XmlPullParserException, IOException {
103         XmlPullParser parser = Xml.newPullParser();
104         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, NAMESPACE != null);
105         parser.setInput(stream, null);
106 
107         // Ensure <carAudioFadeConfiguration> is the root
108         parser.nextTag();
109         parser.require(XmlPullParser.START_TAG, NAMESPACE, TAG_ROOT);
110 
111         // Version check
112         int versionNumber = Integer.parseInt(parser.getAttributeValue(NAMESPACE, VERSION));
113         if (SUPPORTED_VERSIONS.indexOf(versionNumber) == INVALID) {
114             throw new IllegalArgumentException("Latest Supported version:"
115                     + SUPPORTED_VERSION_1 + " , got version:" + versionNumber);
116         }
117 
118         // Get all fade configs configured under <configs> tag
119         while (parser.next() != XmlPullParser.END_TAG) {
120             if (parser.getEventType() != XmlPullParser.START_TAG) {
121                 continue;
122             }
123             if (Objects.equals(parser.getName(), TAG_CONFIGS)) {
124                 parseFadeConfigs(parser);
125             } else {
126                 CarAudioParserUtils.skip(parser);
127             }
128         }
129 
130         if (mNameToCarAudioFadeConfigurationMap.isEmpty()) {
131             throw new MissingResourceException(TAG_CONFIGS + " is missing from configuration ",
132                     TAG_ROOT, TAG_CONFIGS);
133         }
134     }
135 
parseFadeConfigs(XmlPullParser parser)136     private void parseFadeConfigs(XmlPullParser parser) throws XmlPullParserException, IOException {
137         while (parser.next() != XmlPullParser.END_TAG) {
138             if (parser.getEventType() != XmlPullParser.START_TAG) {
139                 continue;
140             }
141             if (Objects.equals(parser.getName(), TAG_CONFIG)) {
142                 CarAudioFadeConfiguration afc = parseFadeConfig(parser);
143                 mNameToCarAudioFadeConfigurationMap.put(afc.getName(), afc);
144             } else {
145                 CarAudioParserUtils.skip(parser);
146             }
147         }
148     }
149 
parseFadeConfig(XmlPullParser parser)150     private CarAudioFadeConfiguration parseFadeConfig(XmlPullParser parser)
151             throws XmlPullParserException, IOException {
152 
153         String configName = parser.getAttributeValue(NAMESPACE, CONFIG_NAME);
154         Objects.requireNonNull(configName, "Config name can not be null");
155         Preconditions.checkArgument(!configName.isEmpty(), "Config name can not be empty");
156 
157         String defaultFadeOutDurationLiteral = parser.getAttributeValue(NAMESPACE,
158                 CONFIG_DEFAULT_FADE_OUT_DURATION);
159         String defaultFadeInDurationLiteral = parser.getAttributeValue(NAMESPACE,
160                 CONFIG_DEFAULT_FADE_IN_DURATION);
161 
162         FadeManagerConfiguration.Builder builder;
163         if (defaultFadeOutDurationLiteral != null && defaultFadeInDurationLiteral != null) {
164             builder = new FadeManagerConfiguration.Builder(
165                     CarAudioParserUtils.parsePositiveLongAttribute(CONFIG_DEFAULT_FADE_OUT_DURATION,
166                             defaultFadeOutDurationLiteral),
167                     CarAudioParserUtils.parsePositiveLongAttribute(CONFIG_DEFAULT_FADE_IN_DURATION,
168                             defaultFadeInDurationLiteral));
169         } else {
170             builder = new FadeManagerConfiguration.Builder();
171         }
172 
173         while (parser.next() != XmlPullParser.END_TAG) {
174             if (parser.getEventType() != XmlPullParser.START_TAG) {
175                 continue;
176             }
177             if (Objects.equals(parser.getName(), CONFIG_FADE_STATE)) {
178                 parseFadeState(parser, builder);
179             } else if (Objects.equals(parser.getName(), CONFIG_FADEABLE_USAGES)) {
180                 parseFadeableUsages(parser, builder);
181             } else if (Objects.equals(parser.getName(), CONFIG_UNFADEABLE_CONTENT_TYPES)) {
182                 parseUnfadeableContentTypes(parser, builder);
183             } else if (Objects.equals(parser.getName(), CONFIG_UNFADEABLE_AUDIO_ATTRIBUTES)) {
184                 parseUnfadeableAudioAttributes(parser, builder, configName);
185             } else if (Objects.equals(parser.getName(), CONFIG_FADE_OUT_CONFIGURATIONS)) {
186                 parseFadeOutConfigurations(parser, builder);
187             } else if (Objects.equals(parser.getName(), CONFIG_FADE_IN_CONFIGURATIONS)) {
188                 parseFadeInConfigurations(parser, builder);
189             } else {
190                 CarAudioParserUtils.skip(parser);
191             }
192         }
193         return new CarAudioFadeConfiguration.Builder(builder.build()).setName(configName).build();
194     }
195 
parseFadeState(XmlPullParser parser, FadeManagerConfiguration.Builder builder)196     private void parseFadeState(XmlPullParser parser, FadeManagerConfiguration.Builder builder)
197             throws XmlPullParserException, IOException {
198         String fadeStateLiteral = parser.getAttributeValue(NAMESPACE, FADE_STATE_VALUE);
199         int fadeState = CarAudioParserUtils.parsePositiveIntAttribute(FADE_STATE_VALUE,
200                 fadeStateLiteral);
201         builder.setFadeState(fadeState);
202         CarAudioParserUtils.skip(parser);
203     }
204 
parseFadeableUsages(XmlPullParser parser, FadeManagerConfiguration.Builder builder)205     private void parseFadeableUsages(XmlPullParser parser, FadeManagerConfiguration.Builder builder)
206             throws XmlPullParserException, IOException {
207         while (parser.next() != XmlPullParser.END_TAG) {
208             if (parser.getEventType() != XmlPullParser.START_TAG) {
209                 continue;
210             }
211             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_USAGE)) {
212                 int usage = CarAudioParserUtils.parseUsageValue(parser,
213                         CarAudioParserUtils.ATTR_USAGE_VALUE);
214                 builder.addFadeableUsage(usage);
215             }
216             CarAudioParserUtils.skip(parser);
217         }
218     }
219 
parseUnfadeableContentTypes(XmlPullParser parser, FadeManagerConfiguration.Builder builder)220     private void parseUnfadeableContentTypes(XmlPullParser parser,
221             FadeManagerConfiguration.Builder builder) throws XmlPullParserException, IOException {
222         while (parser.next() != XmlPullParser.END_TAG) {
223             if (parser.getEventType() != XmlPullParser.START_TAG) {
224                 continue;
225             }
226             if (Objects.equals(parser.getName(), CONFIG_UNFADEABLE_CONTENT_TYPES)) {
227                 builder.addUnfadeableContentType(CarAudioParserUtils.parseContentTypeValue(parser));
228             }
229             CarAudioParserUtils.skip(parser);
230         }
231     }
232 
parseUnfadeableAudioAttributes(XmlPullParser parser, FadeManagerConfiguration.Builder builder, String configName)233     private void parseUnfadeableAudioAttributes(XmlPullParser parser,
234             FadeManagerConfiguration.Builder builder, String configName)
235             throws XmlPullParserException, IOException {
236         while (parser.next() != XmlPullParser.END_TAG) {
237             if (parser.getEventType() != XmlPullParser.START_TAG) {
238                 continue;
239             }
240             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES)) {
241                 builder.setUnfadeableAudioAttributes(
242                         CarAudioParserUtils.parseAudioAttributes(parser, configName));
243             } else {
244                 CarAudioParserUtils.skip(parser);
245             }
246         }
247     }
248 
parseFadeOutConfigurations(XmlPullParser parser, FadeManagerConfiguration.Builder builder)249     private void parseFadeOutConfigurations(XmlPullParser parser,
250             FadeManagerConfiguration.Builder builder) throws XmlPullParserException, IOException {
251         while (parser.next() != XmlPullParser.END_TAG) {
252             if (parser.getEventType() != XmlPullParser.START_TAG) {
253                 continue;
254             }
255             if (Objects.equals(parser.getName(), CONFIG_FADE_CONFIGURATION)) {
256                 parseFadeConfiguration(parser, builder, /* isFadeIn= */ false);
257             } else {
258                 CarAudioParserUtils.skip(parser);
259             }
260         }
261     }
262 
parseFadeInConfigurations(XmlPullParser parser, FadeManagerConfiguration.Builder builder)263     private void parseFadeInConfigurations(XmlPullParser parser,
264             FadeManagerConfiguration.Builder builder) throws XmlPullParserException, IOException {
265         while (parser.next() != XmlPullParser.END_TAG) {
266             if (parser.getEventType() != XmlPullParser.START_TAG) {
267                 continue;
268             }
269             if (Objects.equals(parser.getName(), CONFIG_FADE_CONFIGURATION)) {
270                 parseFadeConfiguration(parser, builder, /* isFadeIn= */ true);
271             } else {
272                 CarAudioParserUtils.skip(parser);
273             }
274         }
275     }
276 
parseFadeConfiguration(XmlPullParser parser, FadeManagerConfiguration.Builder builder, boolean isFadeIn)277     private void parseFadeConfiguration(XmlPullParser parser,
278             FadeManagerConfiguration.Builder builder, boolean isFadeIn)
279             throws XmlPullParserException, IOException {
280         long fadeDuration = CarAudioParserUtils.parsePositiveLongAttribute(
281                 FADE_DURATION,
282                 parser.getAttributeValue(NAMESPACE, FADE_DURATION));
283         while (parser.next() != XmlPullParser.END_TAG) {
284             if (parser.getEventType() != XmlPullParser.START_TAG) {
285                 continue;
286             }
287             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_AUDIO_ATTRIBUTES)) {
288                 parseAudioAttributesForFade(parser, builder, fadeDuration, isFadeIn);
289             } else {
290                 CarAudioParserUtils.skip(parser);
291             }
292         }
293     }
294 
parseAudioAttributesForFade(XmlPullParser parser, FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)295     private void parseAudioAttributesForFade(XmlPullParser parser,
296             FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)
297             throws XmlPullParserException, IOException {
298         while (parser.next() != XmlPullParser.END_TAG) {
299             if (parser.getEventType() != XmlPullParser.START_TAG) {
300                 continue;
301             }
302             if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_USAGE)) {
303                 parseFadeDurationForUsage(parser, builder, fadeDuration, isFadeIn);
304             } else if (Objects.equals(parser.getName(), CarAudioParserUtils.TAG_AUDIO_ATTRIBUTE)) {
305                 parseFadeDurationForAudioAttributes(parser, builder, fadeDuration, isFadeIn);
306             }
307             CarAudioParserUtils.skip(parser);
308         }
309     }
310 
parseFadeDurationForUsage(XmlPullParser parser, FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)311     private void parseFadeDurationForUsage(XmlPullParser parser,
312             FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)
313             throws XmlPullParserException, IOException {
314         int usage = CarAudioParserUtils.parseUsageValue(parser,
315                 CarAudioParserUtils.ATTR_USAGE_VALUE);
316         if (isFadeIn) {
317             builder.setFadeInDurationForUsage(usage, fadeDuration);
318         } else {
319             builder.setFadeOutDurationForUsage(usage, fadeDuration);
320         }
321     }
322 
parseFadeDurationForAudioAttributes(XmlPullParser parser, FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)323     private void parseFadeDurationForAudioAttributes(XmlPullParser parser,
324             FadeManagerConfiguration.Builder builder, long fadeDuration, boolean isFadeIn)
325             throws XmlPullParserException, IOException {
326         AudioAttributes attr = CarAudioParserUtils.parseAudioAttribute(parser,
327                 CONFIG_FADE_CONFIGURATION);
328         if (isFadeIn) {
329             builder.setFadeInDurationForAudioAttributes(attr, fadeDuration);
330         } else {
331             builder.setFadeOutDurationForAudioAttributes(attr, fadeDuration);
332         }
333     }
334 
335     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dump(IndentingPrintWriter writer)336     void dump(IndentingPrintWriter writer) {
337         writer.increaseIndent();
338         writer.printf("Available fade manager configurations: %d\n",
339                 mNameToCarAudioFadeConfigurationMap.size());
340         for (int index = 0; index < mNameToCarAudioFadeConfigurationMap.size(); index++) {
341             writer.printf((index + 1) + ". " + mNameToCarAudioFadeConfigurationMap.valueAt(index)
342                     + "\n");
343         }
344         writer.decreaseIndent();
345     }
346 
347     @ExcludeFromCodeCoverageGeneratedReport(reason = DUMP_INFO)
dumpProto(ProtoOutputStream proto)348     void dumpProto(ProtoOutputStream proto) {
349         if (mNameToCarAudioFadeConfigurationMap == null) {
350             return;
351         }
352 
353         for (int index = 0; index < mNameToCarAudioFadeConfigurationMap.size(); index++) {
354             CarAudioProtoUtils.dumpCarAudioFadeConfigurationProto(
355                     mNameToCarAudioFadeConfigurationMap.valueAt(index),
356                     CarAudioDumpProto.AVAILABLE_CAR_FADE_CONFIGURATIONS, proto);
357         }
358     }
359 }
360