1package org.robolectric.shadows;
2
3import static android.content.pm.ApplicationInfo.FLAG_ALLOW_BACKUP;
4import static android.content.pm.ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA;
5import static android.content.pm.ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING;
6import static android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE;
7import static android.content.pm.ApplicationInfo.FLAG_HAS_CODE;
8import static android.content.pm.ApplicationInfo.FLAG_KILL_AFTER_RESTORE;
9import static android.content.pm.ApplicationInfo.FLAG_PERSISTENT;
10import static android.content.pm.ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS;
11import static android.content.pm.ApplicationInfo.FLAG_RESTORE_ANY_VERSION;
12import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS;
13import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS;
14import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES;
15import static android.content.pm.ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS;
16import static android.content.pm.ApplicationInfo.FLAG_TEST_ONLY;
17import static android.content.pm.ApplicationInfo.FLAG_VM_SAFE_MODE;
18import static android.os.PatternMatcher.PATTERN_LITERAL;
19import static android.os.PatternMatcher.PATTERN_PREFIX;
20import static android.os.PatternMatcher.PATTERN_SIMPLE_GLOB;
21import static java.util.Arrays.asList;
22
23import android.content.IntentFilter.MalformedMimeTypeException;
24import android.content.pm.ActivityInfo;
25import android.content.pm.ComponentInfo;
26import android.content.pm.PackageInfo;
27import android.content.pm.PackageItemInfo;
28import android.content.pm.PackageParser;
29import android.content.pm.PackageParser.Activity;
30import android.content.pm.PackageParser.ActivityIntentInfo;
31import android.content.pm.PackageParser.IntentInfo;
32import android.content.pm.PackageParser.Package;
33import android.content.pm.PackageParser.Permission;
34import android.content.pm.PackageParser.PermissionGroup;
35import android.content.pm.PackageParser.Service;
36import android.content.pm.PackageParser.ServiceIntentInfo;
37import android.content.pm.PathPermission;
38import android.content.pm.PermissionGroupInfo;
39import android.content.pm.PermissionInfo;
40import android.content.pm.ProviderInfo;
41import android.content.pm.ServiceInfo;
42import android.os.Build;
43import android.os.Build.VERSION_CODES;
44import android.os.Bundle;
45import android.os.Process;
46import android.util.Pair;
47import com.google.common.base.Strings;
48import java.util.ArrayList;
49import java.util.Arrays;
50import java.util.List;
51import java.util.Map;
52import org.robolectric.RuntimeEnvironment;
53import org.robolectric.manifest.ActivityData;
54import org.robolectric.manifest.AndroidManifest;
55import org.robolectric.manifest.BroadcastReceiverData;
56import org.robolectric.manifest.ContentProviderData;
57import org.robolectric.manifest.IntentFilterData;
58import org.robolectric.manifest.IntentFilterData.DataAuthority;
59import org.robolectric.manifest.PackageItemData;
60import org.robolectric.manifest.PathPermissionData;
61import org.robolectric.manifest.PermissionGroupItemData;
62import org.robolectric.manifest.PermissionItemData;
63import org.robolectric.manifest.ServiceData;
64import org.robolectric.res.AttributeResource;
65import org.robolectric.res.ResName;
66import org.robolectric.util.ReflectionHelpers;
67
68/** Creates a {@link PackageInfo} from a {@link AndroidManifest} */
69public class LegacyManifestParser {
70
71  private static final List<Pair<String, Integer>> APPLICATION_FLAGS =
72      asList(
73          Pair.create("android:allowBackup", FLAG_ALLOW_BACKUP),
74          Pair.create("android:allowClearUserData", FLAG_ALLOW_CLEAR_USER_DATA),
75          Pair.create("android:allowTaskReparenting", FLAG_ALLOW_TASK_REPARENTING),
76          Pair.create("android:debuggable", FLAG_DEBUGGABLE),
77          Pair.create("android:hasCode", FLAG_HAS_CODE),
78          Pair.create("android:killAfterRestore", FLAG_KILL_AFTER_RESTORE),
79          Pair.create("android:persistent", FLAG_PERSISTENT),
80          Pair.create("android:resizeable", FLAG_RESIZEABLE_FOR_SCREENS),
81          Pair.create("android:restoreAnyVersion", FLAG_RESTORE_ANY_VERSION),
82          Pair.create("android:largeScreens", FLAG_SUPPORTS_LARGE_SCREENS),
83          Pair.create("android:normalScreens", FLAG_SUPPORTS_NORMAL_SCREENS),
84          Pair.create("android:anyDensity", FLAG_SUPPORTS_SCREEN_DENSITIES),
85          Pair.create("android:smallScreens", FLAG_SUPPORTS_SMALL_SCREENS),
86          Pair.create("android:testOnly", FLAG_TEST_ONLY),
87          Pair.create("android:vmSafeMode", FLAG_VM_SAFE_MODE));
88  private static final List<Pair<String, Integer>> CONFIG_OPTIONS =
89      asList(
90          Pair.create("mcc", ActivityInfo.CONFIG_MCC),
91          Pair.create("mnc", ActivityInfo.CONFIG_MNC),
92          Pair.create("locale", ActivityInfo.CONFIG_LOCALE),
93          Pair.create("touchscreen", ActivityInfo.CONFIG_TOUCHSCREEN),
94          Pair.create("keyboard", ActivityInfo.CONFIG_KEYBOARD),
95          Pair.create("keyboardHidden", ActivityInfo.CONFIG_KEYBOARD_HIDDEN),
96          Pair.create("navigation", ActivityInfo.CONFIG_NAVIGATION),
97          Pair.create("screenLayout", ActivityInfo.CONFIG_SCREEN_LAYOUT),
98          Pair.create("fontScale", ActivityInfo.CONFIG_FONT_SCALE),
99          Pair.create("uiMode", ActivityInfo.CONFIG_UI_MODE),
100          Pair.create("orientation", ActivityInfo.CONFIG_ORIENTATION),
101          Pair.create("screenSize", ActivityInfo.CONFIG_SCREEN_SIZE),
102          Pair.create("smallestScreenSize", ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE));
103
104  public static Package createPackage(AndroidManifest androidManifest) {
105
106    Package pkg = new Package(androidManifest.getPackageName());
107
108    pkg.mVersionName = androidManifest.getVersionName();
109    pkg.mVersionCode = androidManifest.getVersionCode();
110
111    Map<String, PermissionItemData> permissionItemData = androidManifest.getPermissions();
112    for (PermissionItemData itemData : permissionItemData.values()) {
113      Permission permission = new Permission(pkg, createPermissionInfo(pkg, itemData));
114      permission.metaData = permission.info.metaData;
115      pkg.permissions.add(permission);
116    }
117
118    Map<String, PermissionGroupItemData> permissionGroupItemData = androidManifest.getPermissionGroups();
119    for (PermissionGroupItemData itemData : permissionGroupItemData.values()) {
120      PermissionGroup permissionGroup = new PermissionGroup(pkg, createPermissionGroupInfo(pkg, itemData));
121      permissionGroup.metaData = permissionGroup.info.metaData;
122      pkg.permissionGroups.add(permissionGroup);
123    }
124
125    pkg.requestedPermissions.addAll(androidManifest.getUsedPermissions());
126    if (RuntimeEnvironment.getApiLevel() < VERSION_CODES.M) {
127      List<Boolean> permissionsRequired =
128          ReflectionHelpers.getField(pkg, "requestedPermissionsRequired");
129      permissionsRequired.addAll(buildBooleanList(pkg.requestedPermissions.size(), true));
130    }
131
132    pkg.applicationInfo.flags = decodeFlags(androidManifest.getApplicationAttributes());
133    pkg.applicationInfo.targetSdkVersion = androidManifest.getTargetSdkVersion();
134    pkg.applicationInfo.packageName = androidManifest.getPackageName();
135    pkg.applicationInfo.processName = androidManifest.getProcessName();
136    if (!Strings.isNullOrEmpty(androidManifest.getApplicationName())) {
137      pkg.applicationInfo.className =
138          buildClassName(pkg.applicationInfo.packageName, androidManifest.getApplicationName());
139      if (RuntimeEnvironment.getApiLevel() >= VERSION_CODES.N_MR1) {
140        pkg.applicationInfo.name = pkg.applicationInfo.className;
141      }
142    }
143    pkg.applicationInfo.metaData = metaDataToBundle(androidManifest.getApplicationMetaData());
144    pkg.applicationInfo.uid = Process.myUid();
145    if (androidManifest.getThemeRef() != null) {
146      pkg.applicationInfo.theme =
147          RuntimeEnvironment.getAppResourceTable()
148              .getResourceId(
149                  ResName.qualifyResName(
150                      androidManifest.getThemeRef().replace("@", ""), pkg.packageName, "style"));
151    }
152
153    int labelRes = 0;
154    if (androidManifest.getLabelRef() != null) {
155      String fullyQualifiedName =
156          ResName.qualifyResName(androidManifest.getLabelRef(), androidManifest.getPackageName());
157      Integer id =
158          fullyQualifiedName == null
159              ? null
160              : RuntimeEnvironment.getAppResourceTable()
161                  .getResourceId(new ResName(fullyQualifiedName));
162      labelRes = id != null ? id : 0;
163    }
164
165    pkg.applicationInfo.labelRes = labelRes;
166    String labelRef = androidManifest.getLabelRef();
167    if (labelRef != null && !labelRef.startsWith("@")) {
168      pkg.applicationInfo.nonLocalizedLabel = labelRef;
169    }
170
171    Map<String, ActivityData> activityDatas = androidManifest.getActivityDatas();
172    for (ActivityData data : activityDatas.values()) {
173      ActivityInfo activityInfo = new ActivityInfo();
174      activityInfo.name = buildClassName(pkg.packageName, data.getName());
175      activityInfo.packageName = pkg.packageName;
176      activityInfo.configChanges = getConfigChanges(data);
177      activityInfo.parentActivityName = data.getParentActivityName();
178      activityInfo.metaData = metaDataToBundle(data.getMetaData().getValueMap());
179      activityInfo.applicationInfo = pkg.applicationInfo;
180      activityInfo.targetActivity = data.getTargetActivityName();
181      activityInfo.exported = data.isExported();
182      activityInfo.permission = data.getPermission();
183      String themeRef;
184
185      // Based on ShadowActivity
186      if (data.getThemeRef() != null) {
187        themeRef = data.getThemeRef();
188      } else {
189        themeRef = androidManifest.getThemeRef();
190      }
191      if (themeRef != null) {
192        activityInfo.theme =
193            RuntimeEnvironment.getAppResourceTable()
194                .getResourceId(
195                    ResName.qualifyResName(themeRef.replace("@", ""), pkg.packageName, "style"));
196      }
197
198      if (data.getLabel() != null) {
199        activityInfo.labelRes =
200            RuntimeEnvironment.getAppResourceTable()
201                .getResourceId(
202                    ResName.qualifyResName(
203                        data.getLabel().replace("@", ""), pkg.packageName, "string"));
204        if (activityInfo.labelRes == 0) {
205          activityInfo.nonLocalizedLabel = data.getLabel();
206        }
207      }
208
209      Activity activity = createActivity(pkg, activityInfo);
210      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
211        ActivityIntentInfo outInfo = new ActivityIntentInfo(activity);
212        populateIntentInfo(intentFilterData, outInfo);
213        activity.intents.add(outInfo);
214      }
215      pkg.activities.add(activity);
216    }
217
218    for (ContentProviderData data : androidManifest.getContentProviders()) {
219      ProviderInfo info = new ProviderInfo();
220      populateComponentInfo(info, pkg, data);
221      info.authority = data.getAuthorities();
222
223      List<PathPermission> permissions = new ArrayList<>();
224      for (PathPermissionData permissionData : data.getPathPermissionDatas()) {
225        permissions.add(createPathPermission(permissionData));
226      }
227      info.pathPermissions = permissions.toArray(new PathPermission[permissions.size()]);
228      info.readPermission = data.getReadPermission();
229      info.writePermission = data.getWritePermission();
230      info.grantUriPermissions = data.getGrantUriPermissions();
231      pkg.providers.add(createProvider(pkg, info));
232    }
233
234    for (BroadcastReceiverData data : androidManifest.getBroadcastReceivers()) {
235      ActivityInfo info = new ActivityInfo();
236      populateComponentInfo(info, pkg, data);
237      info.permission = data.getPermission();
238      info.exported = data.isExported();
239
240      Activity receiver = createActivity(pkg, info);
241      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
242        ActivityIntentInfo outInfo = new ActivityIntentInfo(receiver);
243        populateIntentInfo(intentFilterData, outInfo);
244        receiver.intents.add(outInfo);
245      }
246      pkg.receivers.add(receiver);
247    }
248
249    for (ServiceData data : androidManifest.getServices()) {
250      ServiceInfo info = new ServiceInfo();
251      populateComponentInfo(info, pkg, data);
252      info.permission = data.getPermission();
253      info.exported = data.isExported();
254
255      Service service = createService(pkg, info);
256      for (IntentFilterData intentFilterData : data.getIntentFilters()) {
257        ServiceIntentInfo outInfo = new ServiceIntentInfo(service);
258        populateIntentInfo(intentFilterData, outInfo);
259        service.intents.add(outInfo);
260      }
261      pkg.services.add(service);
262    }
263
264    return pkg;
265  }
266
267  private static PathPermission createPathPermission(PathPermissionData data) {
268    if (!Strings.isNullOrEmpty(data.pathPattern)) {
269      return new PathPermission(
270          data.pathPattern, PATTERN_SIMPLE_GLOB, data.readPermission, data.writePermission);
271    } else if (!Strings.isNullOrEmpty(data.path)) {
272      return new PathPermission(
273          data.path, PATTERN_LITERAL, data.readPermission, data.writePermission);
274    } else if (!Strings.isNullOrEmpty(data.pathPrefix)) {
275      return new PathPermission(
276          data.pathPrefix, PATTERN_PREFIX, data.readPermission, data.writePermission);
277    } else {
278      throw new IllegalStateException("Permission without type");
279    }
280  }
281
282  private static void populateComponentInfo(
283      ComponentInfo outInfo, Package owner, PackageItemData itemData) {
284    populatePackageItemInfo(outInfo, owner, itemData);
285    outInfo.applicationInfo = owner.applicationInfo;
286  }
287
288  private static void populatePackageItemInfo(
289      PackageItemInfo outInfo, Package owner, PackageItemData itemData) {
290    outInfo.name = buildClassName(owner.packageName, itemData.getName());
291    outInfo.packageName = owner.packageName;
292    outInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
293  }
294
295  private static List<Boolean> buildBooleanList(int size, boolean defaultVal) {
296    Boolean[] barray = new Boolean[size];
297    Arrays.fill(barray, defaultVal);
298    return Arrays.asList(barray);
299  }
300
301  private static PackageParser.Provider createProvider(Package pkg, ProviderInfo info) {
302    PackageParser.Provider provider =
303        ReflectionHelpers.callConstructor(PackageParser.Provider.class);
304    populateComponent(pkg, info, provider);
305    return provider;
306  }
307
308  private static Activity createActivity(Package pkg, ActivityInfo activityInfo) {
309    Activity activity = ReflectionHelpers.callConstructor(Activity.class);
310    populateComponent(pkg, activityInfo, activity);
311    return activity;
312  }
313
314  private static Service createService(Package pkg, ServiceInfo info) {
315    PackageParser.Service service = ReflectionHelpers.callConstructor(PackageParser.Service.class);
316    populateComponent(pkg, info, service);
317    return service;
318  }
319
320  private static void populateComponent(
321      Package pkg, ComponentInfo info, PackageParser.Component component) {
322    ReflectionHelpers.setField(component, "info", info);
323    ReflectionHelpers.setField(component, "intents", new ArrayList<>());
324    ReflectionHelpers.setField(component, "owner", pkg);
325    ReflectionHelpers.setField(component, "className", info.name);
326  }
327
328  private static void populateIntentInfo(IntentFilterData intentFilterData, IntentInfo outInfo) {
329    for (String action : intentFilterData.getActions()) {
330      outInfo.addAction(action);
331    }
332    for (String category : intentFilterData.getCategories()) {
333      outInfo.addCategory(category);
334    }
335    for (DataAuthority dataAuthority : intentFilterData.getAuthorities()) {
336      outInfo.addDataAuthority(dataAuthority.getHost(), dataAuthority.getPort());
337    }
338    for (String mimeType : intentFilterData.getMimeTypes()) {
339      try {
340        outInfo.addDataType(mimeType);
341      } catch (MalformedMimeTypeException e) {
342        throw new RuntimeException(e);
343      }
344    }
345    for (String scheme : intentFilterData.getSchemes()) {
346      outInfo.addDataScheme(scheme);
347    }
348    for (String pathPattern : intentFilterData.getPathPatterns()) {
349      outInfo.addDataPath(pathPattern, PATTERN_SIMPLE_GLOB);
350    }
351    for (String pathPattern : intentFilterData.getPathPrefixes()) {
352      outInfo.addDataPath(pathPattern, PATTERN_PREFIX);
353    }
354    for (String pathPattern : intentFilterData.getPaths()) {
355      outInfo.addDataPath(pathPattern, PATTERN_LITERAL);
356    }
357  }
358
359  private static int getConfigChanges(ActivityData activityData) {
360    String s = activityData.getConfigChanges();
361
362    int res = 0;
363
364    // quick sanity check.
365    if (s == null || "".equals(s)) {
366      return res;
367    }
368
369    String[] pieces = s.split("\\|");
370
371    for (String s1 : pieces) {
372      s1 = s1.trim();
373
374      for (Pair<String, Integer> pair : CONFIG_OPTIONS) {
375        if (s1.equals(pair.first)) {
376          res |= pair.second;
377          break;
378        }
379      }
380    }
381
382    // Matches platform behavior
383    if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O) {
384      res |= ActivityInfo.CONFIG_MNC;
385      res |= ActivityInfo.CONFIG_MCC;
386    }
387
388    return res;
389  }
390
391  private static int decodeFlags(Map<String, String> applicationAttributes) {
392    int applicationFlags = 0;
393    for (Pair<String, Integer> pair : APPLICATION_FLAGS) {
394      if ("true".equals(applicationAttributes.get(pair.first))) {
395        applicationFlags |= pair.second;
396      }
397    }
398    return applicationFlags;
399  }
400
401  private static PermissionInfo createPermissionInfo(Package owner, PermissionItemData itemData) {
402    PermissionInfo permissionInfo = new PermissionInfo();
403    populatePackageItemInfo(permissionInfo, owner, itemData);
404
405    permissionInfo.group = itemData.getPermissionGroup();
406    permissionInfo.protectionLevel = decodeProtectionLevel(itemData.getProtectionLevel());
407    permissionInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
408
409    String descriptionRef = itemData.getDescription();
410    if (descriptionRef != null) {
411      ResName descResName =
412          AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
413      permissionInfo.descriptionRes =
414          RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
415    }
416
417    String labelRefOrString = itemData.getLabel();
418    if (labelRefOrString != null) {
419      if (AttributeResource.isResourceReference(labelRefOrString)) {
420        ResName labelResName =
421            AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
422        permissionInfo.labelRes =
423            RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
424      } else {
425        permissionInfo.nonLocalizedLabel = labelRefOrString;
426      }
427    }
428
429    return permissionInfo;
430  }
431
432  private static PermissionGroupInfo createPermissionGroupInfo(Package owner,
433      PermissionGroupItemData itemData) {
434    PermissionGroupInfo permissionGroupInfo = new PermissionGroupInfo();
435    populatePackageItemInfo(permissionGroupInfo, owner, itemData);
436
437    permissionGroupInfo.metaData = metaDataToBundle(itemData.getMetaData().getValueMap());
438
439    String descriptionRef = itemData.getDescription();
440    if (descriptionRef != null) {
441      ResName descResName =
442          AttributeResource.getResourceReference(descriptionRef, owner.packageName, "string");
443      permissionGroupInfo.descriptionRes =
444          RuntimeEnvironment.getAppResourceTable().getResourceId(descResName);
445    }
446
447    String labelRefOrString = itemData.getLabel();
448    if (labelRefOrString != null) {
449      if (AttributeResource.isResourceReference(labelRefOrString)) {
450        ResName labelResName =
451            AttributeResource.getResourceReference(labelRefOrString, owner.packageName, "string");
452        permissionGroupInfo.labelRes =
453            RuntimeEnvironment.getAppResourceTable().getResourceId(labelResName);
454      } else {
455        permissionGroupInfo.nonLocalizedLabel = labelRefOrString;
456      }
457    }
458
459    return permissionGroupInfo;
460  }
461
462  private static int decodeProtectionLevel(String protectionLevel) {
463    if (protectionLevel == null) {
464      return PermissionInfo.PROTECTION_NORMAL;
465    }
466
467    switch (protectionLevel) {
468      case "normal":
469        return PermissionInfo.PROTECTION_NORMAL;
470      case "dangerous":
471        return PermissionInfo.PROTECTION_DANGEROUS;
472      case "signature":
473        return PermissionInfo.PROTECTION_SIGNATURE;
474      case "signatureOrSystem":
475        return PermissionInfo.PROTECTION_SIGNATURE_OR_SYSTEM;
476      default:
477        throw new IllegalArgumentException("unknown protection level " + protectionLevel);
478    }
479  }
480
481  /**
482   * Goes through the meta data and puts each value in to a bundle as the correct type.
483   *
484   * <p>Note that this will convert resource identifiers specified via the value attribute as well.
485   *
486   * @param meta Meta data to put in to a bundle
487   * @return bundle containing the meta data
488   */
489  private static Bundle metaDataToBundle(Map<String, Object> meta) {
490    if (meta.size() == 0) {
491      return null;
492    }
493
494    Bundle bundle = new Bundle();
495
496    for (Map.Entry<String, Object> entry : meta.entrySet()) {
497      if (Boolean.class.isInstance(entry.getValue())) {
498        bundle.putBoolean(entry.getKey(), (Boolean) entry.getValue());
499      } else if (Float.class.isInstance(entry.getValue())) {
500        bundle.putFloat(entry.getKey(), (Float) entry.getValue());
501      } else if (Integer.class.isInstance(entry.getValue())) {
502        bundle.putInt(entry.getKey(), (Integer) entry.getValue());
503      } else {
504        bundle.putString(entry.getKey(), entry.getValue().toString());
505      }
506    }
507    return bundle;
508  }
509
510  private static String buildClassName(String pkg, String cls) {
511    if (Strings.isNullOrEmpty(cls)) {
512      throw new IllegalArgumentException("Empty class name in package " + pkg);
513    }
514    char c = cls.charAt(0);
515    if (c == '.') {
516      return (pkg + cls).intern();
517    }
518    if (cls.indexOf('.') < 0) {
519      StringBuilder b = new StringBuilder(pkg);
520      b.append('.');
521      b.append(cls);
522      return b.toString();
523    }
524    return cls;
525    // TODO: consider reenabling this for stricter platform-complaint checking
526    // if (c >= 'a' && c <= 'z') {
527    // return cls;
528    // }
529    // throw new IllegalArgumentException("Bad class name " + cls + " in package " + pkg);
530  }
531}
532