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 android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
19 
20 import static com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport.PRIVATE_CONSTRUCTOR;
21 
22 import android.car.builtin.media.AudioManagerHelper;
23 import android.media.AudioAttributes;
24 
25 import com.android.car.internal.ExcludeFromCodeCoverageGeneratedReport;
26 
27 import org.xmlpull.v1.XmlPullParser;
28 import org.xmlpull.v1.XmlPullParserException;
29 
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Objects;
34 
35 /**
36  * Utils for common parser functionalities used across multiple config files
37  */
38 /* package */ final class CarAudioParserUtils {
39 
40     private static final String NAMESPACE = null;
41     private static final int INVALID = -1;
42 
43     public static final String TAG_AUDIO_ATTRIBUTES = "audioAttributes";
44     public static final String TAG_AUDIO_ATTRIBUTE = "audioAttribute";
45     public static final String TAG_USAGE = "usage";
46     public static final String ATTR_USAGE_VALUE = "value";
47     public static final String ATTR_NAME = "name";
48     public static final String ATTR_CONTENT_TYPE = "contentType";
49     public static final String ATTR_USAGE = "usage";
50     public static final String ATTR_TAGS = "tags";
51 
52     @ExcludeFromCodeCoverageGeneratedReport(reason = PRIVATE_CONSTRUCTOR)
CarAudioParserUtils()53     private CarAudioParserUtils() {
54         throw new UnsupportedOperationException(
55                 "CarAudioParserUtils class is non-instantiable, contains static members only");
56     }
57 
parseAudioAttributes(XmlPullParser parser, String sectionName)58     /* package */ static List<AudioAttributes> parseAudioAttributes(XmlPullParser parser,
59             String sectionName) throws XmlPullParserException, IOException {
60         List<AudioAttributes> attrs = new ArrayList<>();
61         while (parser.next() != XmlPullParser.END_TAG) {
62             if (parser.getEventType() != XmlPullParser.START_TAG) {
63                 continue;
64             }
65 
66             if (Objects.equals(parser.getName(), TAG_USAGE)) {
67                 AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
68                 parseUsage(parser, attributesBuilder, ATTR_USAGE_VALUE);
69                 AudioAttributes attributes = attributesBuilder.build();
70                 attrs.add(attributes);
71             } else if (Objects.equals(parser.getName(), TAG_AUDIO_ATTRIBUTE)) {
72                 attrs.add(parseAudioAttribute(parser, sectionName));
73             }
74             // Always skip to upper level since we're at the lowest.
75             skip(parser);
76         }
77         if (attrs.isEmpty()) {
78             throw new IllegalArgumentException("No attributes for config: " + sectionName);
79         }
80         return attrs;
81     }
82 
parseAudioAttribute(XmlPullParser parser, String sectionName)83     static AudioAttributes parseAudioAttribute(XmlPullParser parser,
84             String sectionName) throws XmlPullParserException, IOException {
85         AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
86         // Usage, ContentType and tags are optional but at least one value must be
87         // provided to build a valid audio attributes
88         boolean hasValidUsage = parseUsage(parser, attributesBuilder, ATTR_USAGE);
89         boolean hasValidContentType = parseContentType(parser, attributesBuilder);
90         boolean hasValidTags = parseTags(parser, attributesBuilder);
91         if (!(hasValidUsage || hasValidContentType || hasValidTags)) {
92             throw new RuntimeException("Empty attributes for context: " + sectionName);
93         }
94         return attributesBuilder.build();
95     }
96 
parseUsage(XmlPullParser parser, AudioAttributes.Builder builder, String attrValue)97     private static boolean parseUsage(XmlPullParser parser, AudioAttributes.Builder builder,
98             String attrValue) throws XmlPullParserException, IOException {
99         int usage = parseUsageValue(parser, attrValue);
100         if (usage == INVALID) {
101             return false;
102         }
103 
104         if (AudioAttributes.isSystemUsage(usage)) {
105             builder.setSystemUsage(usage);
106         } else {
107             builder.setUsage(usage);
108         }
109         return true;
110     }
111 
parseUsageValue(XmlPullParser parser, String attrValue)112     static int parseUsageValue(XmlPullParser parser, String attrValue)
113             throws XmlPullParserException, IOException  {
114         String usageLiteral = parser.getAttributeValue(NAMESPACE, attrValue);
115         if (usageLiteral == null) {
116             return INVALID;
117         }
118 
119         int usage = AudioManagerHelper.xsdStringToUsage(usageLiteral);
120         // TODO (b/248106031): Remove once AUDIO_USAGE_NOTIFICATION_EVENT is fixed in core
121         if (Objects.equals(usageLiteral, "AUDIO_USAGE_NOTIFICATION_EVENT")) {
122             usage = USAGE_NOTIFICATION_EVENT;
123         }
124         return usage;
125     }
126 
parseContentType(XmlPullParser parser, AudioAttributes.Builder builder)127     private static boolean parseContentType(XmlPullParser parser, AudioAttributes.Builder builder)
128             throws XmlPullParserException, IOException {
129         int contentType = parseContentTypeValue(parser);
130         if (contentType == INVALID) {
131             return false;
132         }
133 
134         builder.setContentType(contentType);
135         return true;
136     }
137 
parseContentTypeValue(XmlPullParser parser)138     static int parseContentTypeValue(XmlPullParser parser)
139             throws XmlPullParserException, IOException {
140         String contentTypeLiteral = parser.getAttributeValue(NAMESPACE, ATTR_CONTENT_TYPE);
141         if (contentTypeLiteral == null) {
142             return INVALID;
143         }
144         return AudioManagerHelper.xsdStringToContentType(contentTypeLiteral);
145     }
146 
parseTags(XmlPullParser parser, AudioAttributes.Builder builder)147     private static boolean parseTags(XmlPullParser parser, AudioAttributes.Builder builder)
148             throws XmlPullParserException, IOException {
149         String tagsLiteral = parser.getAttributeValue(NAMESPACE, ATTR_TAGS);
150         if (tagsLiteral == null) {
151             return false;
152         }
153         AudioManagerHelper.addTagToAudioAttributes(builder, tagsLiteral);
154         return true;
155     }
156 
parsePositiveIntAttribute(String attribute, String integerString)157     /* package */ static int parsePositiveIntAttribute(String attribute, String integerString) {
158         try {
159             return Integer.parseUnsignedInt(integerString);
160         } catch (NumberFormatException | IndexOutOfBoundsException e) {
161             throw new IllegalArgumentException(attribute + " must be a positive integer, but was \""
162                     + integerString + "\" instead.", e);
163         }
164     }
165 
parsePositiveLongAttribute(String attribute, String longString)166     static long parsePositiveLongAttribute(String attribute, String longString) {
167         try {
168             return Long.parseUnsignedLong(longString);
169         } catch (NumberFormatException | IndexOutOfBoundsException e) {
170             throw new IllegalArgumentException(attribute + " must be a positive long, but was \""
171                     + longString + "\" instead.", e);
172         }
173     }
174 
skip(XmlPullParser parser)175     /* package */ static void skip(XmlPullParser parser)
176             throws XmlPullParserException, IOException {
177         if (parser.getEventType() != XmlPullParser.START_TAG) {
178             throw new IllegalStateException();
179         }
180         int depth = 1;
181         while (depth != 0) {
182             switch (parser.next()) {
183                 case XmlPullParser.END_TAG:
184                     depth--;
185                     break;
186                 case XmlPullParser.START_TAG:
187                     depth++;
188                     break;
189                 default:
190                     break;
191             }
192         }
193     }
194 }
195