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 
17 package com.android.car;
18 
19 import static android.car.drivingstate.CarUxRestrictionsManager.UX_RESTRICTION_MODE_BASELINE;
20 
21 import android.annotation.Nullable;
22 import android.annotation.XmlRes;
23 import android.car.drivingstate.CarDrivingStateEvent;
24 import android.car.drivingstate.CarUxRestrictions;
25 import android.car.drivingstate.CarUxRestrictionsConfiguration;
26 import android.car.drivingstate.CarUxRestrictionsConfiguration.Builder;
27 import android.car.drivingstate.CarUxRestrictionsConfiguration.DrivingStateRestrictions;
28 import android.content.Context;
29 import android.content.res.TypedArray;
30 import android.content.res.XmlResourceParser;
31 import android.util.AttributeSet;
32 import android.util.Log;
33 import android.util.Xml;
34 
35 import org.xmlpull.v1.XmlPullParserException;
36 
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.List;
40 
41 /**
42  * @hide
43  */
44 public final class CarUxRestrictionsConfigurationXmlParser {
45     private static final String TAG = "UxRConfigParser";
46     private static final int UX_RESTRICTIONS_UNKNOWN = -1;
47     private static final float INVALID_SPEED = -1f;
48     // XML tags to parse
49     private static final String ROOT_ELEMENT = "UxRestrictions";
50     private static final String RESTRICTION_MAPPING = "RestrictionMapping";
51     private static final String RESTRICTION_PARAMETERS = "RestrictionParameters";
52     private static final String DRIVING_STATE = "DrivingState";
53     private static final String RESTRICTIONS = "Restrictions";
54     private static final String STRING_RESTRICTIONS = "StringRestrictions";
55     private static final String CONTENT_RESTRICTIONS = "ContentRestrictions";
56 
57     private final Context mContext;
58 
59     private int mMaxRestrictedStringLength = UX_RESTRICTIONS_UNKNOWN;
60     private int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN;
61     private int mMaxContentDepth = UX_RESTRICTIONS_UNKNOWN;
62     private final List<CarUxRestrictionsConfiguration.Builder> mConfigBuilders = new ArrayList<>();
63 
CarUxRestrictionsConfigurationXmlParser(Context context)64     private CarUxRestrictionsConfigurationXmlParser(Context context) {
65         mContext = context;
66     }
67 
68     /**
69      * Loads the UX restrictions related information from the XML resource.
70      *
71      * @return parsed CarUxRestrictionsConfiguration; {@code null} if the XML is malformed.
72      */
73     @Nullable
parse( Context context, @XmlRes int xmlResource)74     public static List<CarUxRestrictionsConfiguration> parse(
75             Context context, @XmlRes int xmlResource)
76             throws IOException, XmlPullParserException {
77         return new CarUxRestrictionsConfigurationXmlParser(context).parse(xmlResource);
78     }
79 
80     @Nullable
parse(@mlRes int xmlResource)81     private List<CarUxRestrictionsConfiguration> parse(@XmlRes int xmlResource)
82             throws IOException, XmlPullParserException {
83 
84         XmlResourceParser parser = mContext.getResources().getXml(xmlResource);
85         if (parser == null) {
86             Log.e(TAG, "Invalid Xml resource");
87             return null;
88         }
89 
90         if (!traverseUntilStartTag(parser)) {
91             Log.e(TAG, "XML root element invalid: " + parser.getName());
92             return null;
93         }
94 
95         if (!traverseUntilEndOfDocument(parser)) {
96             Log.e(TAG, "Could not parse XML to end");
97             return null;
98         }
99 
100         List<CarUxRestrictionsConfiguration> configs = new ArrayList<>();
101         for (CarUxRestrictionsConfiguration.Builder builder : mConfigBuilders) {
102             builder.setMaxStringLength(mMaxRestrictedStringLength)
103                     .setMaxCumulativeContentItems(mMaxCumulativeContentItems)
104                     .setMaxContentDepth(mMaxContentDepth);
105             configs.add(builder.build());
106         }
107         return configs;
108     }
109 
traverseUntilStartTag(XmlResourceParser parser)110     private boolean traverseUntilStartTag(XmlResourceParser parser)
111             throws IOException, XmlPullParserException {
112         int type;
113         // Traverse till we get to the first tag
114         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
115                 && type != XmlResourceParser.START_TAG) {
116             // Do nothing.
117         }
118         return ROOT_ELEMENT.equals(parser.getName());
119     }
120 
traverseUntilEndOfDocument(XmlResourceParser parser)121     private boolean traverseUntilEndOfDocument(XmlResourceParser parser)
122             throws XmlPullParserException, IOException {
123         AttributeSet attrs = Xml.asAttributeSet(parser);
124         while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
125             // Every time we hit a start tag, check for the type of the tag
126             // and load the corresponding information.
127             if (parser.next() == XmlResourceParser.START_TAG) {
128                 switch (parser.getName()) {
129                     case RESTRICTION_MAPPING:
130                         // Each RestrictionMapping tag represents a new set of rules.
131                         mConfigBuilders.add(new CarUxRestrictionsConfiguration.Builder());
132 
133                         if (!mapDrivingStateToRestrictions(parser, attrs)) {
134                             Log.e(TAG, "Could not map driving state to restriction.");
135                             return false;
136                         }
137                         break;
138                     case RESTRICTION_PARAMETERS:
139                         if (!parseRestrictionParameters(parser, attrs)) {
140                             // Failure to parse is automatically handled by falling back to
141                             // defaults. Just log the information here.
142                             if (Log.isLoggable(TAG, Log.INFO)) {
143                                 Log.i(TAG, "Error reading restrictions parameters. "
144                                         + "Falling back to platform defaults.");
145                             }
146                         }
147                         break;
148                     default:
149                         Log.w(TAG, "Unknown class:" + parser.getName());
150                 }
151             }
152         }
153         return true;
154     }
155 
156     /**
157      * Parses the information in the <restrictionMapping> tag to construct the mapping from
158      * driving state to UX restrictions.
159      */
mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs)160     private boolean mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs)
161             throws IOException, XmlPullParserException {
162         if (parser == null || attrs == null) {
163             Log.e(TAG, "Invalid arguments");
164             return false;
165         }
166         // The parser should be at the <RestrictionMapping> tag at this point.
167         if (!RESTRICTION_MAPPING.equals(parser.getName())) {
168             Log.e(TAG, "Parser not at RestrictionMapping element: " + parser.getName());
169             return false;
170         }
171         {
172             // Use a floating block to limit the scope of TypedArray and ensure it's recycled.
173             TypedArray a = mContext.getResources().obtainAttributes(attrs,
174                     R.styleable.UxRestrictions_RestrictionMapping);
175             if (a.hasValue(R.styleable.UxRestrictions_RestrictionMapping_physicalPort)) {
176                 int portValue = a.getInt(
177                         R.styleable.UxRestrictions_RestrictionMapping_physicalPort, 0);
178                 byte port = CarUxRestrictionsConfiguration.Builder.validatePort(portValue);
179                 getCurrentBuilder().setPhysicalPort(port);
180             }
181             a.recycle();
182         }
183 
184         if (!traverseToTag(parser, DRIVING_STATE)) {
185             Log.e(TAG, "No <" + DRIVING_STATE + "> tag in XML");
186             return false;
187         }
188         // Handle all the <DrivingState> tags.
189         while (DRIVING_STATE.equals(parser.getName())) {
190             if (parser.getEventType() == XmlResourceParser.START_TAG) {
191                 // 1. Get the driving state attributes: driving state and speed range
192                 TypedArray a = mContext.getResources().obtainAttributes(attrs,
193                         R.styleable.UxRestrictions_DrivingState);
194                 int drivingState = a.getInt(R.styleable.UxRestrictions_DrivingState_state,
195                         CarDrivingStateEvent.DRIVING_STATE_UNKNOWN);
196                 float minSpeed = a.getFloat(R.styleable.UxRestrictions_DrivingState_minSpeed,
197                         INVALID_SPEED);
198                 float maxSpeed = a.getFloat(R.styleable.UxRestrictions_DrivingState_maxSpeed,
199                         Builder.SpeedRange.MAX_SPEED);
200                 a.recycle();
201 
202                 // 2. Traverse to the <Restrictions> tag
203                 if (!traverseToTag(parser, RESTRICTIONS)) {
204                     Log.e(TAG, "No <" + RESTRICTIONS + "> tag in XML");
205                     return false;
206                 }
207 
208                 // 3. Parse the restrictions for this driving state
209                 Builder.SpeedRange speedRange = parseSpeedRange(minSpeed, maxSpeed);
210                 if (!parseAllRestrictions(parser, attrs, drivingState, speedRange)) {
211                     Log.e(TAG, "Could not parse restrictions for driving state:" + drivingState);
212                     return false;
213                 }
214             }
215             parser.next();
216         }
217         return true;
218     }
219 
220     /**
221      * Parses all <restrictions> tags nested with <drivingState> tag.
222      */
parseAllRestrictions(XmlResourceParser parser, AttributeSet attrs, int drivingState, Builder.SpeedRange speedRange)223     private boolean parseAllRestrictions(XmlResourceParser parser, AttributeSet attrs,
224             int drivingState, Builder.SpeedRange speedRange)
225             throws IOException, XmlPullParserException {
226         if (parser == null || attrs == null) {
227             Log.e(TAG, "Invalid arguments");
228             return false;
229         }
230         // The parser should be at the <Restrictions> tag at this point.
231         if (!RESTRICTIONS.equals(parser.getName())) {
232             Log.e(TAG, "Parser not at Restrictions element: " + parser.getName());
233             return false;
234         }
235         while (RESTRICTIONS.equals(parser.getName())) {
236             if (parser.getEventType() == XmlResourceParser.START_TAG) {
237                 // Parse one restrictions tag.
238                 DrivingStateRestrictions restrictions = parseRestrictions(parser, attrs);
239                 if (restrictions == null) {
240                     Log.e(TAG, "");
241                     return false;
242                 }
243                 restrictions.setSpeedRange(speedRange);
244 
245                 if (Log.isLoggable(TAG, Log.DEBUG)) {
246                     Log.d(TAG, "Map " + drivingState + " : " + restrictions);
247                 }
248 
249                 // Update the builder if the driving state and restrictions info are valid.
250                 if (drivingState != CarDrivingStateEvent.DRIVING_STATE_UNKNOWN
251                         && restrictions != null) {
252                     getCurrentBuilder().setUxRestrictions(drivingState, restrictions);
253                 }
254             }
255             parser.next();
256         }
257         return true;
258     }
259 
260     /**
261      * Parses the <restrictions> tag nested with the <drivingState>.  This provides the restrictions
262      * for the enclosing driving state.
263      */
264     @Nullable
parseRestrictions(XmlResourceParser parser, AttributeSet attrs)265     private DrivingStateRestrictions parseRestrictions(XmlResourceParser parser, AttributeSet attrs)
266             throws IOException, XmlPullParserException {
267         if (parser == null || attrs == null) {
268             Log.e(TAG, "Invalid Arguments");
269             return null;
270         }
271 
272         int restrictions = UX_RESTRICTIONS_UNKNOWN;
273         String restrictionMode = UX_RESTRICTION_MODE_BASELINE;
274         boolean requiresOpt = true;
275         while (RESTRICTIONS.equals(parser.getName())
276                 && parser.getEventType() == XmlResourceParser.START_TAG) {
277             TypedArray a = mContext.getResources().obtainAttributes(attrs,
278                     R.styleable.UxRestrictions_Restrictions);
279             restrictions = a.getInt(
280                     R.styleable.UxRestrictions_Restrictions_uxr,
281                     CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED);
282             requiresOpt = a.getBoolean(
283                     R.styleable.UxRestrictions_Restrictions_requiresDistractionOptimization, true);
284             restrictionMode = a.getString(R.styleable.UxRestrictions_Restrictions_mode);
285 
286             a.recycle();
287             parser.next();
288         }
289         if (restrictionMode == null) {
290             restrictionMode = UX_RESTRICTION_MODE_BASELINE;
291         }
292         return new DrivingStateRestrictions()
293                 .setDistractionOptimizationRequired(requiresOpt)
294                 .setRestrictions(restrictions)
295                 .setMode(restrictionMode);
296     }
297 
298     @Nullable
parseSpeedRange(float minSpeed, float maxSpeed)299     private Builder.SpeedRange parseSpeedRange(float minSpeed, float maxSpeed) {
300         if (Float.compare(minSpeed, 0) < 0 || Float.compare(maxSpeed, 0) < 0) {
301             return null;
302         }
303         return new CarUxRestrictionsConfiguration.Builder.SpeedRange(minSpeed, maxSpeed);
304     }
305 
traverseToTag(XmlResourceParser parser, String tag)306     private boolean traverseToTag(XmlResourceParser parser, String tag)
307             throws IOException, XmlPullParserException {
308         if (tag == null || parser == null) {
309             return false;
310         }
311         int type;
312         while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
313             if (type == XmlResourceParser.START_TAG && parser.getName().equals(tag)) {
314                 return true;
315             }
316         }
317         return false;
318     }
319 
320     /**
321      * Parses the information in the <RestrictionParameters> tag to read the parameters for the
322      * applicable UX restrictions
323      */
parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs)324     private boolean parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs)
325             throws IOException, XmlPullParserException {
326         if (parser == null || attrs == null) {
327             Log.e(TAG, "Invalid arguments");
328             return false;
329         }
330         // The parser should be at the <RestrictionParameters> tag at this point.
331         if (!RESTRICTION_PARAMETERS.equals(parser.getName())) {
332             Log.e(TAG, "Parser not at RestrictionParameters element: " + parser.getName());
333             return false;
334         }
335         while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) {
336             int type = parser.next();
337             // Break if we have parsed all <RestrictionParameters>
338             if (type == XmlResourceParser.END_TAG && RESTRICTION_PARAMETERS.equals(
339                     parser.getName())) {
340                 return true;
341             }
342             if (type == XmlResourceParser.START_TAG) {
343                 TypedArray a = null;
344                 switch (parser.getName()) {
345                     case STRING_RESTRICTIONS:
346                         a = mContext.getResources().obtainAttributes(attrs,
347                                 R.styleable.UxRestrictions_StringRestrictions);
348                         mMaxRestrictedStringLength = a.getInt(
349                                 R.styleable.UxRestrictions_StringRestrictions_maxLength,
350                                 UX_RESTRICTIONS_UNKNOWN);
351 
352                         break;
353                     case CONTENT_RESTRICTIONS:
354                         a = mContext.getResources().obtainAttributes(attrs,
355                                 R.styleable.UxRestrictions_ContentRestrictions);
356                         mMaxCumulativeContentItems = a.getInt(
357                                 R.styleable.UxRestrictions_ContentRestrictions_maxCumulativeItems,
358                                 UX_RESTRICTIONS_UNKNOWN);
359                         mMaxContentDepth = a.getInt(
360                                 R.styleable.UxRestrictions_ContentRestrictions_maxDepth,
361                                 UX_RESTRICTIONS_UNKNOWN);
362                         break;
363                     default:
364                         if (Log.isLoggable(TAG, Log.DEBUG)) {
365                             Log.d(TAG, "Unsupported Restriction Parameters in XML: "
366                                     + parser.getName());
367                         }
368                         break;
369                 }
370                 if (a != null) {
371                     a.recycle();
372                 }
373             }
374         }
375         return true;
376     }
377 
getCurrentBuilder()378     private CarUxRestrictionsConfiguration.Builder getCurrentBuilder() {
379         return mConfigBuilders.get(mConfigBuilders.size() - 1);
380     }
381 }
382 
383