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