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