1 /* 2 * Copyright (C) 2020 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.permissioncontroller.role.model; 18 19 import android.app.AppOpsManager; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PermissionInfo; 24 import android.content.res.XmlResourceParser; 25 import android.os.Build; 26 import android.util.ArrayMap; 27 import android.util.Log; 28 import android.util.Pair; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.annotation.VisibleForTesting; 33 34 import com.android.permissioncontroller.R; 35 36 import org.xmlpull.v1.XmlPullParserException; 37 38 import java.io.IOException; 39 import java.util.ArrayList; 40 import java.util.Collection; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Objects; 44 45 /** 46 * Parser for {@link Role} definitions. 47 */ 48 @VisibleForTesting 49 public class RoleParser { 50 51 private static final String LOG_TAG = RoleParser.class.getSimpleName(); 52 53 private static final String TAG_ROLES = "roles"; 54 private static final String TAG_PERMISSION_SET = "permission-set"; 55 private static final String TAG_PERMISSION = "permission"; 56 private static final String TAG_ROLE = "role"; 57 private static final String TAG_REQUIRED_COMPONENTS = "required-components"; 58 private static final String TAG_ACTIVITY = "activity"; 59 private static final String TAG_PROVIDER = "provider"; 60 private static final String TAG_RECEIVER = "receiver"; 61 private static final String TAG_SERVICE = "service"; 62 private static final String TAG_INTENT_FILTER = "intent-filter"; 63 private static final String TAG_ACTION = "action"; 64 private static final String TAG_CATEGORY = "category"; 65 private static final String TAG_DATA = "data"; 66 private static final String TAG_PERMISSIONS = "permissions"; 67 private static final String TAG_APP_OP_PERMISSIONS = "app-op-permissions"; 68 private static final String TAG_APP_OP_PERMISSION = "app-op-permission"; 69 private static final String TAG_APP_OPS = "app-ops"; 70 private static final String TAG_APP_OP = "app-op"; 71 private static final String TAG_PREFERRED_ACTIVITIES = "preferred-activities"; 72 private static final String TAG_PREFERRED_ACTIVITY = "preferred-activity"; 73 private static final String ATTRIBUTE_NAME = "name"; 74 private static final String ATTRIBUTE_ALLOW_BYPASSING_QUALIFICATION = 75 "allowBypassingQualification"; 76 private static final String ATTRIBUTE_BEHAVIOR = "behavior"; 77 private static final String ATTRIBUTE_DEFAULT_HOLDERS = "defaultHolders"; 78 private static final String ATTRIBUTE_DESCRIPTION = "description"; 79 private static final String ATTRIBUTE_EXCLUSIVE = "exclusive"; 80 private static final String ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER = "fallBackToDefaultHolder"; 81 private static final String ATTRIBUTE_LABEL = "label"; 82 private static final String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion"; 83 private static final String ATTRIBUTE_OVERRIDE_USER_WHEN_GRANTING = "overrideUserWhenGranting"; 84 private static final String ATTRIBUTE_QUERY_FLAGS = "queryFlags"; 85 private static final String ATTRIBUTE_REQUEST_TITLE = "requestTitle"; 86 private static final String ATTRIBUTE_REQUEST_DESCRIPTION = "requestDescription"; 87 private static final String ATTRIBUTE_REQUESTABLE = "requestable"; 88 private static final String ATTRIBUTE_SEARCH_KEYWORDS = "searchKeywords"; 89 private static final String ATTRIBUTE_SHORT_LABEL = "shortLabel"; 90 private static final String ATTRIBUTE_SHOW_NONE = "showNone"; 91 private static final String ATTRIBUTE_STATIC = "static"; 92 private static final String ATTRIBUTE_SYSTEM_ONLY = "systemOnly"; 93 private static final String ATTRIBUTE_VISIBLE = "visible"; 94 private static final String ATTRIBUTE_PERMISSION = "permission"; 95 private static final String ATTRIBUTE_SCHEME = "scheme"; 96 private static final String ATTRIBUTE_MIME_TYPE = "mimeType"; 97 private static final String ATTRIBUTE_MAX_TARGET_SDK_VERSION = "maxTargetSdkVersion"; 98 private static final String ATTRIBUTE_MODE = "mode"; 99 100 private static final String MODE_NAME_ALLOWED = "allowed"; 101 private static final String MODE_NAME_IGNORED = "ignored"; 102 private static final String MODE_NAME_ERRORED = "errored"; 103 private static final String MODE_NAME_DEFAULT = "default"; 104 private static final String MODE_NAME_FOREGROUND = "foreground"; 105 private static final ArrayMap<String, Integer> sModeNameToMode = new ArrayMap<>(); 106 static { sModeNameToMode.put(MODE_NAME_ALLOWED, AppOpsManager.MODE_ALLOWED)107 sModeNameToMode.put(MODE_NAME_ALLOWED, AppOpsManager.MODE_ALLOWED); sModeNameToMode.put(MODE_NAME_IGNORED, AppOpsManager.MODE_IGNORED)108 sModeNameToMode.put(MODE_NAME_IGNORED, AppOpsManager.MODE_IGNORED); sModeNameToMode.put(MODE_NAME_ERRORED, AppOpsManager.MODE_ERRORED)109 sModeNameToMode.put(MODE_NAME_ERRORED, AppOpsManager.MODE_ERRORED); sModeNameToMode.put(MODE_NAME_DEFAULT, AppOpsManager.MODE_DEFAULT)110 sModeNameToMode.put(MODE_NAME_DEFAULT, AppOpsManager.MODE_DEFAULT); sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND)111 sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND); 112 } 113 114 @NonNull 115 private final Context mContext; 116 117 private final boolean mValidationEnabled; 118 RoleParser(@onNull Context context)119 public RoleParser(@NonNull Context context) { 120 this(context, false); 121 } 122 RoleParser(@onNull Context context, boolean validationEnabled)123 RoleParser(@NonNull Context context, boolean validationEnabled) { 124 mContext = context; 125 mValidationEnabled = validationEnabled; 126 } 127 128 /** 129 * Parse the roles defined in {@code roles.xml}. 130 * 131 * @return a map from role name to {@link Role} instances 132 */ 133 @NonNull parse()134 public ArrayMap<String, Role> parse() { 135 try (XmlResourceParser parser = mContext.getResources().getXml(R.xml.roles)) { 136 Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseXml(parser); 137 if (xml == null) { 138 return new ArrayMap<>(); 139 } 140 ArrayMap<String, PermissionSet> permissionSets = xml.first; 141 ArrayMap<String, Role> roles = xml.second; 142 validateResult(permissionSets, roles); 143 return roles; 144 } catch (XmlPullParserException | IOException e) { 145 throwOrLogMessage("Unable to parse roles.xml", e); 146 return new ArrayMap<>(); 147 } 148 } 149 150 @Nullable parseXml( @onNull XmlResourceParser parser)151 private Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseXml( 152 @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException { 153 Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = null; 154 155 int type; 156 int depth; 157 int innerDepth = parser.getDepth() + 1; 158 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 159 && ((depth = parser.getDepth()) >= innerDepth 160 || type != XmlResourceParser.END_TAG)) { 161 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 162 continue; 163 } 164 165 if (parser.getName().equals(TAG_ROLES)) { 166 if (xml != null) { 167 throwOrLogMessage("Duplicate <roles>"); 168 skipCurrentTag(parser); 169 continue; 170 } 171 xml = parseRoles(parser); 172 } else { 173 throwOrLogForUnknownTag(parser); 174 skipCurrentTag(parser); 175 } 176 } 177 178 if (xml == null) { 179 throwOrLogMessage("Missing <roles>"); 180 } 181 return xml; 182 } 183 184 @NonNull parseRoles( @onNull XmlResourceParser parser)185 private Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseRoles( 186 @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException { 187 ArrayMap<String, PermissionSet> permissionSets = new ArrayMap<>(); 188 ArrayMap<String, Role> roles = new ArrayMap<>(); 189 190 int type; 191 int depth; 192 int innerDepth = parser.getDepth() + 1; 193 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 194 && ((depth = parser.getDepth()) >= innerDepth 195 || type != XmlResourceParser.END_TAG)) { 196 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 197 continue; 198 } 199 200 switch (parser.getName()) { 201 case TAG_PERMISSION_SET: { 202 PermissionSet permissionSet = parsePermissionSet(parser); 203 if (permissionSet == null) { 204 continue; 205 } 206 validateNoDuplicateElement(permissionSet.getName(), permissionSets.keySet(), 207 "permission set"); 208 permissionSets.put(permissionSet.getName(), permissionSet); 209 break; 210 } 211 case TAG_ROLE: { 212 Role role = parseRole(parser, permissionSets); 213 if (role == null) { 214 continue; 215 } 216 validateNoDuplicateElement(role.getName(), roles.keySet(), "role"); 217 roles.put(role.getName(), role); 218 break; 219 } 220 default: 221 throwOrLogForUnknownTag(parser); 222 skipCurrentTag(parser); 223 } 224 } 225 226 return new Pair<>(permissionSets, roles); 227 } 228 229 @Nullable parsePermissionSet(@onNull XmlResourceParser parser)230 private PermissionSet parsePermissionSet(@NonNull XmlResourceParser parser) 231 throws IOException, XmlPullParserException { 232 String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION_SET); 233 if (name == null) { 234 skipCurrentTag(parser); 235 return null; 236 } 237 238 List<Permission> permissions = new ArrayList<>(); 239 240 int type; 241 int depth; 242 int innerDepth = parser.getDepth() + 1; 243 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 244 && ((depth = parser.getDepth()) >= innerDepth 245 || type != XmlResourceParser.END_TAG)) { 246 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 247 continue; 248 } 249 250 if (parser.getName().equals(TAG_PERMISSION)) { 251 Permission permission = parsePermission(parser); 252 if (permission == null) { 253 continue; 254 } 255 validateNoDuplicateElement(permission, permissions, "permission"); 256 permissions.add(permission); 257 } else { 258 throwOrLogForUnknownTag(parser); 259 skipCurrentTag(parser); 260 } 261 } 262 263 return new PermissionSet(name, permissions); 264 } 265 266 @Nullable parsePermission(@onNull XmlResourceParser parser)267 private Permission parsePermission(@NonNull XmlResourceParser parser) throws IOException, 268 XmlPullParserException { 269 String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION); 270 if (name == null) { 271 skipCurrentTag(parser); 272 return null; 273 } 274 int minSdkVersion = getAttributeIntValue(parser, ATTRIBUTE_MIN_SDK_VERSION, 275 Build.VERSION_CODES.BASE); 276 return new Permission(name, minSdkVersion); 277 } 278 279 @Nullable parseRole(@onNull XmlResourceParser parser, @NonNull ArrayMap<String, PermissionSet> permissionSets)280 private Role parseRole(@NonNull XmlResourceParser parser, 281 @NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException, 282 XmlPullParserException { 283 String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ROLE); 284 if (name == null) { 285 skipCurrentTag(parser); 286 return null; 287 } 288 289 boolean allowBypassingQualification = getAttributeBooleanValue(parser, 290 ATTRIBUTE_ALLOW_BYPASSING_QUALIFICATION, false); 291 292 String behaviorClassSimpleName = getAttributeValue(parser, ATTRIBUTE_BEHAVIOR); 293 RoleBehavior behavior; 294 if (behaviorClassSimpleName != null) { 295 String behaviorClassName = RoleParser.class.getPackage().getName() + '.' 296 + behaviorClassSimpleName; 297 try { 298 behavior = (RoleBehavior) Class.forName(behaviorClassName).newInstance(); 299 } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { 300 throwOrLogMessage("Unable to instantiate behavior: " + behaviorClassName, e); 301 skipCurrentTag(parser); 302 return null; 303 } 304 } else { 305 behavior = null; 306 } 307 308 String defaultHoldersResourceName = getAttributeValue(parser, ATTRIBUTE_DEFAULT_HOLDERS); 309 310 int descriptionResource = getAttributeResourceValue(parser, ATTRIBUTE_DESCRIPTION, 0); 311 312 boolean visible = getAttributeBooleanValue(parser, ATTRIBUTE_VISIBLE, true); 313 Integer labelResource; 314 Integer shortLabelResource; 315 if (visible) { 316 if (descriptionResource == 0) { 317 skipCurrentTag(parser); 318 return null; 319 } 320 321 labelResource = requireAttributeResourceValue(parser, ATTRIBUTE_LABEL, 0, TAG_ROLE); 322 if (labelResource == null) { 323 skipCurrentTag(parser); 324 return null; 325 } 326 327 shortLabelResource = requireAttributeResourceValue(parser, ATTRIBUTE_SHORT_LABEL, 0, 328 TAG_ROLE); 329 if (shortLabelResource == null) { 330 skipCurrentTag(parser); 331 return null; 332 } 333 } else { 334 labelResource = 0; 335 shortLabelResource = 0; 336 } 337 338 Boolean exclusive = requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true, 339 TAG_ROLE); 340 if (exclusive == null) { 341 skipCurrentTag(parser); 342 return null; 343 } 344 345 boolean fallBackToDefaultHolder = getAttributeBooleanValue(parser, 346 ATTRIBUTE_FALL_BACK_TO_DEFAULT_HOLDER, false); 347 348 int minSdkVersion = getAttributeIntValue(parser, ATTRIBUTE_MIN_SDK_VERSION, 349 Build.VERSION_CODES.BASE); 350 351 boolean overrideUserWhenGranting = getAttributeBooleanValue(parser, 352 ATTRIBUTE_OVERRIDE_USER_WHEN_GRANTING, true); 353 354 boolean requestable = getAttributeBooleanValue(parser, ATTRIBUTE_REQUESTABLE, visible); 355 Integer requestDescriptionResource; 356 Integer requestTitleResource; 357 if (requestable) { 358 requestDescriptionResource = requireAttributeResourceValue(parser, 359 ATTRIBUTE_REQUEST_DESCRIPTION, 0, TAG_ROLE); 360 if (requestDescriptionResource == null) { 361 skipCurrentTag(parser); 362 return null; 363 } 364 365 requestTitleResource = requireAttributeResourceValue(parser, ATTRIBUTE_REQUEST_TITLE, 0, 366 TAG_ROLE); 367 if (requestTitleResource == null) { 368 skipCurrentTag(parser); 369 return null; 370 } 371 } else { 372 requestDescriptionResource = 0; 373 requestTitleResource = 0; 374 } 375 376 int searchKeywordsResource = getAttributeResourceValue(parser, ATTRIBUTE_SEARCH_KEYWORDS, 377 0); 378 379 boolean showNone = getAttributeBooleanValue(parser, ATTRIBUTE_SHOW_NONE, false); 380 if (showNone && !exclusive) { 381 throwOrLogMessage("showNone=\"true\" is invalid for a non-exclusive role: " + name); 382 skipCurrentTag(parser); 383 return null; 384 } 385 386 boolean statik = getAttributeBooleanValue(parser, ATTRIBUTE_STATIC, false); 387 if (statik && (visible || requestable)) { 388 throwOrLogMessage("static=\"true\" is invalid for a visible or requestable role: " 389 + name); 390 skipCurrentTag(parser); 391 return null; 392 } 393 394 boolean systemOnly = getAttributeBooleanValue(parser, ATTRIBUTE_SYSTEM_ONLY, false); 395 396 List<RequiredComponent> requiredComponents = null; 397 List<Permission> permissions = null; 398 List<String> appOpPermissions = null; 399 List<AppOp> appOps = null; 400 List<PreferredActivity> preferredActivities = null; 401 402 int type; 403 int depth; 404 int innerDepth = parser.getDepth() + 1; 405 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 406 && ((depth = parser.getDepth()) >= innerDepth 407 || type != XmlResourceParser.END_TAG)) { 408 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 409 continue; 410 } 411 412 switch (parser.getName()) { 413 case TAG_REQUIRED_COMPONENTS: 414 if (requiredComponents != null) { 415 throwOrLogMessage("Duplicate <required-components> in role: " + name); 416 skipCurrentTag(parser); 417 continue; 418 } 419 requiredComponents = parseRequiredComponents(parser); 420 break; 421 case TAG_PERMISSIONS: 422 if (permissions != null) { 423 throwOrLogMessage("Duplicate <permissions> in role: " + name); 424 skipCurrentTag(parser); 425 continue; 426 } 427 permissions = parsePermissions(parser, permissionSets); 428 break; 429 case TAG_APP_OP_PERMISSIONS: 430 if (appOpPermissions != null) { 431 throwOrLogMessage("Duplicate <app-op-permissions> in role: " + name); 432 skipCurrentTag(parser); 433 continue; 434 } 435 appOpPermissions = parseAppOpPermissions(parser); 436 break; 437 case TAG_APP_OPS: 438 if (appOps != null) { 439 throwOrLogMessage("Duplicate <app-ops> in role: " + name); 440 skipCurrentTag(parser); 441 continue; 442 } 443 appOps = parseAppOps(parser); 444 break; 445 case TAG_PREFERRED_ACTIVITIES: 446 if (preferredActivities != null) { 447 throwOrLogMessage("Duplicate <preferred-activities> in role: " + name); 448 skipCurrentTag(parser); 449 continue; 450 } 451 preferredActivities = parsePreferredActivities(parser); 452 break; 453 default: 454 throwOrLogForUnknownTag(parser); 455 skipCurrentTag(parser); 456 } 457 } 458 459 if (requiredComponents == null) { 460 requiredComponents = Collections.emptyList(); 461 } 462 if (permissions == null) { 463 permissions = Collections.emptyList(); 464 } 465 if (appOpPermissions == null) { 466 appOpPermissions = Collections.emptyList(); 467 } 468 if (appOps == null) { 469 appOps = Collections.emptyList(); 470 } 471 if (preferredActivities == null) { 472 preferredActivities = Collections.emptyList(); 473 } 474 return new Role(name, allowBypassingQualification, behavior, defaultHoldersResourceName, 475 descriptionResource, exclusive, fallBackToDefaultHolder, labelResource, 476 minSdkVersion, overrideUserWhenGranting, requestDescriptionResource, 477 requestTitleResource, requestable, searchKeywordsResource, shortLabelResource, 478 showNone, statik, systemOnly, visible, requiredComponents, permissions, 479 appOpPermissions, appOps, preferredActivities); 480 } 481 482 @NonNull parseRequiredComponents( @onNull XmlResourceParser parser)483 private List<RequiredComponent> parseRequiredComponents( 484 @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException { 485 List<RequiredComponent> requiredComponents = new ArrayList<>(); 486 487 int type; 488 int depth; 489 int innerDepth = parser.getDepth() + 1; 490 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 491 && ((depth = parser.getDepth()) >= innerDepth 492 || type != XmlResourceParser.END_TAG)) { 493 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 494 continue; 495 } 496 497 String name = parser.getName(); 498 switch (name) { 499 case TAG_ACTIVITY: 500 case TAG_PROVIDER: 501 case TAG_RECEIVER: 502 case TAG_SERVICE: { 503 RequiredComponent requiredComponent = parseRequiredComponent(parser, name); 504 if (requiredComponent == null) { 505 continue; 506 } 507 validateNoDuplicateElement(requiredComponent, requiredComponents, 508 "require component"); 509 requiredComponents.add(requiredComponent); 510 break; 511 } 512 default: 513 throwOrLogForUnknownTag(parser); 514 skipCurrentTag(parser); 515 } 516 } 517 518 return requiredComponents; 519 } 520 521 @Nullable parseRequiredComponent(@onNull XmlResourceParser parser, @NonNull String name)522 private RequiredComponent parseRequiredComponent(@NonNull XmlResourceParser parser, 523 @NonNull String name) throws IOException, XmlPullParserException { 524 String permission = getAttributeValue(parser, ATTRIBUTE_PERMISSION); 525 int queryFlags = getAttributeIntValue(parser, ATTRIBUTE_QUERY_FLAGS, 0); 526 IntentFilterData intentFilterData = null; 527 528 int type; 529 int depth; 530 int innerDepth = parser.getDepth() + 1; 531 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 532 && ((depth = parser.getDepth()) >= innerDepth 533 || type != XmlResourceParser.END_TAG)) { 534 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 535 continue; 536 } 537 538 switch (parser.getName()) { 539 case TAG_INTENT_FILTER: 540 if (intentFilterData != null) { 541 throwOrLogMessage("Duplicate <intent-filter> in <" + name + ">"); 542 skipCurrentTag(parser); 543 continue; 544 } 545 intentFilterData = parseIntentFilterData(parser); 546 break; 547 default: 548 throwOrLogForUnknownTag(parser); 549 skipCurrentTag(parser); 550 } 551 } 552 553 if (intentFilterData == null) { 554 throwOrLogMessage("Missing <intent-filter> in <" + name + ">"); 555 return null; 556 } 557 switch (name) { 558 case TAG_ACTIVITY: 559 return new RequiredActivity(intentFilterData, permission, queryFlags); 560 case TAG_PROVIDER: 561 return new RequiredContentProvider(intentFilterData, permission, queryFlags); 562 case TAG_RECEIVER: 563 return new RequiredBroadcastReceiver(intentFilterData, permission, queryFlags); 564 case TAG_SERVICE: 565 return new RequiredService(intentFilterData, permission, queryFlags); 566 default: 567 throwOrLogMessage("Unknown tag <" + name + ">"); 568 return null; 569 } 570 } 571 572 @Nullable parseIntentFilterData(@onNull XmlResourceParser parser)573 private IntentFilterData parseIntentFilterData(@NonNull XmlResourceParser parser) 574 throws IOException, XmlPullParserException { 575 String action = null; 576 List<String> categories = new ArrayList<>(); 577 boolean hasData = false; 578 String dataScheme = null; 579 String dataType = null; 580 581 int type; 582 int depth; 583 int innerDepth = parser.getDepth() + 1; 584 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 585 && ((depth = parser.getDepth()) >= innerDepth 586 || type != XmlResourceParser.END_TAG)) { 587 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 588 continue; 589 } 590 591 switch (parser.getName()) { 592 case TAG_ACTION: 593 if (action != null) { 594 throwOrLogMessage("Duplicate <action> in <intent-filter>"); 595 skipCurrentTag(parser); 596 continue; 597 } 598 action = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ACTION); 599 break; 600 case TAG_CATEGORY: { 601 String category = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_CATEGORY); 602 if (category == null) { 603 continue; 604 } 605 validateIntentFilterCategory(category); 606 validateNoDuplicateElement(category, categories, "category"); 607 categories.add(category); 608 break; 609 } 610 case TAG_DATA: 611 if (!hasData) { 612 hasData = true; 613 } else { 614 throwOrLogMessage("Duplicate <data> in <intent-filter>"); 615 skipCurrentTag(parser); 616 continue; 617 } 618 dataScheme = getAttributeValue(parser, ATTRIBUTE_SCHEME); 619 dataType = getAttributeValue(parser, ATTRIBUTE_MIME_TYPE); 620 if (dataType != null) { 621 validateIntentFilterDataType(dataType); 622 } 623 break; 624 default: 625 throwOrLogForUnknownTag(parser); 626 skipCurrentTag(parser); 627 } 628 } 629 630 if (action == null) { 631 throwOrLogMessage("Missing <action> in <intent-filter>"); 632 return null; 633 } 634 return new IntentFilterData(action, categories, dataScheme, dataType); 635 } 636 validateIntentFilterCategory(@onNull String category)637 private void validateIntentFilterCategory(@NonNull String category) { 638 if (Objects.equals(category, Intent.CATEGORY_DEFAULT)) { 639 throwOrLogMessage("<category> should not include " + Intent.CATEGORY_DEFAULT); 640 } 641 } 642 643 /** 644 * Validates the data type with the same logic in {@link 645 * android.content.IntentFilter#addDataType(String)} to prevent the {@code 646 * MalformedMimeTypeException}. 647 */ validateIntentFilterDataType(@onNull String type)648 private void validateIntentFilterDataType(@NonNull String type) { 649 int slashIndex = type.indexOf('/'); 650 if (slashIndex <= 0 || type.length() < slashIndex + 2) { 651 throwOrLogMessage("Invalid attribute \"mimeType\" value on <data>: " + type); 652 } 653 } 654 655 @NonNull parsePermissions(@onNull XmlResourceParser parser, @NonNull ArrayMap<String, PermissionSet> permissionSets)656 private List<Permission> parsePermissions(@NonNull XmlResourceParser parser, 657 @NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException, 658 XmlPullParserException { 659 List<Permission> permissions = new ArrayList<>(); 660 661 int type; 662 int depth; 663 int innerDepth = parser.getDepth() + 1; 664 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 665 && ((depth = parser.getDepth()) >= innerDepth 666 || type != XmlResourceParser.END_TAG)) { 667 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 668 continue; 669 } 670 671 switch (parser.getName()) { 672 case TAG_PERMISSION_SET: { 673 String permissionSetName = requireAttributeValue(parser, ATTRIBUTE_NAME, 674 TAG_PERMISSION_SET); 675 if (permissionSetName == null) { 676 continue; 677 } 678 if (!permissionSets.containsKey(permissionSetName)) { 679 throwOrLogMessage("Unknown permission set:" + permissionSetName); 680 continue; 681 } 682 PermissionSet permissionSet = permissionSets.get(permissionSetName); 683 // We do allow intersection between permission sets. 684 permissions.addAll(permissionSet.getPermissions()); 685 break; 686 } 687 case TAG_PERMISSION: { 688 Permission permission = parsePermission(parser); 689 if (permission == null) { 690 continue; 691 } 692 validateNoDuplicateElement(permission, permissions, "permission"); 693 permissions.add(permission); 694 break; 695 } 696 default: 697 throwOrLogForUnknownTag(parser); 698 skipCurrentTag(parser); 699 } 700 } 701 702 return permissions; 703 } 704 705 @NonNull parseAppOpPermissions(@onNull XmlResourceParser parser)706 private List<String> parseAppOpPermissions(@NonNull XmlResourceParser parser) 707 throws IOException, XmlPullParserException { 708 List<String> appOpPermissions = new ArrayList<>(); 709 710 int type; 711 int depth; 712 int innerDepth = parser.getDepth() + 1; 713 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 714 && ((depth = parser.getDepth()) >= innerDepth 715 || type != XmlResourceParser.END_TAG)) { 716 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 717 continue; 718 } 719 720 if (parser.getName().equals(TAG_APP_OP_PERMISSION)) { 721 String appOpPermission = requireAttributeValue(parser, ATTRIBUTE_NAME, 722 TAG_APP_OP_PERMISSION); 723 if (appOpPermission == null) { 724 continue; 725 } 726 validateNoDuplicateElement(appOpPermission, appOpPermissions, "app op permission"); 727 appOpPermissions.add(appOpPermission); 728 } else { 729 throwOrLogForUnknownTag(parser); 730 skipCurrentTag(parser); 731 } 732 } 733 734 return appOpPermissions; 735 } 736 737 @NonNull parseAppOps(@onNull XmlResourceParser parser)738 private List<AppOp> parseAppOps(@NonNull XmlResourceParser parser) throws IOException, 739 XmlPullParserException { 740 List<String> appOpNames = new ArrayList<>(); 741 List<AppOp> appOps = new ArrayList<>(); 742 743 int type; 744 int depth; 745 int innerDepth = parser.getDepth() + 1; 746 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 747 && ((depth = parser.getDepth()) >= innerDepth 748 || type != XmlResourceParser.END_TAG)) { 749 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 750 continue; 751 } 752 753 if (parser.getName().equals(TAG_APP_OP)) { 754 String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_APP_OP); 755 if (name == null) { 756 continue; 757 } 758 validateNoDuplicateElement(name, appOpNames, "app op"); 759 appOpNames.add(name); 760 Integer maxTargetSdkVersion = getAttributeIntValue(parser, 761 ATTRIBUTE_MAX_TARGET_SDK_VERSION, Integer.MIN_VALUE); 762 if (maxTargetSdkVersion == Integer.MIN_VALUE) { 763 maxTargetSdkVersion = null; 764 } 765 if (maxTargetSdkVersion != null && maxTargetSdkVersion < Build.VERSION_CODES.BASE) { 766 throwOrLogMessage("Invalid value for \"maxTargetSdkVersion\": " 767 + maxTargetSdkVersion); 768 } 769 String modeName = requireAttributeValue(parser, ATTRIBUTE_MODE, TAG_APP_OP); 770 if (modeName == null) { 771 continue; 772 } 773 int modeIndex = sModeNameToMode.indexOfKey(modeName); 774 if (modeIndex < 0) { 775 throwOrLogMessage("Unknown value for \"mode\" on <app-op>: " + modeName); 776 continue; 777 } 778 int mode = sModeNameToMode.valueAt(modeIndex); 779 AppOp appOp = new AppOp(name, maxTargetSdkVersion, mode); 780 appOps.add(appOp); 781 } else { 782 throwOrLogForUnknownTag(parser); 783 skipCurrentTag(parser); 784 } 785 } 786 787 return appOps; 788 } 789 790 @NonNull parsePreferredActivities( @onNull XmlResourceParser parser)791 private List<PreferredActivity> parsePreferredActivities( 792 @NonNull XmlResourceParser parser) throws IOException, XmlPullParserException { 793 List<PreferredActivity> preferredActivities = new ArrayList<>(); 794 795 int type; 796 int depth; 797 int innerDepth = parser.getDepth() + 1; 798 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 799 && ((depth = parser.getDepth()) >= innerDepth 800 || type != XmlResourceParser.END_TAG)) { 801 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 802 continue; 803 } 804 805 if (parser.getName().equals(TAG_PREFERRED_ACTIVITY)) { 806 PreferredActivity preferredActivity = parsePreferredActivity(parser); 807 if (preferredActivity == null) { 808 continue; 809 } 810 validateNoDuplicateElement(preferredActivity, preferredActivities, 811 "preferred activity"); 812 preferredActivities.add(preferredActivity); 813 } else { 814 throwOrLogForUnknownTag(parser); 815 skipCurrentTag(parser); 816 } 817 } 818 819 return preferredActivities; 820 } 821 822 @Nullable parsePreferredActivity(@onNull XmlResourceParser parser)823 private PreferredActivity parsePreferredActivity(@NonNull XmlResourceParser parser) 824 throws IOException, XmlPullParserException { 825 RequiredActivity activity = null; 826 List<IntentFilterData> intentFilterDatas = new ArrayList<>(); 827 828 int type; 829 int depth; 830 int innerDepth = parser.getDepth() + 1; 831 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 832 && ((depth = parser.getDepth()) >= innerDepth 833 || type != XmlResourceParser.END_TAG)) { 834 if (depth > innerDepth || type != XmlResourceParser.START_TAG) { 835 continue; 836 } 837 838 switch (parser.getName()) { 839 case TAG_ACTIVITY: 840 if (activity != null) { 841 throwOrLogMessage("Duplicate <activity> in <preferred-activity>"); 842 skipCurrentTag(parser); 843 continue; 844 } 845 activity = (RequiredActivity) parseRequiredComponent(parser, TAG_ACTIVITY); 846 break; 847 case TAG_INTENT_FILTER: 848 IntentFilterData intentFilterData = parseIntentFilterData(parser); 849 if (intentFilterData == null) { 850 continue; 851 } 852 validateNoDuplicateElement(intentFilterData, intentFilterDatas, 853 "intent filter"); 854 if (intentFilterData.getDataType() != null) { 855 throwOrLogMessage("mimeType in <data> is not supported when setting a" 856 + " preferred activity"); 857 } 858 intentFilterDatas.add(intentFilterData); 859 break; 860 default: 861 throwOrLogForUnknownTag(parser); 862 skipCurrentTag(parser); 863 } 864 } 865 866 if (activity == null) { 867 throwOrLogMessage("Missing <activity> in <preferred-activity>"); 868 return null; 869 } 870 if (intentFilterDatas.isEmpty()) { 871 throwOrLogMessage("Missing <intent-filter> in <preferred-activity>"); 872 return null; 873 } 874 return new PreferredActivity(activity, intentFilterDatas); 875 } 876 skipCurrentTag(@onNull XmlResourceParser parser)877 private void skipCurrentTag(@NonNull XmlResourceParser parser) 878 throws XmlPullParserException, IOException { 879 int type; 880 int innerDepth = parser.getDepth() + 1; 881 while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT 882 && (parser.getDepth() >= innerDepth || type != XmlResourceParser.END_TAG)) { 883 // Do nothing 884 } 885 } 886 887 @Nullable getAttributeValue(@onNull XmlResourceParser parser, @NonNull String name)888 private String getAttributeValue(@NonNull XmlResourceParser parser, 889 @NonNull String name) { 890 return parser.getAttributeValue(null, name); 891 } 892 893 @Nullable requireAttributeValue(@onNull XmlResourceParser parser, @NonNull String name, @NonNull String tagName)894 private String requireAttributeValue(@NonNull XmlResourceParser parser, 895 @NonNull String name, @NonNull String tagName) { 896 String value = getAttributeValue(parser, name); 897 if (value == null) { 898 throwOrLogMessage("Missing attribute \"" + name + "\" on <" + tagName + ">"); 899 } 900 return value; 901 } 902 getAttributeBooleanValue(@onNull XmlResourceParser parser, @NonNull String name, boolean defaultValue)903 private boolean getAttributeBooleanValue(@NonNull XmlResourceParser parser, 904 @NonNull String name, boolean defaultValue) { 905 return parser.getAttributeBooleanValue(null, name, defaultValue); 906 } 907 908 @Nullable requireAttributeBooleanValue(@onNull XmlResourceParser parser, @NonNull String name, boolean defaultValue, @NonNull String tagName)909 private Boolean requireAttributeBooleanValue(@NonNull XmlResourceParser parser, 910 @NonNull String name, boolean defaultValue, @NonNull String tagName) { 911 String value = requireAttributeValue(parser, name, tagName); 912 if (value == null) { 913 return null; 914 } 915 return getAttributeBooleanValue(parser, name, defaultValue); 916 } 917 getAttributeIntValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue)918 private int getAttributeIntValue(@NonNull XmlResourceParser parser, 919 @NonNull String name, int defaultValue) { 920 return parser.getAttributeIntValue(null, name, defaultValue); 921 } 922 getAttributeResourceValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue)923 private int getAttributeResourceValue(@NonNull XmlResourceParser parser, 924 @NonNull String name, int defaultValue) { 925 return parser.getAttributeResourceValue(null, name, defaultValue); 926 } 927 928 @Nullable requireAttributeResourceValue(@onNull XmlResourceParser parser, @NonNull String name, int defaultValue, @NonNull String tagName)929 private Integer requireAttributeResourceValue(@NonNull XmlResourceParser parser, 930 @NonNull String name, int defaultValue, @NonNull String tagName) { 931 String value = requireAttributeValue(parser, name, tagName); 932 if (value == null) { 933 return null; 934 } 935 return getAttributeResourceValue(parser, name, defaultValue); 936 } 937 validateNoDuplicateElement(@onNull T element, @NonNull Collection<T> collection, @NonNull String name)938 private <T> void validateNoDuplicateElement(@NonNull T element, 939 @NonNull Collection<T> collection, @NonNull String name) { 940 if (collection.contains(element)) { 941 throwOrLogMessage("Duplicate " + name + ": " + element); 942 } 943 } 944 throwOrLogMessage(String message)945 private void throwOrLogMessage(String message) { 946 if (mValidationEnabled) { 947 throw new IllegalArgumentException(message); 948 } else { 949 Log.wtf(LOG_TAG, message); 950 } 951 } 952 throwOrLogMessage(String message, Throwable cause)953 private void throwOrLogMessage(String message, Throwable cause) { 954 if (mValidationEnabled) { 955 throw new IllegalArgumentException(message, cause); 956 } else { 957 Log.wtf(LOG_TAG, message, cause); 958 } 959 } 960 throwOrLogForUnknownTag(@onNull XmlResourceParser parser)961 private void throwOrLogForUnknownTag(@NonNull XmlResourceParser parser) { 962 throwOrLogMessage("Unknown tag: " + parser.getName()); 963 } 964 965 /** 966 * Validates the permission names with {@code PackageManager} and ensures that all app ops with 967 * a permission in {@code AppOpsManager} have declared that permission in its role and ensures 968 * that all preferred activities are listed in the required components. 969 */ validateResult(@onNull ArrayMap<String, PermissionSet> permissionSets, @NonNull ArrayMap<String, Role> roles)970 private void validateResult(@NonNull ArrayMap<String, PermissionSet> permissionSets, 971 @NonNull ArrayMap<String, Role> roles) { 972 if (!mValidationEnabled) { 973 return; 974 } 975 976 int permissionSetsSize = permissionSets.size(); 977 for (int permissionSetsIndex = 0; permissionSetsIndex < permissionSetsSize; 978 permissionSetsIndex++) { 979 PermissionSet permissionSet = permissionSets.valueAt(permissionSetsIndex); 980 981 List<Permission> permissions = permissionSet.getPermissions(); 982 int permissionsSize = permissions.size(); 983 for (int permissionsIndex = 0; permissionsIndex < permissionsSize; permissionsIndex++) { 984 Permission permission = permissions.get(permissionsIndex); 985 986 validatePermission(permission); 987 } 988 } 989 990 int rolesSize = roles.size(); 991 for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) { 992 Role role = roles.valueAt(rolesIndex); 993 994 if (!role.isAvailableBySdkVersion()) { 995 continue; 996 } 997 998 List<RequiredComponent> requiredComponents = role.getRequiredComponents(); 999 int requiredComponentsSize = requiredComponents.size(); 1000 for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize; 1001 requiredComponentsIndex++) { 1002 RequiredComponent requiredComponent = requiredComponents.get( 1003 requiredComponentsIndex); 1004 1005 String permission = requiredComponent.getPermission(); 1006 if (permission != null) { 1007 validatePermission(permission); 1008 } 1009 } 1010 1011 List<Permission> permissions = role.getPermissions(); 1012 int permissionsSize = permissions.size(); 1013 for (int i = 0; i < permissionsSize; i++) { 1014 Permission permission = permissions.get(i); 1015 1016 validatePermission(permission); 1017 } 1018 1019 List<AppOp> appOps = role.getAppOps(); 1020 int appOpsSize = appOps.size(); 1021 for (int i = 0; i < appOpsSize; i++) { 1022 AppOp appOp = appOps.get(i); 1023 1024 validateAppOp(appOp); 1025 } 1026 1027 List<String> appOpPermissions = role.getAppOpPermissions(); 1028 int appOpPermissionsSize = appOpPermissions.size(); 1029 for (int i = 0; i < appOpPermissionsSize; i++) { 1030 String appOpPermission = appOpPermissions.get(i); 1031 1032 validateAppOpPermission(appOpPermission); 1033 } 1034 1035 List<PreferredActivity> preferredActivities = role.getPreferredActivities(); 1036 int preferredActivitiesSize = preferredActivities.size(); 1037 for (int preferredActivitiesIndex = 0; 1038 preferredActivitiesIndex < preferredActivitiesSize; 1039 preferredActivitiesIndex++) { 1040 PreferredActivity preferredActivity = preferredActivities.get( 1041 preferredActivitiesIndex); 1042 1043 if (!role.getRequiredComponents().contains(preferredActivity.getActivity())) { 1044 throw new IllegalArgumentException("<activity> of <preferred-activity> not" 1045 + " required in <required-components>, role: " + role.getName() 1046 + ", preferred activity: " + preferredActivity); 1047 } 1048 } 1049 } 1050 } 1051 validatePermission(@onNull Permission permission)1052 private void validatePermission(@NonNull Permission permission) { 1053 if (!permission.isAvailable()) { 1054 return; 1055 } 1056 validatePermission(permission.getName(), true); 1057 } 1058 validatePermission(@onNull String permission)1059 private void validatePermission(@NonNull String permission) { 1060 validatePermission(permission, false); 1061 } 1062 validatePermission(@onNull String permission, boolean enforceIsRuntimeOrRole)1063 private void validatePermission(@NonNull String permission, boolean enforceIsRuntimeOrRole) { 1064 PackageManager packageManager = mContext.getPackageManager(); 1065 boolean isAutomotive = packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); 1066 // Skip validation for car permissions which may not be available on all build targets. 1067 if (!isAutomotive && permission.startsWith("android.car")) { 1068 return; 1069 } 1070 1071 PermissionInfo permissionInfo; 1072 try { 1073 permissionInfo = packageManager.getPermissionInfo(permission, 0); 1074 } catch (PackageManager.NameNotFoundException e) { 1075 throw new IllegalArgumentException("Unknown permission: " + permission, e); 1076 } 1077 1078 if (enforceIsRuntimeOrRole) { 1079 if (!(permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS 1080 || (permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_ROLE) 1081 == PermissionInfo.PROTECTION_FLAG_ROLE)) { 1082 throw new IllegalArgumentException( 1083 "Permission is not a runtime or role permission: " + permission); 1084 } 1085 } 1086 } 1087 validateAppOpPermission(@onNull String appOpPermission)1088 private void validateAppOpPermission(@NonNull String appOpPermission) { 1089 PackageManager packageManager = mContext.getPackageManager(); 1090 PermissionInfo permissionInfo; 1091 try { 1092 permissionInfo = packageManager.getPermissionInfo(appOpPermission, 0); 1093 } catch (PackageManager.NameNotFoundException e) { 1094 throw new IllegalArgumentException("Unknown app op permission: " + appOpPermission, e); 1095 } 1096 if ((permissionInfo.getProtectionFlags() & PermissionInfo.PROTECTION_FLAG_APPOP) 1097 != PermissionInfo.PROTECTION_FLAG_APPOP) { 1098 throw new IllegalArgumentException("Permission is not an app op permission: " 1099 + appOpPermission); 1100 } 1101 } 1102 validateAppOp(@onNull AppOp appOp)1103 private void validateAppOp(@NonNull AppOp appOp) { 1104 // This throws IllegalArgumentException if app op is unknown. 1105 String permission = AppOpsManager.opToPermission(appOp.getName()); 1106 if (permission != null) { 1107 PackageManager packageManager = mContext.getPackageManager(); 1108 PermissionInfo permissionInfo; 1109 try { 1110 permissionInfo = packageManager.getPermissionInfo(permission, 0); 1111 } catch (PackageManager.NameNotFoundException e) { 1112 throw new RuntimeException(e); 1113 } 1114 if (permissionInfo.getProtection() == PermissionInfo.PROTECTION_DANGEROUS) { 1115 throw new IllegalArgumentException("App op has an associated runtime permission: " 1116 + appOp.getName()); 1117 } 1118 } 1119 } 1120 } 1121