blob: 80523594b4dba47d7f7c0186d855df6c092a8b81 [file] [log] [blame]
package org.robolectric.shadows;
import static android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP;
import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
import static android.content.pm.ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING;
import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE;
import static android.content.pm.ApplicationInfo.FLAG_HAS_CODE;
import static android.content.pm.ApplicationInfo.FLAG_KILL_AFTER_RESTORE;
import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
import static android.content.pm.ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_RESTORE_ANY_VERSION;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES;
import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS;
import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
import static android.content.pm.ApplicationInfo.FLAG_VM_SAFE_MODE;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.PatternMatcher.PATTERN_LITERAL;
import static android.os.PatternMatcher.PATTERN_PREFIX;
import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB;
import static java.util.Arrays.asList;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageItemInfo;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.Activity;
import android.content.pm.PackageParser.ActivityIntentInfo;
import android.content.pm.PackageParser.IntentInfo;
import android.content.pm.PackageParser.Package;
import android.content.pm.PackageParser.Permission;
import android.content.pm.PackageParser.PermissionGroup;
import android.content.pm.PackageParser.Service;
import android.content.pm.PackageParser.ServiceIntentInfo;
import android.content.pm.PathPermission;
import android.content.pm.PermissionGroupInfo;
import android.content.pm.PermissionInfo;
import android.content.pm.ProviderInfo;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Process;
import android.util.Pair;
import com.google.common.base.Strings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.manifest.ActivityData;
import org.robolectric.manifest.AndroidManifest;
import org.robolectric.manifest.BroadcastReceiverData;
import org.robolectric.manifest.ContentProviderData;
import org.robolectric.manifest.IntentFilterData;
import org.robolectric.manifest.IntentFilterData.DataAuthority;
import org.robolectric.manifest.PackageItemData;
import org.robolectric.manifest.PathPermissionData;
import org.robolectric.manifest.PermissionGroupItemData;
import org.robolectric.manifest.PermissionItemData;
import org.robolectric.manifest.ServiceData;
import org.robolectric.res.AttributeResource;
import org.robolectric.res.ResName;
import org.robolectric.util.ReflectionHelpers;
/** Creates a {@link PackageInfo} from a {@link AndroidManifest} */
public class LegacyManifestParser {
private static final List<Pair<String, Integer>> APPLICATION_FLAGS =
asList(
Pair.create("android:allowBackup", FLAG_ALLOW_BACKUP),
Pair.create("android:allowClearUserData", FLAG_ALLOW_CLEAR_USER_DATA),
Pair.create("android:allowTaskReparenting", FLAG_ALLOW_TASK_REPARENTING),
Pair.create("android:debuggable", FLAG_DEBUGGABLE),
Pair.create("android:hasCode", FLAG_HAS_CODE),
Pair.create("android:killAfterRestore", FLAG_KILL_AFTER_RESTORE),
Pair.create("android:persistent", FLAG_PERSISTENT),
Pair.create("android:resizeable", FLAG_RESIZEABLE_FOR_SCREENS),
Pair.create("android:restoreAnyVersion", FLAG_RESTORE_ANY_VERSION),
Pair.create("android:largeScreens", FLAG_SUPPORTS_LARGE_SCREENS),
Pair.create("android:normalScreens", FLAG_SUPPORTS_NORMAL_SCREENS),
Pair.create("android:anyDensity", FLAG_SUPPORTS_SCREEN_DENSITIES),
Pair.create("android:smallScreens", FLAG_SUPPORTS_SMALL_SCREENS),
Pair.create("android:testOnly", FLAG_TEST_ONLY),
Pair.create("android:vmSafeMode", FLAG_VM_SAFE_MODE));
private static final List<Pair<String, Integer>> CONFIG_OPTIONS =
asList(
Pair.create("mcc", ActivityInfo.CONFIG_MCC),
Pair.create("mnc", ActivityInfo.CONFIG_MNC),
Pair.create("locale", ActivityInfo.CONFIG_LOCALE),
Pair.create("touchscreen", ActivityInfo.CONFIG_TOUCHSCREEN),
Pair.create("keyboard", ActivityInfo.CONFIG_KEYBOARD),
Pair.create("keyboardHidden", ActivityInfo.CONFIG_KEYBOARD_HIDDEN),
Pair.create("navigation", ActivityInfo.CONFIG_NAVIGATION),
Pair.create("screenLayout", ActivityInfo.CONFIG_SCREEN_LAYOUT),
Pair.create("fontScale", ActivityInfo.CONFIG_FONT_SCALE),
Pair.create("uiMode", ActivityInfo.CONFIG_UI_MODE),
Pair.create("orientation", ActivityInfo.CONFIG_ORIENTATION),
Pair.create("screenSize", ActivityInfo.CONFIG_SCREEN_SIZE),
Pair.create("smallestScreenSize", ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE));
public static Package createPackage(AndroidManifest androidManifest) {
Package pkg = new Package(androidManifest.getPackageName());
pkg.mVersionName = androidManifest.getVersionName();
pkg.mVersionCode = androidManifest.getVersionCode();
Map<String, PermissionItemData> permissionItemData = androidManifest.getPermissions();
for (PermissionItemData itemData : permissionItemData.values()) {
Permission permission = new Permission(pkg, createPermissionInfo(pkg, itemData));
permission.metaData = permission.info.metaData;
pkg.permissions.add(permission);
}
Map<String, PermissionGroupItemData> permissionGroupItemData =
androidManifest.getPermissionGroups();
for (PermissionGroupItemData itemData : permissionGroupItemData.values()) {
PermissionGroup permissionGroup =
new PermissionGroup(pkg, createPermissionGroupInfo(pkg, itemData));
permissionGroup.metaData = permissionGroup.info.metaData;
pkg.permissionGroups.add(permissionGroup);
}
pkg.requestedPermissions.addAll(androidManifest.getUsedPermissions());
if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.M) {
List<Boolean> permissionsRequired =
ReflectionHelpers.getField(pkg, "requestedPermissionsRequired");
permissionsRequired.addAll(buildBooleanList(pkg.requestedPermissions.size(), true));
}
pkg.applicationInfo.flags = decodeFlags(androidManifest.getApplicationAttributes());
pkg.applicationInfo.targetSdkVersion = androidManifest.getTargetSdkVersion();
pkg.applicationInfo.packageName = androidManifest.getPackageName();
pkg.applicationInfo.processName = androidManifest.getProcessName();
if (!Strings.isNullOrEmpty(androidManifest.getApplicationName())) {
pkg.applicationInfo.className =
buildClassName(pkg.applicationInfo.packageName, androidManifest.getApplicationName());
if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.N_MR1) {
pkg.applicationInfo.name = pkg.applicationInfo.className;
}
}
pkg.applicationInfo.metaData = metaDataToBundle(androidManifest.getApplicationMetaData());
pkg.applicationInfo.uid = Process.myUid();
if (androidManifest.getThemeRef() != null) {
pkg.applicationInfo.theme =
RuntimeEnvironment.getAppResourceTable()
.getResourceId(
ResName.qualifyResName(
androidManifest.getThemeRef().replace("@", ""), pkg.packageName, "style"));
}
int labelRes = 0;
if (androidManifest.getLabelRef() != null) {
String fullyQualifiedName =
ResName.qualifyResName(androidManifest.getLabelRef(), androidManifest.getPackageName());
Integer id =
fullyQualifiedName == null
? null
: RuntimeEnvironment.getAppResourceTable()
.getResourceId(new ResName(fullyQualifiedName));
labelRes = id != null ? id : 0;
}
pkg.applicationInfo.labelRes = labelRes;
String labelRef = androidManifest.getLabelRef();
if (labelRef != null && !labelRef.startsWith("@")) {
pkg.applicationInfo.nonLocalizedLabel = labelRef;
}
Map<String, ActivityData> activityDatas = androidManifest.getActivityDatas();
for (ActivityData data : activityDatas.values()) {
ActivityInfo activityInfo = new ActivityInfo();
activityInfo.name = buildClassName(pkg.packageName, data.getName());
activityInfo.packageName = pkg.packageName;
activityInfo.configChanges = getConfigChanges(data);
activityInfo.parentActivityName = data.getParentActivityName();
activityInfo.metaData = metaDataToBundle(data.getMetaData().getValueMap());
activityInfo.applicationInfo = pkg.applicationInfo;
activityInfo.targetActivity = data.getTargetActivityName();
activityInfo.exported = data.isExported();
activityInfo.permission = data.getPermission();
activityInfo.enabled = data.isEnabled();
String themeRef;
// Based on ShadowActivity
if (data.getThemeRef() != null) {
themeRef = data.getThemeRef();
} else {
themeRef = androidManifest.getThemeRef();
}
if (themeRef != null) {
activityInfo.theme =
RuntimeEnvironment.getAppResourceTable()
.getResourceId(
ResName.qualifyResName(themeRef.replace("@", ""), pkg.packageName, "style"));
}
if (data.getLabel() != null) {
activityInfo.labelRes =
RuntimeEnvironment.getAppResourceTable()
.getResourceId(
ResName.qualifyResName(
data.getLabel().replace("@", ""), pkg.packageName, "string"));
if (activityInfo.labelRes == 0) {
activityInfo.nonLocalizedLabel = data.getLabel();
}
}
Activity activity = createActivity(pkg, activityInfo);
for (IntentFilterData intentFilterData : data.getIntentFilters()) {
ActivityIntentInfo outInfo = new ActivityIntentInfo(activity);
populateIntentInfo(intentFilterData, outInfo);
activity.intents.add(outInfo);
}
pkg.activities.add(activity);
}
for (ContentProviderData data : androidManifest.getContentProviders()) {
ProviderInfo info = new ProviderInfo();
populateComponentInfo(info, pkg, data);
info.authority = data.getAuthorities();
List<PathPermission> permissions = new ArrayList<>();
for (PathPermissionData permissionData : data.getPathPermissionDatas()) {
permissions.add(createPathPermission(permissionData));
}
info.pathPermissions = permissions.toArray(new PathPermission[permissions.size()]);
info.readPermission = data.getReadPermission();
info.writePermission = data.getWritePermission();
info.grantUriPermissions = data.getGrantUriPermissions();
info.enabled = data.isEnabled();
pkg.providers.add(createProvider(pkg, info));
}
for (BroadcastReceiverData data : androidManifest.getBroadcastReceivers()) {
ActivityInfo info = new ActivityInfo();
populateComponentInfo(info, pkg, data);
info.permission = data.getPermission();
info.exported = data.isExported();
info.enabled = data.isEnabled();
Activity receiver = createActivity(pkg, info);
for (IntentFilterData intentFilterData : data.getIntentFilters()) {
ActivityIntentInfo outInfo = new ActivityIntentInfo(receiver);
populateIntentInfo(intentFilterData, outInfo);
receiver.intents.add(outInfo);
}
pkg.receivers.add(receiver);
}
for (ServiceData data : androidManifest.getServices()) {
ServiceInfo info = new ServiceInfo();
populateComponentInfo(info, pkg, data);
info.permission = data.getPermission();
info.exported = data.isExported();
info.enabled = data.isEnabled();
Service service = createService(pkg, info);
for (IntentFilterData intentFilterData : data.getIntentFilters()) {
ServiceIntentInfo outInfo = new ServiceIntentInfo(service);
populateIntentInfo(intentFilterData, outInfo);
service.intents.add(outInfo);
}
pkg.services.add(service);
}
String codePath = RuntimeEnvironment.getTempDirectory()
.createIfNotExists(pkg.packageName + "-codePath")
.toAbsolutePath()
.toString();
if (RuntimeEnvironment.getApiLevel() >= LOLLIPOP) {
pkg.codePath = codePath;
} else {
ReflectionHelpers.setField(Package.class, pkg, "mPath", codePath);
}
return pkg;
}
private static PathPermission createPathPermission(PathPermissionData data) {
if (!Strings.isNullOrEmpty(data.pathPattern)) {
return new PathPermission(
data.pathPattern, PATTERN_SIMPLE_GLOB, data.readPermission, data.writePermission);
} else if (!Strings.isNullOrEmpty(data.path)) {
return new PathPermission(
data.path, PATTERN_LITERAL, data.readPermission, data.writePermission);
} else if (!Strings.isNullOrEmpty(data.pathPrefix)) {
return new PathPermission(
data.pathPrefix, PATTERN_PREFIX, data.readPermission, data.writePermission);
} else {
throw new IllegalStateException("Permission without type");
}
}
private static void populateComponentInfo(
ComponentInfo outInfo, Package owner, PackageItemData itemData) {
populatePackageItemInfo(outInfo, owner, itemData);
outInfo.applicationInfo = owner.applicationInfo;
}
private static void populatePackageItemInfo(
PackageItemInfo outInfo, Package owner, PackageItemData itemData) {
outInfo.name = buildClassName(owner.packageName, itemData.getName());
outInfo.packageName = owner.packageName;
outInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
}
private static List<Boolean> buildBooleanList(int size, boolean defaultVal) {
Boolean[] barray = new Boolean[size];
Arrays.fill(barray, defaultVal);
return Arrays.asList(barray);
}
private static PackageParser.Provider createProvider(Package pkg, ProviderInfo info) {
PackageParser.Provider provider =
ReflectionHelpers.callConstructor(PackageParser.Provider.class);
populateComponent(pkg, info, provider);
return provider;
}
private static Activity createActivity(Package pkg, ActivityInfo activityInfo) {
Activity activity = ReflectionHelpers.callConstructor(Activity.class);
populateComponent(pkg, activityInfo, activity);
return activity;
}
private static Service createService(Package pkg, ServiceInfo info) {
PackageParser.Service service = ReflectionHelpers.callConstructor(PackageParser.Service.class);
populateComponent(pkg, info, service);
return service;
}
private static void populateComponent(
Package pkg, ComponentInfo info, PackageParser.Component component) {
ReflectionHelpers.setField(component, "info", info);
ReflectionHelpers.setField(component, "intents", new ArrayList<>());
ReflectionHelpers.setField(component, "owner", pkg);
ReflectionHelpers.setField(component, "className", info.name);
}
private static void populateIntentInfo(IntentFilterData intentFilterData, IntentInfo outInfo) {
for (String action : intentFilterData.getActions()) {
outInfo.addAction(action);
}
for (String category : intentFilterData.getCategories()) {
outInfo.addCategory(category);
}
for (DataAuthority dataAuthority : intentFilterData.getAuthorities()) {
outInfo.addDataAuthority(dataAuthority.getHost(), dataAuthority.getPort());
}
for (String mimeType : intentFilterData.getMimeTypes()) {
try {
outInfo.addDataType(mimeType);
} catch (MalformedMimeTypeException e) {
throw new RuntimeException(e);
}
}
for (String scheme : intentFilterData.getSchemes()) {
outInfo.addDataScheme(scheme);
}
for (String pathPattern : intentFilterData.getPathPatterns()) {
outInfo.addDataPath(pathPattern, PATTERN_SIMPLE_GLOB);
}
for (String pathPattern : intentFilterData.getPathPrefixes()) {
outInfo.addDataPath(pathPattern, PATTERN_PREFIX);
}
for (String pathPattern : intentFilterData.getPaths()) {
outInfo.addDataPath(pathPattern, PATTERN_LITERAL);
}
}
private static int getConfigChanges(ActivityData activityData) {
String s = activityData.getConfigChanges();
int res = 0;
// quick sanity check.
if (s == null || "".equals(s)) {
return res;
}
String[] pieces = s.split("\\|", 0);
for (String s1 : pieces) {
s1 = s1.trim();
for (Pair<String, Integer> pair : CONFIG_OPTIONS) {
if (s1.equals(pair.first)) {
res |= pair.second;
break;
}
}
}
// Matches platform behavior
if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O) {
res |= ActivityInfo.CONFIG_MNC;
res |= ActivityInfo.CONFIG_MCC;
}
return res;
}
private static int decodeFlags(Map<String, String> applicationAttributes) {
int applicationFlags = 0;
for (Pair<String, Integer> pair : APPLICATION_FLAGS) {
if ("true".equals(applicationAttributes.get(pair.first))) {
applicationFlags |= pair.second;
}
}
return applicationFlags;
}
private static PermissionInfo createPermissionInfo(Package owner, PermissionItemData itemData) {
PermissionInfo permissionInfo = new PermissionInfo();
populatePackageItemInfo(permissionInfo, owner, itemData);
permissionInfo.group = itemData.getPermissionGroup();
permissionInfo.protectionLevel = decodeProtectionLevel(itemData.getProtectionLevel());
permissionInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
String descriptionRef = itemData.getDescription();
if (descriptionRef != null) {
ResName descResName =
AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
permissionInfo.descriptionRes =
RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
}
String labelRefOrString = itemData.getLabel();
if (labelRefOrString != null) {
if (AttributeResource.isResourceReference(labelRefOrString)) {
ResName labelResName =
AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
permissionInfo.labelRes =
RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
} else {
permissionInfo.nonLocalizedLabel = labelRefOrString;
}
}
return permissionInfo;
}
private static PermissionGroupInfo createPermissionGroupInfo(Package owner,
PermissionGroupItemData itemData) {
PermissionGroupInfo permissionGroupInfo = new PermissionGroupInfo();
populatePackageItemInfo(permissionGroupInfo, owner, itemData);
permissionGroupInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
String descriptionRef = itemData.getDescription();
if (descriptionRef != null) {
ResName descResName =
AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
permissionGroupInfo.descriptionRes =
RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
}
String labelRefOrString = itemData.getLabel();
if (labelRefOrString != null) {
if (AttributeResource.isResourceReference(labelRefOrString)) {
ResName labelResName =
AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
permissionGroupInfo.labelRes =
RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
} else {
permissionGroupInfo.nonLocalizedLabel = labelRefOrString;
}
}
return permissionGroupInfo;
}
private static int decodeProtectionLevel(String protectionLevel) {
if (protectionLevel == null) {
return PermissionInfo.PROTECTION_NORMAL;
}
int permissions = PermissionInfo.PROTECTION_NORMAL;
String[] levels = protectionLevel.split("\\|", 0);
for (String level : levels) {
switch (level) {
case "normal":
permissions |= PermissionInfo.PROTECTION_NORMAL;
break;
case "dangerous":
permissions |= PermissionInfo.PROTECTION_DANGEROUS;
break;
case "signature":
permissions |= PermissionInfo.PROTECTION_SIGNATURE;
break;
case "signatureOrSystem":
permissions |= PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM;
break;
case "privileged":
permissions |= PermissionInfo.PROTECTION_FLAG_PRIVILEGED;
break;
case "system":
permissions |= PermissionInfo.PROTECTION_FLAG_SYSTEM;
break;
case "development":
permissions |= PermissionInfo.PROTECTION_FLAG_DEVELOPMENT;
break;
case "appop":
permissions |= PermissionInfo.PROTECTION_FLAG_APPOP;
break;
case "pre23":
permissions |= PermissionInfo.PROTECTION_FLAG_PRE23;
break;
case "installer":
permissions |= PermissionInfo.PROTECTION_FLAG_INSTALLER;
break;
case "verifier":
permissions |= PermissionInfo.PROTECTION_FLAG_VERIFIER;
break;
case "preinstalled":
permissions |= PermissionInfo.PROTECTION_FLAG_PREINSTALLED;
break;
case "setup":
permissions |= PermissionInfo.PROTECTION_FLAG_SETUP;
break;
case "instant":
permissions |= PermissionInfo.PROTECTION_FLAG_INSTANT;
break;
case "runtime":
permissions |= PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY;
break;
case "oem":
permissions |= PermissionInfo.PROTECTION_FLAG_OEM;
break;
case "vendorPrivileged":
permissions |= PermissionInfo.PROTECTION_FLAG_VENDOR_PRIVILEGED;
break;
case "textClassifier":
permissions |= PermissionInfo.PROTECTION_FLAG_SYSTEM_TEXT_CLASSIFIER;
break;
default:
throw new IllegalArgumentException("unknown protection level " + protectionLevel);
}
}
return permissions;
}
/**
* Goes through the meta data and puts each value in to a bundle as the correct type.
*
* <p>Note that this will convert resource identifiers specified via the value attribute as well.
*
* @param meta Meta data to put in to a bundle
* @return bundle containing the meta data
*/
private static Bundle metaDataToBundle(Map<String, Object> meta) {
if (meta.size() == 0) {
return null;
}
Bundle bundle = new Bundle();
for (Map.Entry<String, Object> entry : meta.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (Boolean.class.isInstance(value)) {
bundle.putBoolean(key, (Boolean) value);
} else if (Float.class.isInstance(value)) {
bundle.putFloat(key, (Float) value);
} else if (Integer.class.isInstance(value)) {
bundle.putInt(key, (Integer) value);
} else {
bundle.putString(key, value == null ? null : value.toString());
}
}
return bundle;
}
private static String buildClassName(String pkg, String cls) {
if (Strings.isNullOrEmpty(cls)) {
throw new IllegalArgumentException("Empty class name in package " + pkg);
}
char c = cls.charAt(0);
if (c == '.') {
return (pkg + cls).intern();
}
if (cls.indexOf('.') < 0) {
StringBuilder b = new StringBuilder(pkg);
b.append('.');
b.append(cls);
return b.toString();
}
return cls;
// TODO: consider reenabling this for stricter platform-complaint checking
// if (c >= 'a' && c <= 'z') {
// return cls;
// }
// throw new IllegalArgumentException("Bad class name " + cls + " in package " + pkg);
}
}