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 android.annotation.Nullable; 20 import android.annotation.XmlRes; 21 import android.car.drivingstate.CarDrivingStateEvent; 22 import android.car.drivingstate.CarDrivingStateEvent.CarDrivingState; 23 import android.car.drivingstate.CarUxRestrictions; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.content.res.XmlResourceParser; 27 import android.os.SystemClock; 28 import android.util.AttributeSet; 29 import android.util.Log; 30 import android.util.Pair; 31 import android.util.Xml; 32 33 import org.xmlpull.v1.XmlPullParserException; 34 35 import java.io.IOException; 36 import java.io.PrintWriter; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 42 /** 43 * Helper class to {@link CarUxRestrictionsManagerService} and it takes care of the foll: 44 * <ol> 45 * <li>Parses the given XML resource and builds a hashmap to store the driving state to UX 46 * restrictions mapping information provided in the XML.</li> 47 * <li>Finds the UX restrictions for the given driving state and speed from the data structure it 48 * built above.</li> 49 * </ol> 50 */ 51 /* package */ class CarUxRestrictionsServiceHelper { 52 private static final String TAG = "UxRServiceHelper"; 53 private static final int UX_RESTRICTIONS_UNKNOWN = -1; 54 // XML tags to parse 55 private static final String ROOT_ELEMENT = "UxRestrictions"; 56 private static final String RESTRICTION_MAPPING = "RestrictionMapping"; 57 private static final String RESTRICTION_PARAMETERS = "RestrictionParameters"; 58 private static final String DRIVING_STATE = "DrivingState"; 59 private static final String RESTRICTIONS = "Restrictions"; 60 private static final String STRING_RESTRICTIONS = "StringRestrictions"; 61 private static final String CONTENT_RESTRICTIONS = "ContentRestrictions"; 62 63 /* Hashmap that maps driving state to RestrictionsInfo. 64 RestrictionsInfo maintains a list of RestrictionsPerSpeedRange. 65 The list size will be one for Parked and Idling states, but could be more than one 66 for Moving state, if moving state supports multiple speed ranges.*/ 67 private Map<Integer, RestrictionsInfo> mRestrictionsMap = new HashMap<>(); 68 private RestrictionParameters mRestrictionParameters = new RestrictionParameters(); 69 private final Context mContext; 70 @XmlRes 71 private final int mXmlResource; 72 CarUxRestrictionsServiceHelper(Context context, @XmlRes int xmlRes)73 CarUxRestrictionsServiceHelper(Context context, @XmlRes int xmlRes) { 74 mContext = context; 75 mXmlResource = xmlRes; 76 } 77 78 /** 79 * Loads the UX restrictions related information from the XML resource. 80 * 81 * @return true if successful, false if the XML is malformed 82 */ loadUxRestrictionsFromXml()83 public boolean loadUxRestrictionsFromXml() throws IOException, XmlPullParserException { 84 mRestrictionsMap.clear(); 85 XmlResourceParser parser = mContext.getResources().getXml(mXmlResource); 86 AttributeSet attrs = Xml.asAttributeSet(parser); 87 int type; 88 // Traverse till we get to the first tag 89 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 90 && type != XmlResourceParser.START_TAG) { 91 } 92 if (!ROOT_ELEMENT.equals(parser.getName())) { 93 Log.e(TAG, "XML root element invalid: " + parser.getName()); 94 return false; 95 } 96 97 // Traverse till the end and every time we hit a start tag, check for the type of the tag 98 // and load the corresponding information. 99 while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) { 100 if (parser.next() == XmlResourceParser.START_TAG) { 101 switch (parser.getName()) { 102 case RESTRICTION_MAPPING: 103 if (!mapDrivingStateToRestrictions(parser, attrs)) { 104 return false; 105 } 106 break; 107 case RESTRICTION_PARAMETERS: 108 if (!parseRestrictionParameters(parser, attrs)) { 109 // Failure to parse is automatically handled by falling back to 110 // defaults. Just log the information here 111 if (Log.isLoggable(TAG, Log.INFO)) { 112 Log.i(TAG, 113 "Error reading restrictions parameters. Falling back to " 114 + "platform defaults."); 115 } 116 } 117 break; 118 default: 119 Log.w(TAG, "Unknown class:" + parser.getName()); 120 } 121 } 122 } 123 return true; 124 } 125 126 /** 127 * Parses the information in the <restrictionMapping> tag to construct the mapping from 128 * driving state to UX restrictions. 129 */ mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs)130 private boolean mapDrivingStateToRestrictions(XmlResourceParser parser, AttributeSet attrs) 131 throws IOException, XmlPullParserException { 132 if (parser == null || attrs == null) { 133 Log.e(TAG, "Invalid arguments"); 134 return false; 135 } 136 // The parser should be at the <RestrictionMapping> tag at this point. 137 if (!RESTRICTION_MAPPING.equals(parser.getName())) { 138 Log.e(TAG, "Parser not at RestrictionMapping element: " + parser.getName()); 139 return false; 140 } 141 if (!traverseToTag(parser, DRIVING_STATE)) { 142 Log.e(TAG, "No <" + DRIVING_STATE + "> tag in XML"); 143 return false; 144 } 145 // Handle all the <DrivingState> tags. 146 while (DRIVING_STATE.equals(parser.getName())) { 147 if (parser.getEventType() == XmlResourceParser.START_TAG) { 148 // 1. Get the driving state attributes: driving state and speed range 149 TypedArray a = mContext.getResources().obtainAttributes(attrs, 150 R.styleable.UxRestrictions_DrivingState); 151 int drivingState = a 152 .getInt(R.styleable.UxRestrictions_DrivingState_state, 153 CarDrivingStateEvent.DRIVING_STATE_UNKNOWN); 154 float minSpeed = a 155 .getFloat( 156 R.styleable 157 .UxRestrictions_DrivingState_minSpeed, 158 RestrictionsPerSpeedRange.SPEED_INVALID); 159 float maxSpeed = a 160 .getFloat( 161 R.styleable 162 .UxRestrictions_DrivingState_maxSpeed, 163 RestrictionsPerSpeedRange.SPEED_INVALID); 164 a.recycle(); 165 166 // 2. Traverse to the <Restrictions> tag 167 if (!traverseToTag(parser, RESTRICTIONS)) { 168 Log.e(TAG, "No <" + RESTRICTIONS + "> tag in XML"); 169 return false; 170 } 171 172 // 3. Parse the restrictions for this driving state 173 Pair<Boolean, Integer> restrictions = parseRestrictions(parser, attrs); 174 if (Log.isLoggable(TAG, Log.DEBUG)) { 175 Log.d(TAG, "Map " + drivingState + " : " + restrictions); 176 } 177 178 // Update the hashmap if the driving state and restrictions info are valid. 179 if (drivingState != CarDrivingStateEvent.DRIVING_STATE_UNKNOWN 180 && restrictions != null) { 181 addToRestrictionsMap(drivingState, minSpeed, maxSpeed, restrictions.first, 182 restrictions.second); 183 } 184 } 185 parser.next(); 186 } 187 return true; 188 } 189 190 /** 191 * Parses the <restrictions> tag nested with the <drivingState>. This provides the restrictions 192 * for the enclosing driving state. 193 */ 194 @Nullable parseRestrictions(XmlResourceParser parser, AttributeSet attrs)195 private Pair<Boolean, Integer> parseRestrictions(XmlResourceParser parser, AttributeSet attrs) 196 throws IOException, XmlPullParserException { 197 int restrictions = UX_RESTRICTIONS_UNKNOWN; 198 boolean requiresOpt = true; 199 if (parser == null || attrs == null) { 200 Log.e(TAG, "Invalid Arguments"); 201 return null; 202 } 203 204 while (RESTRICTIONS.equals(parser.getName()) 205 && parser.getEventType() == XmlResourceParser.START_TAG) { 206 TypedArray a = mContext.getResources().obtainAttributes(attrs, 207 R.styleable.UxRestrictions_Restrictions); 208 restrictions = a.getInt( 209 R.styleable.UxRestrictions_Restrictions_uxr, 210 CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED); 211 requiresOpt = a.getBoolean( 212 R.styleable.UxRestrictions_Restrictions_requiresDistractionOptimization, true); 213 a.recycle(); 214 parser.next(); 215 } 216 return new Pair<>(requiresOpt, restrictions); 217 } 218 addToRestrictionsMap(int drivingState, float minSpeed, float maxSpeed, boolean requiresOpt, int restrictions)219 private void addToRestrictionsMap(int drivingState, float minSpeed, float maxSpeed, 220 boolean requiresOpt, int restrictions) { 221 RestrictionsPerSpeedRange res = new RestrictionsPerSpeedRange(minSpeed, maxSpeed, 222 restrictions, requiresOpt); 223 RestrictionsInfo restrictionsList = mRestrictionsMap.get(drivingState); 224 if (restrictionsList == null) { 225 restrictionsList = new RestrictionsInfo(); 226 } 227 restrictionsList.addRestrictions(res); 228 mRestrictionsMap.put(drivingState, restrictionsList); 229 } 230 traverseToTag(XmlResourceParser parser, String tag)231 private boolean traverseToTag(XmlResourceParser parser, String tag) 232 throws IOException, XmlPullParserException { 233 if (tag == null || parser == null) { 234 return false; 235 } 236 int type; 237 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) { 238 if (type == XmlResourceParser.START_TAG && parser.getName().equals(tag)) { 239 return true; 240 } 241 } 242 return false; 243 } 244 245 /** 246 * Parses the information in the <RestrictionParameters> tag to read the parameters for the 247 * applicable UX restrictions 248 */ parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs)249 private boolean parseRestrictionParameters(XmlResourceParser parser, AttributeSet attrs) 250 throws IOException, XmlPullParserException { 251 if (parser == null || attrs == null) { 252 Log.e(TAG, "Invalid arguments"); 253 return false; 254 } 255 // The parser should be at the <RestrictionParameters> tag at this point. 256 if (!RESTRICTION_PARAMETERS.equals(parser.getName())) { 257 Log.e(TAG, "Parser not at RestrictionParameters element: " + parser.getName()); 258 return false; 259 } 260 while (parser.getEventType() != XmlResourceParser.END_DOCUMENT) { 261 int type = parser.next(); 262 // Break if we have parsed all <RestrictionParameters> 263 if (type == XmlResourceParser.END_TAG && RESTRICTION_PARAMETERS.equals( 264 parser.getName())) { 265 return true; 266 } 267 if (type == XmlResourceParser.START_TAG) { 268 TypedArray a = null; 269 switch (parser.getName()) { 270 case STRING_RESTRICTIONS: 271 a = mContext.getResources().obtainAttributes(attrs, 272 R.styleable.UxRestrictions_StringRestrictions); 273 mRestrictionParameters.mMaxStringLength = a 274 .getInt(R.styleable.UxRestrictions_StringRestrictions_maxLength, 275 UX_RESTRICTIONS_UNKNOWN); 276 277 break; 278 case CONTENT_RESTRICTIONS: 279 a = mContext.getResources().obtainAttributes(attrs, 280 R.styleable.UxRestrictions_ContentRestrictions); 281 mRestrictionParameters.mMaxCumulativeContentItems = a.getInt(R.styleable 282 .UxRestrictions_ContentRestrictions_maxCumulativeItems, 283 UX_RESTRICTIONS_UNKNOWN); 284 mRestrictionParameters.mMaxContentDepth = a 285 .getInt(R.styleable.UxRestrictions_ContentRestrictions_maxDepth, 286 UX_RESTRICTIONS_UNKNOWN); 287 break; 288 default: 289 if (Log.isLoggable(TAG, Log.DEBUG)) { 290 Log.d(TAG, "Unsupported Restriction Parameters in XML: " 291 + parser.getName()); 292 } 293 break; 294 } 295 if (a != null) { 296 a.recycle(); 297 } 298 } 299 } 300 return true; 301 } 302 303 /** 304 * Dump the driving state to UX restrictions mapping. 305 */ dump(PrintWriter writer)306 public void dump(PrintWriter writer) { 307 for (Integer state : mRestrictionsMap.keySet()) { 308 RestrictionsInfo list = mRestrictionsMap.get(state); 309 writer.println("==========================================="); 310 writer.println("Driving State to UXR"); 311 if (list != null && list.mRestrictionsList != null) { 312 writer.println("State:" + getDrivingStateName(state) + " num restrictions:" 313 + list.mRestrictionsList.size()); 314 for (RestrictionsPerSpeedRange r : list.mRestrictionsList) { 315 writer.println( 316 "Speed Range: " + r.mMinSpeed + "-" + r.mMaxSpeed + " Requires DO? " 317 + r.mRequiresDistractionOptimization + " Restrictions: 0x" 318 + Integer.toHexString(r.mRestrictions)); 319 writer.println("==========================================="); 320 } 321 } 322 } 323 writer.println("Max String length: " + mRestrictionParameters.mMaxStringLength); 324 writer.println( 325 "Max Cumul Content Items: " + mRestrictionParameters.mMaxCumulativeContentItems); 326 writer.println("Max Content depth: " + mRestrictionParameters.mMaxContentDepth); 327 } 328 getDrivingStateName(int state)329 private static String getDrivingStateName(int state) { 330 switch (state) { 331 case 0: 332 return "parked"; 333 case 1: 334 return "idling"; 335 case 2: 336 return "moving"; 337 default: 338 return "unknown"; 339 } 340 } 341 342 /** 343 * Get the UX restrictions for the given driving state and speed. 344 * 345 * @param drivingState driving state of the vehicle 346 * @param currentSpeed speed of the vehicle 347 * @return UX restrictions for the given driving state and speed. 348 */ getUxRestrictions(@arDrivingState int drivingState, float currentSpeed)349 public CarUxRestrictions getUxRestrictions(@CarDrivingState int drivingState, 350 float currentSpeed) { 351 RestrictionsPerSpeedRange restrictions; 352 RestrictionsInfo restrictionsList = mRestrictionsMap.get(drivingState); 353 // If the XML hasn't been parsed or if the given driving state is not supported in the 354 // XML, return fully restricted. 355 if (restrictionsList == null || restrictionsList.mRestrictionsList == null 356 || restrictionsList.mRestrictionsList.isEmpty()) { 357 return createUxRestrictionsEvent(true, 358 CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED); 359 } 360 // For Parked and Idling, the restrictions list will have only one item, since multiple 361 // speed ranges don't make sense in those driving states. 362 if (restrictionsList.mRestrictionsList.size() == 1) { 363 restrictions = restrictionsList.mRestrictionsList.get(0); 364 } else { 365 restrictions = restrictionsList.findRestrictions(currentSpeed); 366 } 367 if (restrictions != null) { 368 return createUxRestrictionsEvent(restrictions.mRequiresDistractionOptimization, 369 restrictions.mRestrictions); 370 } else { 371 return createUxRestrictionsEvent(true, 372 CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED); 373 } 374 } 375 createUxRestrictionsEvent(boolean requiresOpt, @CarUxRestrictions.CarUxRestrictionsInfo int uxr)376 /* package */ CarUxRestrictions createUxRestrictionsEvent(boolean requiresOpt, 377 @CarUxRestrictions.CarUxRestrictionsInfo int uxr) { 378 // In case the UXR is not baseline, set requiresDistractionOptimization to true since it 379 // doesn't make sense to have an active non baseline restrictions without 380 // requiresDistractionOptimization set to true. 381 if (uxr != CarUxRestrictions.UX_RESTRICTIONS_BASELINE) { 382 requiresOpt = true; 383 } 384 CarUxRestrictions.Builder builder = new CarUxRestrictions.Builder(requiresOpt, uxr, 385 SystemClock.elapsedRealtimeNanos()); 386 if (mRestrictionParameters.mMaxStringLength != UX_RESTRICTIONS_UNKNOWN) { 387 builder.setMaxStringLength(mRestrictionParameters.mMaxStringLength); 388 } 389 if (mRestrictionParameters.mMaxCumulativeContentItems != UX_RESTRICTIONS_UNKNOWN) { 390 builder.setMaxCumulativeContentItems( 391 mRestrictionParameters.mMaxCumulativeContentItems); 392 } 393 if (mRestrictionParameters.mMaxContentDepth != UX_RESTRICTIONS_UNKNOWN) { 394 builder.setMaxContentDepth(mRestrictionParameters.mMaxContentDepth); 395 } 396 return builder.build(); 397 } 398 399 /** 400 * Container for the UX restrictions that could be parametrized 401 */ 402 private class RestrictionParameters { 403 int mMaxStringLength = UX_RESTRICTIONS_UNKNOWN; 404 int mMaxCumulativeContentItems = UX_RESTRICTIONS_UNKNOWN; 405 int mMaxContentDepth = UX_RESTRICTIONS_UNKNOWN; 406 } 407 408 /** 409 * Container for UX restrictions for a speed range. 410 * Speed range is valid only for the {@link CarDrivingStateEvent#DRIVING_STATE_MOVING}. 411 */ 412 private class RestrictionsPerSpeedRange { 413 static final int SPEED_INVALID = -1; 414 final float mMinSpeed; 415 final float mMaxSpeed; 416 final int mRestrictions; 417 final boolean mRequiresDistractionOptimization; 418 RestrictionsPerSpeedRange(float minSpeed, float maxSpeed, int restrictions, boolean requiresOpt)419 RestrictionsPerSpeedRange(float minSpeed, float maxSpeed, int restrictions, 420 boolean requiresOpt) { 421 mMinSpeed = minSpeed; 422 mMaxSpeed = maxSpeed; 423 mRestrictions = restrictions; 424 mRequiresDistractionOptimization = requiresOpt; 425 } 426 427 /** 428 * Return if the given speed is in the range of ( {@link #mMinSpeed}, {@link #mMaxSpeed} 429 * 430 * @param speed Speed to check 431 * @return true if in range false if not. 432 */ includes(float speed)433 boolean includes(float speed) { 434 if (mMinSpeed != SPEED_INVALID && mMaxSpeed == SPEED_INVALID) { 435 // This is for a range [minSpeed, infinity). If maxSpeed 436 // is invalid and mMinSpeed is a valid, this represents a 437 // anything greater than mMinSpeed. 438 return speed >= mMinSpeed; 439 } else { 440 return speed >= mMinSpeed && speed < mMaxSpeed; 441 } 442 } 443 } 444 445 /** 446 * Container for a list of {@link RestrictionsPerSpeedRange} per driving state. 447 */ 448 private class RestrictionsInfo { 449 private List<RestrictionsPerSpeedRange> mRestrictionsList = new ArrayList<>(); 450 addRestrictions(RestrictionsPerSpeedRange r)451 void addRestrictions(RestrictionsPerSpeedRange r) { 452 mRestrictionsList.add(r); 453 } 454 455 /** 456 * Find the restrictions for the given speed. It finds the range that the given speed falls 457 * in and gets the restrictions for that speed. 458 */ 459 @Nullable findRestrictions(float speed)460 RestrictionsPerSpeedRange findRestrictions(float speed) { 461 for (RestrictionsPerSpeedRange r : mRestrictionsList) { 462 if (r.includes(speed)) { 463 return r; 464 } 465 } 466 return null; 467 } 468 } 469 } 470