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