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