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