Home | History | Annotate | Download | only in shadows
      1 package org.robolectric.shadows;
      2 
      3 import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
      4 import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
      5 import static android.content.pm.PackageParser.PARSE_COLLECT_CERTIFICATES;
      6 import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
      7 import static org.robolectric.shadow.api.Shadow.directlyOn;
      8 import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
      9 
     10 import android.content.pm.PackageManager;
     11 import android.content.pm.PackageParser;
     12 import android.content.pm.PackageParser.Package;
     13 import android.content.pm.PackageParser.ParseFlags;
     14 import android.content.pm.PackageParser.SigningDetails;
     15 import android.content.pm.Signature;
     16 import android.content.res.AssetManager;
     17 import android.content.res.Resources;
     18 import android.content.res.TypedArray;
     19 import android.content.res.XmlResourceParser;
     20 import android.os.Build;
     21 import android.os.Bundle;
     22 import android.os.Trace;
     23 import android.util.AttributeSet;
     24 import android.util.DisplayMetrics;
     25 import android.util.Slog;
     26 import android.util.TypedValue;
     27 import com.android.internal.util.XmlUtils;
     28 import java.io.File;
     29 import java.io.IOException;
     30 import java.security.cert.Certificate;
     31 import libcore.io.IoUtils;
     32 import org.robolectric.RuntimeEnvironment;
     33 import org.robolectric.annotation.Implementation;
     34 import org.robolectric.annotation.Implements;
     35 import org.robolectric.annotation.RealObject;
     36 import org.robolectric.res.FsFile;
     37 import org.robolectric.util.ReflectionHelpers;
     38 import org.xmlpull.v1.XmlPullParser;
     39 import org.xmlpull.v1.XmlPullParserException;
     40 
     41 /**
     42  * Shadow for {@link PackageParser}
     43  */
     44 @Implements(value = PackageParser.class, isInAndroidSdk = false)
     45 public class ShadowPackageParser {
     46 
     47   private static final String TAG = "ShadowPackageParser";
     48 
     49   private int mParseError;
     50   private String mArchiveSourcePath;
     51 
     52   @RealObject PackageParser realObject;
     53   private static String MANIFEST_FILE;
     54 
     55   /** Parses an AndroidManifest.xml file using the framework PackageParser. */
     56   public static Package callParsePackage(FsFile manifestFile) {
     57     MANIFEST_FILE = manifestFile.getPath();
     58     PackageParser packageParser = new PackageParser();
     59 
     60     int flags = PackageParser.PARSE_IGNORE_PROCESSES;
     61     try {
     62       if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.LOLLIPOP) {
     63         return packageParser.parsePackage(new File(manifestFile.getPath()), flags);
     64       } else { // JB -> KK
     65         return ReflectionHelpers.callInstanceMethod(
     66             PackageParser.class,
     67             packageParser,
     68             "parsePackage",
     69             from(File.class, new File(manifestFile.getPath())),
     70             from(String.class, manifestFile.getPath()),
     71             from(DisplayMetrics.class, new DisplayMetrics()),
     72             from(int.class, flags));
     73       }
     74     } catch (Exception e) {
     75       throw new RuntimeException(e);
     76     }
     77   }
     78 
     79   /**
     80    * We only need to implement this method because the framework expects the AndroidManifest.xml
     81    * file to exist in the root of the apk. We can't set the field statically since its final and
     82    * therefore inlined by the compiler. We should be able to remove this method and others below for
     83    * other framework versions if we move to a model that mirrors what the framework expects, e.g:
     84    * the AndroidManifest.xml at the root of the resources.ap_
     85    */
     86   @Implementation(minSdk = Build.VERSION_CODES.JELLY_BEAN, maxSdk = Build.VERSION_CODES.KITKAT)
     87   public Package parsePackage(
     88       File sourceFile, String destCodePath, DisplayMetrics metrics, int flags) {
     89     mParseError = PackageManager.INSTALL_SUCCEEDED;
     90 
     91     XmlResourceParser parser = null;
     92     AssetManager assmgr = null;
     93     Resources res = null;
     94     boolean assetError = true;
     95     try {
     96       assmgr = new AssetManager();
     97       int cookie = mArchiveSourcePath != null ? assmgr.addAssetPath(mArchiveSourcePath) : 1;
     98       if (cookie != 0) {
     99         res = new Resources(assmgr, metrics, null);
    100         parser = assmgr.openXmlResourceParser(cookie, MANIFEST_FILE);
    101         assetError = false;
    102       } else {
    103         Slog.w(TAG, "Failed adding asset path:" + mArchiveSourcePath);
    104       }
    105     } catch (Exception e) {
    106       Slog.w(TAG, "Unable to read AndroidManifest.xml of " + mArchiveSourcePath, e);
    107     }
    108     if (assetError) {
    109       if (assmgr != null) {
    110         assmgr.close();
    111       }
    112       mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
    113       throw new RuntimeException("Failed to parse Manifest");
    114     }
    115     String[] errorText = new String[1];
    116     Package pkg = null;
    117     try {
    118       pkg =
    119           directlyOn(
    120               realObject,
    121               PackageParser.class,
    122               "parsePackage",
    123               ReflectionHelpers.ClassParameter.from(Resources.class, res),
    124               ReflectionHelpers.ClassParameter.from(XmlResourceParser.class, parser),
    125               ReflectionHelpers.ClassParameter.from(int.class, flags),
    126               ReflectionHelpers.ClassParameter.from(String[].class, errorText));
    127     } catch (Exception e) {
    128       mParseError = INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
    129       throw new RuntimeException("Failed to parse Manifest", e);
    130     }
    131 
    132     if (pkg == null) {
    133       parser.close();
    134       assmgr.close();
    135       throw new RuntimeException("Failed to parse Manifest" + errorText[0]);
    136     }
    137 
    138     parser.close();
    139     assmgr.close();
    140 
    141     if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.O_MR1) {
    142       ReflectionHelpers.setField(pkg, "mSignatures", null);
    143     } else {
    144       // BEGIN-INTERNAL
    145       ReflectionHelpers.setField(pkg, "mSigningDetails", SigningDetails.UNKNOWN);
    146       // END-INTERNAL
    147     }
    148 
    149     return pkg;
    150   }
    151 
    152   @Implementation
    153   public Bundle parseMetaData(
    154       Resources res, XmlPullParser parser, AttributeSet attrs, Bundle data, String[] outError)
    155       throws XmlPullParserException, IOException {
    156 
    157     TypedArray sa =
    158         res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifestMetaData);
    159 
    160     if (data == null) {
    161       data = new Bundle();
    162     }
    163 
    164     String name =
    165         sa.getNonConfigurationString(
    166             com.android.internal.R.styleable.AndroidManifestMetaData_name, 0);
    167     if (name == null) {
    168       outError[0] = "<meta-data> requires an android:name attribute";
    169       sa.recycle();
    170       return null;
    171     }
    172 
    173     name = name.intern();
    174 
    175     TypedValue v = sa.peekValue(com.android.internal.R.styleable.AndroidManifestMetaData_resource);
    176     if (v != null && v.resourceId != 0) {
    177       // Slog.i(TAG, "Meta data ref " + name + ": " + v);
    178       data.putInt(name, v.resourceId);
    179     } else {
    180       v = sa.peekValue(com.android.internal.R.styleable.AndroidManifestMetaData_value);
    181       // Slog.i(TAG, "Meta data " + name + ": " + v);
    182       if (v != null) {
    183         if (v.type == TypedValue.TYPE_STRING) {
    184           CharSequence cs = v.coerceToString();
    185           data.putString(name, cs != null ? cs.toString().intern() : null);
    186         } else if (v.type == TypedValue.TYPE_INT_BOOLEAN) {
    187           data.putBoolean(name, v.data != 0);
    188         } else if (v.type >= TypedValue.TYPE_FIRST_INT && v.type <= TypedValue.TYPE_LAST_INT) {
    189           data.putInt(name, v.data);
    190         } else if (v.type == TypedValue.TYPE_FLOAT) {
    191           data.putFloat(name, v.getFloat());
    192         } else {
    193           if (true) {
    194             Slog.w(
    195                 TAG,
    196                 "<meta-data> only supports string, integer, float, color, boolean, and "
    197                     + "resource reference types: "
    198                     + parser.getName()
    199                     + " at "
    200                     + mArchiveSourcePath
    201                     + " "
    202                     + parser.getPositionDescription());
    203           } else {
    204             outError[0] =
    205                 "<meta-data> only supports string, integer, float, color, boolean, and resource "
    206                 + "reference types";
    207             data = null;
    208           }
    209         }
    210       } else {
    211         outError[0] = "<meta-data> requires an android:value or android:resource attribute";
    212         data = null;
    213       }
    214     }
    215 
    216     sa.recycle();
    217 
    218     XmlUtils.skipCurrentTag(parser);
    219 
    220     return data;
    221   }
    222 
    223   /*
    224    * Only required because framework expects AndroidManifest.xml at the root of the resources file.
    225    * See comment on
    226    * {@link #parsePackage(java.io.File, java.lang.String, android.util.DisplayMetrics, int)}
    227    */
    228   @Implementation(minSdk = Build.VERSION_CODES.LOLLIPOP)
    229   public Package parseBaseApk(File apkFile, AssetManager assets, int flags) {
    230     final String apkPath = apkFile.getAbsolutePath();
    231 
    232     mParseError = PackageManager.INSTALL_SUCCEEDED;
    233     mArchiveSourcePath = apkFile.getAbsolutePath();
    234 
    235     final int cookie = 0;
    236 
    237     Resources res = null;
    238     XmlResourceParser parser = null;
    239     try {
    240       res = new Resources(assets, new DisplayMetrics(), null);
    241 
    242       parser = assets.openXmlResourceParser(cookie, MANIFEST_FILE);
    243       final String[] outError = new String[1];
    244       final Package pkg;
    245       if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O) {
    246         pkg =
    247             directlyOn(
    248                 realObject,
    249                 PackageParser.class,
    250                 "parseBaseApk",
    251                 ReflectionHelpers.ClassParameter.from(String.class, apkFile.getAbsolutePath()),
    252                 ReflectionHelpers.ClassParameter.from(Resources.class, res),
    253                 ReflectionHelpers.ClassParameter.from(XmlResourceParser.class, parser),
    254                 ReflectionHelpers.ClassParameter.from(int.class, flags),
    255                 ReflectionHelpers.ClassParameter.from(String[].class, outError));
    256       } else {
    257         pkg =
    258             directlyOn(
    259                 realObject,
    260                 PackageParser.class,
    261                 "parseBaseApk",
    262                 ReflectionHelpers.ClassParameter.from(Resources.class, res),
    263                 ReflectionHelpers.ClassParameter.from(XmlResourceParser.class, parser),
    264                 ReflectionHelpers.ClassParameter.from(int.class, flags),
    265                 ReflectionHelpers.ClassParameter.from(String[].class, outError));
    266       }
    267 
    268       if (pkg == null) {
    269         throw new Exception(
    270             "Parse error at " + parser.getPositionDescription() + "): " + outError[0]);
    271       }
    272 
    273       pkg.baseCodePath = apkPath;
    274 
    275       if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.O_MR1) {
    276         ReflectionHelpers.setField(pkg, "mSignatures", null);
    277       } else {
    278         // BEGIN-INTERNAL
    279         ReflectionHelpers.setField(pkg, "mSigningDetails", SigningDetails.UNKNOWN);
    280         // END-INTERNAL
    281       }
    282 
    283       return pkg;
    284     } catch (Exception e) {
    285       throw new RuntimeException(
    286           "Failed to read manifest from "
    287               + apkPath
    288               + "Error code: "
    289               + INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
    290           e);
    291     } finally {
    292       IoUtils.closeQuietly(parser);
    293     }
    294   }
    295 
    296   /*
    297    * Only required because framework expects AndroidManifest.xml at the root of the resources file.
    298    * See comment on
    299    * {@link #parsePackage(java.io.File, java.lang.String, android.util.DisplayMetrics, int)}
    300    */
    301   @Implementation(minSdk = Build.VERSION_CODES.N)
    302   public static Object parseApkLite(File apkFile, @ParseFlags int flags) {
    303     final String apkPath = apkFile.getAbsolutePath();
    304 
    305     AssetManager assets = null;
    306     XmlResourceParser parser = null;
    307     try {
    308       assets = new AssetManager();
    309 
    310       int cookie = assets.addAssetPath(apkPath);
    311       if (cookie == 0) {
    312         throw new RuntimeException(
    313             "Failed to parse " + apkPath + "Error code: " + INSTALL_PARSE_FAILED_NOT_APK);
    314       }
    315 
    316       final DisplayMetrics metrics = new DisplayMetrics();
    317       metrics.setToDefaults();
    318 
    319       final Resources res = new Resources(assets, metrics, null);
    320       parser = assets.openXmlResourceParser(cookie, MANIFEST_FILE);
    321 
    322       Signature[] signatures = null;
    323       Certificate[][] certificates = null;
    324       Object signatureDetails = null;
    325       if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) {
    326         // TODO: factor signature related items out of Package object
    327         final Package tempPkg = ReflectionHelpers.newInstance(Package.class);
    328 
    329         try {
    330           directlyOn(
    331               PackageParser.class,
    332               "collectCertificates",
    333               ReflectionHelpers.ClassParameter.from(Package.class, tempPkg),
    334               ReflectionHelpers.ClassParameter.from(File.class, apkFile),
    335               ReflectionHelpers.ClassParameter.from(int.class, 0 /*parseFlags*/));
    336         } finally {
    337           Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    338         }
    339 
    340         if (RuntimeEnvironment.getApiLevel() <= Build.VERSION_CODES.O_MR1) {
    341           signatures = ReflectionHelpers.getField(tempPkg, "mSignatures");
    342           certificates = ReflectionHelpers.getField(tempPkg, "mCertificates");
    343         } else {
    344           // BEGIN-INTERNAL
    345           signatureDetails = tempPkg.mSigningDetails;
    346           // END-INTERNAL
    347         }
    348       }
    349 
    350       final AttributeSet attrs = parser;
    351 
    352       // BEGIN-INTERNAL
    353       if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.P) {
    354         return directlyOn(
    355             PackageParser.class,
    356             "parseApkLite",
    357             ReflectionHelpers.ClassParameter.from(String.class, apkPath),
    358             ReflectionHelpers.ClassParameter.from(XmlPullParser.class, parser),
    359             ReflectionHelpers.ClassParameter.from(AttributeSet.class, attrs),
    360             ReflectionHelpers.ClassParameter.from(SigningDetails.class, signatureDetails));
    361       } else
    362       // END-INTERNAL
    363       if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O_MR1) {
    364         return directlyOn(
    365             PackageParser.class,
    366             "parseApkLite",
    367             ReflectionHelpers.ClassParameter.from(String.class, apkPath),
    368             ReflectionHelpers.ClassParameter.from(XmlPullParser.class, parser),
    369             ReflectionHelpers.ClassParameter.from(AttributeSet.class, attrs),
    370             ReflectionHelpers.ClassParameter.from(Signature[].class, signatures),
    371             ReflectionHelpers.ClassParameter.from(Certificate[][].class, certificates));
    372       } else if (RuntimeEnvironment.getApiLevel() >= Build.VERSION_CODES.O) {
    373         return directlyOn(
    374             PackageParser.class,
    375             "parseApkLite",
    376             ReflectionHelpers.ClassParameter.from(String.class, apkPath),
    377             ReflectionHelpers.ClassParameter.from(XmlPullParser.class, parser),
    378             ReflectionHelpers.ClassParameter.from(AttributeSet.class, attrs),
    379             ReflectionHelpers.ClassParameter.from(int.class, flags),
    380             ReflectionHelpers.ClassParameter.from(Signature[].class, signatures),
    381             ReflectionHelpers.ClassParameter.from(Certificate[][].class, certificates));
    382       } else {
    383         return directlyOn(
    384             PackageParser.class,
    385             "parseApkLite",
    386             ReflectionHelpers.ClassParameter.from(String.class, apkPath),
    387             ReflectionHelpers.ClassParameter.from(Resources.class, res),
    388             ReflectionHelpers.ClassParameter.from(XmlPullParser.class, parser),
    389             ReflectionHelpers.ClassParameter.from(AttributeSet.class, attrs),
    390             ReflectionHelpers.ClassParameter.from(int.class, flags),
    391             ReflectionHelpers.ClassParameter.from(Signature[].class, signatures),
    392             ReflectionHelpers.ClassParameter.from(Certificate[][].class, certificates));
    393       }
    394 
    395     } catch (Exception e) {
    396       throw new RuntimeException(
    397           "Failed to parse "
    398               + apkPath
    399               + "Error code: "
    400               + INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION);
    401     } finally {
    402       IoUtils.closeQuietly(parser);
    403       IoUtils.closeQuietly(assets);
    404     }
    405   }
    406 }
    407