blob: 61245c3f19639b3430f6a5649fcdeb178baca83a [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.packageinstaller.role.model;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.XmlResourceParser;
import android.os.Build;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.permissioncontroller.R;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* Provides access to all the {@link Role} definitions.
*/
public class Roles {
private static final String LOG_TAG = Roles.class.getSimpleName();
// STOPSHIP: Turn this off before we ship.
private static final boolean DEBUG = true;
private static final String TAG_ROLES = "roles";
private static final String TAG_PERMISSION_SET = "permission-set";
private static final String TAG_PERMISSION = "permission";
private static final String TAG_ROLE = "role";
private static final String TAG_REQUIRED_COMPONENTS = "required-components";
private static final String TAG_ACTIVITY = "activity";
private static final String TAG_PROVIDER = "provider";
private static final String TAG_RECEIVER = "receiver";
private static final String TAG_SERVICE = "service";
private static final String TAG_INTENT_FILTER = "intent-filter";
private static final String TAG_ACTION = "action";
private static final String TAG_CATEGORY = "category";
private static final String TAG_DATA = "data";
private static final String TAG_META_DATA = "meta-data";
private static final String TAG_PERMISSIONS = "permissions";
private static final String TAG_APP_OPS = "app-ops";
private static final String TAG_APP_OP = "app-op";
private static final String TAG_PREFERRED_ACTIVITIES = "preferred-activities";
private static final String TAG_PREFERRED_ACTIVITY = "preferred-activity";
private static final String ATTRIBUTE_NAME = "name";
private static final String ATTRIBUTE_BEHAVIOR = "behavior";
private static final String ATTRIBUTE_EXCLUSIVE = "exclusive";
private static final String ATTRIBUTE_LABEL = "label";
private static final String ATTRIBUTE_SHOW_NONE = "showNone";
private static final String ATTRIBUTE_SYSTEM_ONLY = "systemOnly";
private static final String ATTRIBUTE_PERMISSION = "permission";
private static final String ATTRIBUTE_SCHEME = "scheme";
private static final String ATTRIBUTE_MIME_TYPE = "mimeType";
private static final String ATTRIBUTE_VALUE = "value";
private static final String ATTRIBUTE_OPTIONAL = "optional";
private static final String ATTRIBUTE_MAX_TARGET_SDK_VERSION = "maxTargetSdkVersion";
private static final String ATTRIBUTE_MODE = "mode";
private static final String MODE_NAME_ALLOWED = "allowed";
private static final String MODE_NAME_IGNORED = "ignored";
private static final String MODE_NAME_ERRORED = "errored";
private static final String MODE_NAME_DEFAULT = "default";
private static final String MODE_NAME_FOREGROUND = "foreground";
private static final ArrayMap<String, Integer> sModeNameToMode = new ArrayMap<>();
static {
sModeNameToMode.put(MODE_NAME_ALLOWED, AppOpsManager.MODE_ALLOWED);
sModeNameToMode.put(MODE_NAME_IGNORED, AppOpsManager.MODE_IGNORED);
sModeNameToMode.put(MODE_NAME_ERRORED, AppOpsManager.MODE_ERRORED);
sModeNameToMode.put(MODE_NAME_DEFAULT, AppOpsManager.MODE_DEFAULT);
sModeNameToMode.put(MODE_NAME_FOREGROUND, AppOpsManager.MODE_FOREGROUND);
}
@NonNull
private static final Object sLock = new Object();
@Nullable
private static ArrayMap<String, Role> sRoles;
private static boolean sIsolatedStorage;
private static final List<String> ISOLATED_STORAGE_PERMISSIONS = new ArrayList<>();
static {
ISOLATED_STORAGE_PERMISSIONS.add(android.Manifest.permission.READ_MEDIA_AUDIO);
ISOLATED_STORAGE_PERMISSIONS.add(android.Manifest.permission.READ_MEDIA_VIDEO);
ISOLATED_STORAGE_PERMISSIONS.add(android.Manifest.permission.READ_MEDIA_IMAGES);
}
private static final List<String> LEGACY_STORAGE_PERMISSIONS = new ArrayList<>();
static {
LEGACY_STORAGE_PERMISSIONS.add(android.Manifest.permission.READ_EXTERNAL_STORAGE);
LEGACY_STORAGE_PERMISSIONS.add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
private Roles() {}
/**
* Get the roles defined in {@code roles.xml}.
*
* @param context the {@code Context} used to read the XML resource
*
* @return a map from role name to {@link Role} instances
*/
@NonNull
public static ArrayMap<String, Role> get(@NonNull Context context) {
synchronized (sLock) {
if (sRoles == null) {
sRoles = load(context);
}
return sRoles;
}
}
@NonNull
private static ArrayMap<String, Role> load(@NonNull Context context) {
// If the storage model feature flag is disabled, we need to fiddle
// around with permission definitions to return us to pre-Q behavior.
// STOPSHIP(b/112545973): remove once feature enabled by default
try {
context.getPackageManager().getPermissionInfo(ISOLATED_STORAGE_PERMISSIONS.get(0), 0);
sIsolatedStorage = true;
} catch (NameNotFoundException e) {
sIsolatedStorage = false;
}
try (XmlResourceParser parser = context.getResources().getXml(R.xml.roles)) {
Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = parseXml(parser);
if (xml == null) {
return new ArrayMap<>();
}
ArrayMap<String, PermissionSet> permissionSets = xml.first;
ArrayMap<String, Role> roles = xml.second;
validateParseResult(permissionSets, roles, context);
return roles;
} catch (XmlPullParserException | IOException e) {
throwOrLogMessage("Unable to parse roles.xml", e);
return new ArrayMap<>();
}
}
@Nullable
private static Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseXml(
@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> xml = null;
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
if (parser.getName().equals(TAG_ROLES)) {
if (xml != null) {
throwOrLogMessage("Duplicate <roles>");
skipCurrentTag(parser);
continue;
}
xml = parseRoles(parser);
} else {
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
if (xml == null) {
throwOrLogMessage("Missing <roles>");
}
return xml;
}
@NonNull
private static Pair<ArrayMap<String, PermissionSet>, ArrayMap<String, Role>> parseRoles(
@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
ArrayMap<String, PermissionSet> permissionSets = new ArrayMap<>();
ArrayMap<String, Role> roles = new ArrayMap<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_PERMISSION_SET: {
PermissionSet permissionSet = parsePermissionSet(parser);
if (permissionSet == null) {
continue;
}
checkDuplicateElement(permissionSet.getName(), permissionSets.keySet(),
"permission set");
permissionSets.put(permissionSet.getName(), permissionSet);
break;
}
case TAG_ROLE: {
Role role = parseRole(parser, permissionSets);
if (role == null) {
continue;
}
checkDuplicateElement(role.getName(), roles.keySet(), "role");
roles.put(role.getName(), role);
break;
}
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
return new Pair<>(permissionSets, roles);
}
@Nullable
private static PermissionSet parsePermissionSet(@NonNull XmlResourceParser parser)
throws IOException, XmlPullParserException {
String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION_SET);
if (name == null) {
skipCurrentTag(parser);
return null;
}
List<String> permissions = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
if (parser.getName().equals(TAG_PERMISSION)) {
String permission = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_PERMISSION);
if (permission == null) {
continue;
}
checkDuplicateElement(permission, permissions, "permission");
permissions.add(permission);
} else {
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
return new PermissionSet(name, permissions);
}
@Nullable
private static Role parseRole(@NonNull XmlResourceParser parser,
@NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException,
XmlPullParserException {
String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ROLE);
if (name == null) {
skipCurrentTag(parser);
return null;
}
String behaviorClassSimpleName = getAttributeValue(parser, ATTRIBUTE_BEHAVIOR);
RoleBehavior behavior;
if (behaviorClassSimpleName != null) {
String behaviorClassName = Roles.class.getPackage().getName() + '.'
+ behaviorClassSimpleName;
try {
behavior = (RoleBehavior) Class.forName(behaviorClassName).newInstance();
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
throwOrLogMessage("Unable to instantiate behavior: " + behaviorClassName, e);
skipCurrentTag(parser);
return null;
}
} else {
behavior = null;
}
Boolean exclusive = requireAttributeBooleanValue(parser, ATTRIBUTE_EXCLUSIVE, true,
TAG_ROLE);
if (exclusive == null) {
skipCurrentTag(parser);
return null;
}
Integer labelResource = requireAttributeResourceValue(parser, ATTRIBUTE_LABEL, 0, TAG_ROLE);
if (labelResource == null) {
skipCurrentTag(parser);
return null;
}
boolean showNone = getAttributeBooleanValue(parser, ATTRIBUTE_SHOW_NONE, false);
if (showNone && !exclusive) {
throwOrLogMessage("showNone=\"true\" is invalid for a non-exclusive role: " + name);
skipCurrentTag(parser);
return null;
}
boolean systemOnly = getAttributeBooleanValue(parser, ATTRIBUTE_SYSTEM_ONLY, false);
List<RequiredComponent> requiredComponents = null;
List<String> permissions = null;
List<AppOp> appOps = null;
List<PreferredActivity> preferredActivities = null;
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_REQUIRED_COMPONENTS:
if (requiredComponents != null) {
throwOrLogMessage("Duplicate <required-components> in role: " + name);
skipCurrentTag(parser);
continue;
}
requiredComponents = parseRequiredComponents(parser);
break;
case TAG_PERMISSIONS:
if (permissions != null) {
throwOrLogMessage("Duplicate <permissions> in role: " + name);
skipCurrentTag(parser);
continue;
}
permissions = parsePermissions(parser, permissionSets);
break;
case TAG_APP_OPS:
if (appOps != null) {
throwOrLogMessage("Duplicate <app-ops> in role: " + name);
skipCurrentTag(parser);
continue;
}
appOps = parseAppOps(parser);
break;
case TAG_PREFERRED_ACTIVITIES:
if (preferredActivities != null) {
throwOrLogMessage("Duplicate <preferred-activities> in role: " + name);
skipCurrentTag(parser);
continue;
}
preferredActivities = parsePreferredActivities(parser);
break;
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
if (requiredComponents == null) {
requiredComponents = Collections.emptyList();
}
if (permissions == null) {
permissions = Collections.emptyList();
}
if (appOps == null) {
appOps = Collections.emptyList();
}
if (preferredActivities == null) {
preferredActivities = Collections.emptyList();
}
return new Role(name, behavior, exclusive, labelResource, showNone, systemOnly,
requiredComponents, permissions, appOps, preferredActivities);
}
@NonNull
private static List<RequiredComponent> parseRequiredComponents(
@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
List<RequiredComponent> requiredComponents = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
String name = parser.getName();
switch (name) {
case TAG_ACTIVITY:
case TAG_PROVIDER:
case TAG_RECEIVER:
case TAG_SERVICE: {
RequiredComponent requiredComponent = parseRequiredComponent(parser, name);
if (requiredComponent == null) {
continue;
}
checkDuplicateElement(requiredComponent, requiredComponents,
"require component");
requiredComponents.add(requiredComponent);
break;
}
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
return requiredComponents;
}
@Nullable
private static RequiredComponent parseRequiredComponent(@NonNull XmlResourceParser parser,
@NonNull String name) throws IOException, XmlPullParserException {
String permission = getAttributeValue(parser, ATTRIBUTE_PERMISSION);
IntentFilterData intentFilterData = null;
List<RequiredMetaData> metaData = new ArrayList<>();
List<String> metaDataNames;
if (DEBUG) {
metaDataNames = new ArrayList<>();
}
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_INTENT_FILTER:
if (intentFilterData != null) {
throwOrLogMessage("Duplicate <intent-filter> in <" + name + ">");
skipCurrentTag(parser);
continue;
}
intentFilterData = parseIntentFilterData(parser);
break;
case TAG_META_DATA:
String metaDataName = requireAttributeValue(parser, ATTRIBUTE_NAME,
TAG_META_DATA);
if (metaDataName == null) {
continue;
}
if (DEBUG) {
checkDuplicateElement(metaDataName, metaDataNames, "meta data");
}
// HACK: Only support boolean for now.
// TODO: Support android:resource and other types of android:value, maybe by
// switching to TypedArray and styleables.
Boolean metaDataValue = requireAttributeBooleanValue(parser, ATTRIBUTE_VALUE,
false, TAG_META_DATA);
if (metaDataValue == null) {
continue;
}
boolean metaDataOptional = getAttributeBooleanValue(parser, ATTRIBUTE_OPTIONAL,
false);
RequiredMetaData requiredMetaData = new RequiredMetaData(metaDataName,
metaDataValue, metaDataOptional);
metaData.add(requiredMetaData);
if (DEBUG) {
metaDataNames.add(metaDataName);
}
break;
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
if (intentFilterData == null) {
throwOrLogMessage("Missing <intent-filter> in <" + name + ">");
return null;
}
switch (name) {
case TAG_ACTIVITY:
return new RequiredActivity(intentFilterData, permission, metaData);
case TAG_PROVIDER:
return new RequiredContentProvider(intentFilterData, permission, metaData);
case TAG_RECEIVER:
return new RequiredBroadcastReceiver(intentFilterData, permission, metaData);
case TAG_SERVICE:
return new RequiredService(intentFilterData, permission, metaData);
default:
throwOrLogMessage("Unknown tag <" + name + ">");
return null;
}
}
@Nullable
private static IntentFilterData parseIntentFilterData(@NonNull XmlResourceParser parser)
throws IOException, XmlPullParserException {
String action = null;
List<String> categories = new ArrayList<>();
boolean hasData = false;
String dataScheme = null;
String dataType = null;
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_ACTION:
if (action != null) {
throwOrLogMessage("Duplicate <action> in <intent-filter>");
skipCurrentTag(parser);
continue;
}
action = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_ACTION);
break;
case TAG_CATEGORY: {
String category = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_CATEGORY);
if (category == null) {
continue;
}
validateIntentFilterCategory(category);
checkDuplicateElement(category, categories, "category");
categories.add(category);
break;
}
case TAG_DATA:
if (!hasData) {
hasData = true;
} else {
throwOrLogMessage("Duplicate <data> in <intent-filter>");
skipCurrentTag(parser);
continue;
}
dataScheme = getAttributeValue(parser, ATTRIBUTE_SCHEME);
dataType = getAttributeValue(parser, ATTRIBUTE_MIME_TYPE);
if (dataType != null) {
validateIntentFilterDataType(dataType);
}
break;
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
if (action == null) {
throwOrLogMessage("Missing <action> in <intent-filter>");
return null;
}
return new IntentFilterData(action, categories, dataScheme, dataType);
}
private static void validateIntentFilterCategory(@NonNull String category) {
if (Objects.equals(category, Intent.CATEGORY_DEFAULT)) {
throwOrLogMessage("<category> should not include " + Intent.CATEGORY_DEFAULT);
}
}
/**
* Validates the data type with the same logic in {@link
* android.content.IntentFilter#addDataType(String)} to prevent the {@code
* MalformedMimeTypeException}.
*/
private static void validateIntentFilterDataType(@NonNull String type) {
int slashIndex = type.indexOf('/');
if (slashIndex <= 0 || type.length() < slashIndex + 2) {
throwOrLogMessage("Invalid attribute \"mimeType\" value on <data>: " + type);
}
}
@NonNull
private static List<String> parsePermissions(@NonNull XmlResourceParser parser,
@NonNull ArrayMap<String, PermissionSet> permissionSets) throws IOException,
XmlPullParserException {
List<String> permissions = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_PERMISSION_SET: {
String permissionSetName = requireAttributeValue(parser, ATTRIBUTE_NAME,
TAG_PERMISSION_SET);
if (permissionSetName == null) {
continue;
}
if (!permissionSets.containsKey(permissionSetName)) {
throwOrLogMessage("Unknown permission set:" + permissionSetName);
continue;
}
PermissionSet permissionSet = permissionSets.get(permissionSetName);
// We do allow intersection between permission sets.
permissions.addAll(permissionSet.getPermissions());
break;
}
case TAG_PERMISSION: {
String permission = requireAttributeValue(parser, ATTRIBUTE_NAME,
TAG_PERMISSION);
if (permission == null) {
continue;
}
checkDuplicateElement(permission, permissions, "permission");
permissions.add(permission);
break;
}
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
// If the storage model feature flag is disabled, we need to fiddle
// around with permission definitions to return us to pre-Q behavior.
// STOPSHIP(b/112545973): remove once feature enabled by default
if (!sIsolatedStorage) {
boolean removed = permissions.removeAll(ISOLATED_STORAGE_PERMISSIONS);
if (removed) {
permissions.addAll(LEGACY_STORAGE_PERMISSIONS);
}
}
return permissions;
}
@NonNull
private static List<AppOp> parseAppOps(@NonNull XmlResourceParser parser) throws IOException,
XmlPullParserException {
List<String> appOpNames = new ArrayList<>();
List<AppOp> appOps = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
if (parser.getName().equals(TAG_APP_OP)) {
String name = requireAttributeValue(parser, ATTRIBUTE_NAME, TAG_APP_OP);
if (name == null) {
continue;
}
validateAppOpName(name);
checkDuplicateElement(name, appOpNames, "app op");
appOpNames.add(name);
Integer maxTargetSdkVersion = getAttributeIntValue(parser,
ATTRIBUTE_MAX_TARGET_SDK_VERSION, Integer.MIN_VALUE);
if (maxTargetSdkVersion == Integer.MIN_VALUE) {
maxTargetSdkVersion = null;
}
if (DEBUG) {
if (maxTargetSdkVersion != null
&& maxTargetSdkVersion < Build.VERSION_CODES.BASE) {
throwOrLogMessage("Invalid value for \"maxTargetSdkVersion\": "
+ maxTargetSdkVersion);
}
}
String modeName = requireAttributeValue(parser, ATTRIBUTE_MODE, TAG_APP_OP);
if (modeName == null) {
continue;
}
int modeIndex = sModeNameToMode.indexOfKey(modeName);
if (modeIndex < 0) {
throwOrLogMessage("Unknown value for \"mode\" on <app-op>: " + modeName);
continue;
}
int mode = sModeNameToMode.valueAt(modeIndex);
AppOp appOp = new AppOp(name, maxTargetSdkVersion, mode);
appOps.add(appOp);
} else {
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
return appOps;
}
private static void validateAppOpName(@NonNull String appOpName) {
if (DEBUG) {
// Throws IllegalArgumentException if unknown.
AppOpsManager.opToPermission(appOpName);
}
}
@NonNull
private static List<PreferredActivity> parsePreferredActivities(
@NonNull XmlResourceParser parser) throws IOException, XmlPullParserException {
List<PreferredActivity> preferredActivities = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
if (parser.getName().equals(TAG_PREFERRED_ACTIVITY)) {
PreferredActivity preferredActivity = parsePreferredActivity(parser);
if (preferredActivity == null) {
continue;
}
checkDuplicateElement(preferredActivity, preferredActivities,
"preferred activity");
preferredActivities.add(preferredActivity);
} else {
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
return preferredActivities;
}
@Nullable
private static PreferredActivity parsePreferredActivity(@NonNull XmlResourceParser parser)
throws IOException, XmlPullParserException {
RequiredActivity activity = null;
List<IntentFilterData> intentFilterDatas = new ArrayList<>();
int type;
int depth;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& ((depth = parser.getDepth()) >= innerDepth
|| type != XmlResourceParser.END_TAG)) {
if (depth > innerDepth || type != XmlResourceParser.START_TAG) {
continue;
}
switch (parser.getName()) {
case TAG_ACTIVITY:
if (activity != null) {
throwOrLogMessage("Duplicate <activity> in <preferred-activity>");
skipCurrentTag(parser);
continue;
}
activity = (RequiredActivity) parseRequiredComponent(parser, TAG_ACTIVITY);
break;
case TAG_INTENT_FILTER:
IntentFilterData intentFilterData = parseIntentFilterData(parser);
if (intentFilterData == null) {
continue;
}
checkDuplicateElement(intentFilterData, intentFilterDatas,
"intent filter");
if (DEBUG) {
if (intentFilterData.getDataType() != null) {
throwOrLogMessage("mimeType in <data> is not supported when setting a"
+ " preferred activity");
}
}
intentFilterDatas.add(intentFilterData);
break;
default:
throwOrLogForUnknownTag(parser);
skipCurrentTag(parser);
}
}
if (activity == null) {
throwOrLogMessage("Missing <activity> in <preferred-activity>");
return null;
}
if (intentFilterDatas.isEmpty()) {
throwOrLogMessage("Missing <intent-filter> in <preferred-activity>");
return null;
}
return new PreferredActivity(activity, intentFilterDatas);
}
private static void skipCurrentTag(@NonNull XmlResourceParser parser)
throws XmlPullParserException, IOException {
int type;
int innerDepth = parser.getDepth() + 1;
while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT
&& (parser.getDepth() >= innerDepth || type != XmlResourceParser.END_TAG)) {
// Do nothing
}
}
@Nullable
private static String getAttributeValue(@NonNull XmlResourceParser parser,
@NonNull String name) {
return parser.getAttributeValue(null, name);
}
@Nullable
private static String requireAttributeValue(@NonNull XmlResourceParser parser,
@NonNull String name, @NonNull String tagName) {
String value = getAttributeValue(parser, name);
if (value == null) {
throwOrLogMessage("Missing attribute \"" + name + "\" on <" + tagName + ">");
}
return value;
}
private static boolean getAttributeBooleanValue(@NonNull XmlResourceParser parser,
@NonNull String name, boolean defaultValue) {
return parser.getAttributeBooleanValue(null, name, defaultValue);
}
@Nullable
private static Boolean requireAttributeBooleanValue(@NonNull XmlResourceParser parser,
@NonNull String name, boolean defaultValue, @NonNull String tagName) {
String value = requireAttributeValue(parser, name, tagName);
if (value == null) {
return null;
}
return getAttributeBooleanValue(parser, name, defaultValue);
}
private static int getAttributeIntValue(@NonNull XmlResourceParser parser,
@NonNull String name, int defaultValue) {
return parser.getAttributeIntValue(null, name, defaultValue);
}
private static int getAttributeResourceValue(@NonNull XmlResourceParser parser,
@NonNull String name, int defaultValue) {
return parser.getAttributeResourceValue(null, name, defaultValue);
}
@Nullable
private static Integer requireAttributeResourceValue(@NonNull XmlResourceParser parser,
@NonNull String name, int defaultValue, @NonNull String tagName) {
String value = requireAttributeValue(parser, name, tagName);
if (value == null) {
return null;
}
return getAttributeResourceValue(parser, name, defaultValue);
}
private static void throwOrLogMessage(String message) {
if (DEBUG) {
throw new IllegalArgumentException(message);
} else {
Log.wtf(LOG_TAG, message);
}
}
private static void throwOrLogMessage(String message, Throwable cause) {
if (DEBUG) {
throw new IllegalArgumentException(message, cause);
} else {
Log.wtf(LOG_TAG, message, cause);
}
}
private static void throwOrLogForUnknownTag(@NonNull XmlResourceParser parser) {
throwOrLogMessage("Unknown tag: " + parser.getName());
}
private static <T> void checkDuplicateElement(@NonNull T element,
@NonNull Collection<T> collection, @NonNull String name) {
if (DEBUG) {
if (collection.contains(element)) {
throw new IllegalArgumentException("Duplicate " + name + ": " + element);
}
}
}
/**
* Validates the permission names with {@code PackageManager} and ensures that all app ops with
* a permission in {@code AppOpsManager} have declared that permission in its role and ensures
* that all preferred activities are listed in the required components.
*/
private static void validateParseResult(@NonNull ArrayMap<String, PermissionSet> permissionSets,
@NonNull ArrayMap<String, Role> roles, @NonNull Context context) {
if (!DEBUG) {
return;
}
int permissionSetsSize = permissionSets.size();
for (int permissionSetsIndex = 0; permissionSetsIndex < permissionSetsSize;
permissionSetsIndex++) {
PermissionSet permissionSet = permissionSets.valueAt(permissionSetsIndex);
List<String> permissions = permissionSet.getPermissions();
int permissionsSize = permissions.size();
for (int permissionsIndex = 0; permissionsIndex < permissionsSize; permissionsIndex++) {
String permission = permissions.get(permissionsIndex);
// If the storage model feature flag is disabled, we need to fiddle
// around with permission definitions to return us to pre-Q behavior.
// STOPSHIP(b/112545973): remove once feature enabled by default
if (!sIsolatedStorage) {
if (ISOLATED_STORAGE_PERMISSIONS.contains(permission)) {
continue;
}
}
validatePermission(permission, context);
}
}
int rolesSize = roles.size();
for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) {
Role role = roles.valueAt(rolesIndex);
List<RequiredComponent> requiredComponents = role.getRequiredComponents();
int requiredComponentsSize = requiredComponents.size();
for (int requiredComponentsIndex = 0; requiredComponentsIndex < requiredComponentsSize;
requiredComponentsIndex++) {
RequiredComponent requiredComponent = requiredComponents.get(
requiredComponentsIndex);
String permission = requiredComponent.getPermission();
if (permission != null) {
validatePermission(permission, context);
}
}
List<String> permissions = role.getPermissions();
int permissionsSize = permissions.size();
for (int i = 0; i < permissionsSize; i++) {
String permission = permissions.get(i);
validatePermission(permission, context);
}
List<AppOp> appOps = role.getAppOps();
int appOpsSize = appOps.size();
for (int i = 0; i < appOpsSize; i++) {
AppOp appOp = appOps.get(i);
String permission = AppOpsManager.opToPermission(appOp.getName());
if (permission != null) {
throw new IllegalArgumentException("App op has an associated permission: "
+ appOp.getName());
}
}
List<PreferredActivity> preferredActivities = role.getPreferredActivities();
int preferredActivitiesSize = preferredActivities.size();
for (int preferredActivitiesIndex = 0;
preferredActivitiesIndex < preferredActivitiesSize;
preferredActivitiesIndex++) {
PreferredActivity preferredActivity = preferredActivities.get(
preferredActivitiesIndex);
if (!role.getRequiredComponents().contains(preferredActivity.getActivity())) {
throw new IllegalArgumentException("<activity> of <preferred-activity> not"
+ " required in <required-components>, role: " + role.getName()
+ ", preferred activity: " + preferredActivity);
}
}
}
}
private static void validatePermission(@NonNull String permission, @NonNull Context context) {
PackageManager packageManager = context.getPackageManager();
try {
packageManager.getPermissionInfo(permission, 0);
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalArgumentException("Unknown permission: " + permission, e);
}
}
}