Home | History | Annotate | Download | only in annotation
      1 package org.robolectric.annotation;
      2 
      3 import android.app.Application;
      4 import android.content.pm.PackageInfo;
      5 import java.lang.annotation.Annotation;
      6 import java.lang.annotation.Documented;
      7 import java.lang.annotation.ElementType;
      8 import java.lang.annotation.Inherited;
      9 import java.lang.annotation.Retention;
     10 import java.lang.annotation.RetentionPolicy;
     11 import java.lang.annotation.Target;
     12 import java.util.ArrayList;
     13 import java.util.Arrays;
     14 import java.util.HashSet;
     15 import java.util.List;
     16 import java.util.Properties;
     17 import java.util.Set;
     18 import javax.annotation.Nonnull;
     19 
     20 /**
     21  * Configuration settings that can be used on a per-class or per-test basis.
     22  */
     23 @Documented
     24 @Inherited
     25 @Retention(RetentionPolicy.RUNTIME)
     26 @Target({ElementType.TYPE, ElementType.METHOD})
     27 @SuppressWarnings(value = {"BadAnnotationImplementation", "ImmutableAnnotationChecker"})
     28 public @interface Config {
     29   /**
     30    * TODO(vnayar): Create named constants for default values instead of magic numbers.
     31    * Array named constants must be avoided in order to dodge a JDK 1.7 bug.
     32    *   error: annotation Config is missing value for the attribute <clinit>
     33    * See <a href="https://bugs.openjdk.java.net/browse/JDK-8013485">JDK-8013485</a>.
     34    */
     35   String NONE = "--none";
     36   String DEFAULT_VALUE_STRING = "--default";
     37   int DEFAULT_VALUE_INT = -1;
     38 
     39   String DEFAULT_MANIFEST_NAME = "AndroidManifest.xml";
     40   Class<? extends Application> DEFAULT_APPLICATION = DefaultApplication.class;
     41   String DEFAULT_PACKAGE_NAME = "";
     42   String DEFAULT_QUALIFIERS = "";
     43   String DEFAULT_RES_FOLDER = "res";
     44   String DEFAULT_ASSET_FOLDER = "assets";
     45 
     46   int ALL_SDKS = -2;
     47   int TARGET_SDK = -3;
     48   int OLDEST_SDK = -4;
     49   int NEWEST_SDK = -5;
     50 
     51   /**
     52    * The Android SDK level to emulate. This value will also be set as Build.VERSION.SDK_INT.
     53    */
     54   int[] sdk() default {};  // DEFAULT_SDK
     55 
     56   /**
     57    * The minimum Android SDK level to emulate when running tests on multiple API versions.
     58    */
     59   int minSdk() default -1;
     60 
     61   /**
     62    * The maximum Android SDK level to emulate when running tests on multiple API versions.
     63    */
     64   int maxSdk() default -1;
     65 
     66   /**
     67    * The Android manifest file to load; Robolectric will look relative to the current directory.
     68    * Resources and assets will be loaded relative to the manifest.
     69    *
     70    * If not specified, Robolectric defaults to {@code AndroidManifest.xml}.
     71    *
     72    * If your project has no manifest or resources, use {@link Config#NONE}.
     73    * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test
     74    * please migrate to the preferred way to configure
     75    * builds http://robolectric.org/getting-started/
     76    *
     77    * @return The Android manifest file to load.
     78    */
     79   @Deprecated
     80   String manifest() default DEFAULT_VALUE_STRING;
     81 
     82   /**
     83    * The {@link android.app.Application} class to use in the test, this takes precedence over any application
     84    * specified in the AndroidManifest.xml.
     85    *
     86    * @return The {@link android.app.Application} class to use in the test.
     87    */
     88   Class<? extends Application> application() default DefaultApplication.class;  // DEFAULT_APPLICATION
     89 
     90   /**
     91    * Java package name where the "R.class" file is located. This only needs to be specified if you
     92    * define an {@code applicationId} associated with {@code productFlavors} or specify {@code
     93    * applicationIdSuffix} in your build.gradle.
     94    *
     95    * <p>If not specified, Robolectric defaults to the {@code applicationId}.
     96    *
     97    * @return The java package name for R.class.
     98    * @deprecated To change your package name please override the applicationId in your build system.
     99    *     Changing package name here is broken as the package name will no longer match the package
    100    *     name encoded in the arsc resources file. If you are looking to simulate another application
    101    *     you can create another applications Context using {@link
    102    *     android.content.Context#createPackageContext(String, int)}. Note that you must add this
    103    *     package to {@link org.robolectric.shadows.ShadowPackageManager#addPackage(PackageInfo)}
    104    *     first.
    105    */
    106   @Deprecated
    107   String packageName() default DEFAULT_PACKAGE_NAME;
    108 
    109   /**
    110    * Qualifiers specifying device configuration for this test, such as "fr-normal-port-hdpi".
    111    *
    112    * If the string is prefixed with '+', the qualifiers that follow are overlayed on any more
    113    * broadly-scoped qualifiers.
    114    *
    115    * See [Device Configuration](http://robolectric.org/device-configuration/) for details.
    116    *
    117    * @return Qualifiers used for device configuration and resource resolution.
    118    */
    119   String qualifiers() default DEFAULT_QUALIFIERS;
    120 
    121   /**
    122    * The directory from which to load resources.  This should be relative to the directory containing AndroidManifest.xml.
    123    *
    124    * If not specified, Robolectric defaults to {@code res}.
    125    * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test
    126    * please migrate to the preferred way to configure
    127    *
    128    * @return Android resource directory.
    129    */
    130   @Deprecated
    131   String resourceDir() default DEFAULT_RES_FOLDER;
    132 
    133   /**
    134    * The directory from which to load assets. This should be relative to the directory containing AndroidManifest.xml.
    135    *
    136    * If not specified, Robolectric defaults to {@code assets}.
    137    * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test
    138    * please migrate to the preferred way to configure
    139    *
    140    * @return Android asset directory.
    141    */
    142   @Deprecated
    143   String assetDir() default DEFAULT_ASSET_FOLDER;
    144 
    145   /**
    146    * A list of shadow classes to enable, in addition to those that are already present.
    147    *
    148    * @return A list of additional shadow classes to enable.
    149    */
    150   Class<?>[] shadows() default {};  // DEFAULT_SHADOWS
    151 
    152   /**
    153    * A list of instrumented packages, in addition to those that are already instrumented.
    154    *
    155    * @return A list of additional instrumented packages.
    156    */
    157   String[] instrumentedPackages() default {};  // DEFAULT_INSTRUMENTED_PACKAGES
    158 
    159   /**
    160    * A list of folders containing Android Libraries on which this project depends.
    161    *
    162    * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test
    163    * please migrate to the preferred way to configure
    164    *
    165    * @return A list of Android Libraries.
    166    */
    167   @Deprecated
    168   String[] libraries() default {};  // DEFAULT_LIBRARIES;
    169 
    170   class Implementation implements Config {
    171     private final int[] sdk;
    172     private final int minSdk;
    173     private final int maxSdk;
    174     private final String manifest;
    175     private final String qualifiers;
    176     private final String resourceDir;
    177     private final String assetDir;
    178     private final String packageName;
    179     private final Class<?>[] shadows;
    180     private final String[] instrumentedPackages;
    181     private final Class<? extends Application> application;
    182     private final String[] libraries;
    183 
    184     public static Config fromProperties(Properties properties) {
    185       if (properties == null || properties.size() == 0) return null;
    186       return new Implementation(
    187           parseSdkArrayProperty(properties.getProperty("sdk", "")),
    188           parseSdkInt(properties.getProperty("minSdk", "-1")),
    189           parseSdkInt(properties.getProperty("maxSdk", "-1")),
    190           properties.getProperty("manifest", DEFAULT_VALUE_STRING),
    191           properties.getProperty("qualifiers", DEFAULT_QUALIFIERS),
    192           properties.getProperty("packageName", DEFAULT_PACKAGE_NAME),
    193           properties.getProperty("resourceDir", DEFAULT_RES_FOLDER),
    194           properties.getProperty("assetDir", DEFAULT_ASSET_FOLDER),
    195           parseClasses(properties.getProperty("shadows", "")),
    196           parseStringArrayProperty(properties.getProperty("instrumentedPackages", "")),
    197           parseApplication(
    198               properties.getProperty("application", DEFAULT_APPLICATION.getCanonicalName())),
    199           parseStringArrayProperty(properties.getProperty("libraries", "")));
    200     }
    201 
    202     private static Class<?> parseClass(String className) {
    203       if (className.isEmpty()) return null;
    204       try {
    205         return Implementation.class.getClassLoader().loadClass(className);
    206       } catch (ClassNotFoundException e) {
    207         throw new RuntimeException("Could not load class: " + className);
    208       }
    209     }
    210 
    211     private static Class<?>[] parseClasses(String input) {
    212       if (input.isEmpty()) return new Class[0];
    213       final String[] classNames = input.split("[, ]+", 0);
    214       final Class[] classes = new Class[classNames.length];
    215       for (int i = 0; i < classNames.length; i++) {
    216         classes[i] = parseClass(classNames[i]);
    217       }
    218       return classes;
    219     }
    220 
    221     @SuppressWarnings("unchecked")
    222     private static <T extends Application> Class<T> parseApplication(String className) {
    223       return (Class<T>) parseClass(className);
    224     }
    225 
    226     private static String[] parseStringArrayProperty(String property) {
    227       if (property.isEmpty()) return new String[0];
    228       return property.split("[, ]+");
    229     }
    230 
    231     private static int[] parseSdkArrayProperty(String property) {
    232       String[] parts = parseStringArrayProperty(property);
    233       int[] result = new int[parts.length];
    234       for (int i = 0; i < parts.length; i++) {
    235         result[i] = parseSdkInt(parts[i]);
    236       }
    237 
    238       return result;
    239     }
    240 
    241     private static int parseSdkInt(String part) {
    242       String spec = part.trim();
    243       switch (spec) {
    244         case "ALL_SDKS":
    245           return Config.ALL_SDKS;
    246         case "TARGET_SDK":
    247           return Config.TARGET_SDK;
    248         case "OLDEST_SDK":
    249           return Config.OLDEST_SDK;
    250         case "NEWEST_SDK":
    251           return Config.NEWEST_SDK;
    252         default:
    253           return Integer.parseInt(spec);
    254       }
    255     }
    256 
    257     private static void validate(Config config) {
    258       //noinspection ConstantConditions
    259       if (config.sdk() != null && config.sdk().length > 0 &&
    260           (config.minSdk() != DEFAULT_VALUE_INT || config.maxSdk() != DEFAULT_VALUE_INT)) {
    261         throw new IllegalArgumentException("sdk and minSdk/maxSdk may not be specified together" +
    262             " (sdk=" + Arrays.toString(config.sdk()) + ", minSdk=" + config.minSdk() + ", maxSdk=" + config.maxSdk() + ")");
    263       }
    264 
    265       if (config.minSdk() > DEFAULT_VALUE_INT && config.maxSdk() > DEFAULT_VALUE_INT && config.minSdk() > config.maxSdk()) {
    266         throw new IllegalArgumentException("minSdk may not be larger than maxSdk" +
    267             " (minSdk=" + config.minSdk() + ", maxSdk=" + config.maxSdk() + ")");
    268       }
    269     }
    270 
    271     public Implementation(
    272         int[] sdk,
    273         int minSdk,
    274         int maxSdk,
    275         String manifest,
    276         String qualifiers,
    277         String packageName,
    278         String resourceDir,
    279         String assetDir,
    280         Class<?>[] shadows,
    281         String[] instrumentedPackages,
    282         Class<? extends Application> application,
    283         String[] libraries) {
    284       this.sdk = sdk;
    285       this.minSdk = minSdk;
    286       this.maxSdk = maxSdk;
    287       this.manifest = manifest;
    288       this.qualifiers = qualifiers;
    289       this.packageName = packageName;
    290       this.resourceDir = resourceDir;
    291       this.assetDir = assetDir;
    292       this.shadows = shadows;
    293       this.instrumentedPackages = instrumentedPackages;
    294       this.application = application;
    295       this.libraries = libraries;
    296 
    297       validate(this);
    298     }
    299 
    300     @Override
    301     public int[] sdk() {
    302       return sdk;
    303     }
    304 
    305     @Override
    306     public int minSdk() {
    307       return minSdk;
    308     }
    309 
    310     @Override
    311     public int maxSdk() {
    312       return maxSdk;
    313     }
    314 
    315     @Override
    316     public String manifest() {
    317       return manifest;
    318     }
    319 
    320     @Override
    321     public Class<? extends Application> application() {
    322       return application;
    323     }
    324 
    325     @Override
    326     public String qualifiers() {
    327       return qualifiers;
    328     }
    329 
    330     @Override
    331     public String packageName() {
    332       return packageName;
    333     }
    334 
    335     @Override
    336     public String resourceDir() {
    337       return resourceDir;
    338     }
    339 
    340     @Override
    341     public String assetDir() {
    342       return assetDir;
    343     }
    344 
    345     @Override
    346     public Class<?>[] shadows() {
    347       return shadows;
    348     }
    349 
    350     @Override
    351     public String[] instrumentedPackages() {
    352       return instrumentedPackages;
    353     }
    354 
    355     @Override
    356     public String[] libraries() {
    357       return libraries;
    358     }
    359 
    360     @Nonnull @Override
    361     public Class<? extends Annotation> annotationType() {
    362       return Config.class;
    363     }
    364   }
    365 
    366   class Builder {
    367     protected int[] sdk = new int[0];
    368     protected int minSdk = -1;
    369     protected int maxSdk = -1;
    370     protected String manifest = Config.DEFAULT_VALUE_STRING;
    371     protected String qualifiers = Config.DEFAULT_QUALIFIERS;
    372     protected String packageName = Config.DEFAULT_PACKAGE_NAME;
    373     protected String resourceDir = Config.DEFAULT_RES_FOLDER;
    374     protected String assetDir = Config.DEFAULT_ASSET_FOLDER;
    375     protected Class<?>[] shadows = new Class[0];
    376     protected String[] instrumentedPackages = new String[0];
    377     protected Class<? extends Application> application = DEFAULT_APPLICATION;
    378     protected String[] libraries = new String[0];
    379 
    380     public Builder() {
    381     }
    382 
    383     public Builder(Config config) {
    384       sdk = config.sdk();
    385       minSdk = config.minSdk();
    386       maxSdk = config.maxSdk();
    387       manifest = config.manifest();
    388       qualifiers = config.qualifiers();
    389       packageName = config.packageName();
    390       resourceDir = config.resourceDir();
    391       assetDir = config.assetDir();
    392       shadows = config.shadows();
    393       instrumentedPackages = config.instrumentedPackages();
    394       application = config.application();
    395       libraries = config.libraries();
    396     }
    397 
    398     public Builder setSdk(int... sdk) {
    399       this.sdk = sdk;
    400       return this;
    401     }
    402 
    403     public Builder setMinSdk(int minSdk) {
    404       this.minSdk = minSdk;
    405       return this;
    406     }
    407 
    408     public Builder setMaxSdk(int maxSdk) {
    409       this.maxSdk = maxSdk;
    410       return this;
    411     }
    412 
    413     public Builder setManifest(String manifest) {
    414       this.manifest = manifest;
    415       return this;
    416     }
    417 
    418     public Builder setQualifiers(String qualifiers) {
    419       this.qualifiers = qualifiers;
    420       return this;
    421     }
    422 
    423     public Builder setPackageName(String packageName) {
    424       this.packageName = packageName;
    425       return this;
    426     }
    427 
    428     public Builder setResourceDir(String resourceDir) {
    429       this.resourceDir = resourceDir;
    430       return this;
    431     }
    432 
    433     public Builder setAssetDir(String assetDir) {
    434       this.assetDir = assetDir;
    435       return this;
    436     }
    437 
    438     public Builder setShadows(Class<?>[] shadows) {
    439       this.shadows = shadows;
    440       return this;
    441     }
    442 
    443     public Builder setInstrumentedPackages(String[] instrumentedPackages) {
    444       this.instrumentedPackages = instrumentedPackages;
    445       return this;
    446     }
    447 
    448     public Builder setApplication(Class<? extends Application> application) {
    449       this.application = application;
    450       return this;
    451     }
    452 
    453     public Builder setLibraries(String[] libraries) {
    454       this.libraries = libraries;
    455       return this;
    456     }
    457 
    458     /**
    459      * This returns actual default values where they exist, in the sense that we could use
    460      * the values, rather than markers like {@code -1} or {@code --default}.
    461      */
    462     public static Builder defaults() {
    463       return new Builder()
    464           .setManifest(DEFAULT_MANIFEST_NAME)
    465           .setResourceDir(DEFAULT_RES_FOLDER)
    466           .setAssetDir(DEFAULT_ASSET_FOLDER);
    467     }
    468 
    469     public Builder overlay(Config overlayConfig) {
    470       int[] overlaySdk = overlayConfig.sdk();
    471       int overlayMinSdk = overlayConfig.minSdk();
    472       int overlayMaxSdk = overlayConfig.maxSdk();
    473 
    474       //noinspection ConstantConditions
    475       if (overlaySdk != null && overlaySdk.length > 0) {
    476         this.sdk = overlaySdk;
    477         this.minSdk = overlayMinSdk;
    478         this.maxSdk = overlayMaxSdk;
    479       } else {
    480         if (overlayMinSdk != DEFAULT_VALUE_INT || overlayMaxSdk != DEFAULT_VALUE_INT) {
    481           this.sdk = new int[0];
    482         } else {
    483           this.sdk = pickSdk(this.sdk, overlaySdk, new int[0]);
    484         }
    485         this.minSdk = pick(this.minSdk, overlayMinSdk, DEFAULT_VALUE_INT);
    486         this.maxSdk = pick(this.maxSdk, overlayMaxSdk, DEFAULT_VALUE_INT);
    487       }
    488       this.manifest = pick(this.manifest, overlayConfig.manifest(), DEFAULT_VALUE_STRING);
    489 
    490       String qualifiersOverlayValue = overlayConfig.qualifiers();
    491       if (qualifiersOverlayValue != null && !qualifiersOverlayValue.equals("")) {
    492         if (qualifiersOverlayValue.startsWith("+")) {
    493           this.qualifiers = this.qualifiers + " " + qualifiersOverlayValue;
    494         } else {
    495           this.qualifiers = qualifiersOverlayValue;
    496         }
    497       }
    498 
    499       this.packageName = pick(this.packageName, overlayConfig.packageName(), "");
    500       this.resourceDir = pick(this.resourceDir, overlayConfig.resourceDir(), Config.DEFAULT_RES_FOLDER);
    501       this.assetDir = pick(this.assetDir, overlayConfig.assetDir(), Config.DEFAULT_ASSET_FOLDER);
    502 
    503       List<Class<?>> shadows = new ArrayList<>(Arrays.asList(this.shadows));
    504       shadows.addAll(Arrays.asList(overlayConfig.shadows()));
    505       this.shadows = shadows.toArray(new Class[shadows.size()]);
    506 
    507       Set<String> instrumentedPackages = new HashSet<>();
    508       instrumentedPackages.addAll(Arrays.asList(this.instrumentedPackages));
    509       instrumentedPackages.addAll(Arrays.asList(overlayConfig.instrumentedPackages()));
    510       this.instrumentedPackages = instrumentedPackages.toArray(new String[instrumentedPackages.size()]);
    511 
    512       this.application = pick(this.application, overlayConfig.application(), DEFAULT_APPLICATION);
    513 
    514       Set<String> libraries = new HashSet<>();
    515       libraries.addAll(Arrays.asList(this.libraries));
    516       libraries.addAll(Arrays.asList(overlayConfig.libraries()));
    517       this.libraries = libraries.toArray(new String[libraries.size()]);
    518 
    519       return this;
    520     }
    521 
    522     private <T> T pick(T baseValue, T overlayValue, T nullValue) {
    523       return overlayValue != null ? (overlayValue.equals(nullValue) ? baseValue : overlayValue) : null;
    524     }
    525 
    526     private int[] pickSdk(int[] baseValue, int[] overlayValue, int[] nullValue) {
    527       return Arrays.equals(overlayValue, nullValue) ? baseValue : overlayValue;
    528     }
    529 
    530     public Implementation build() {
    531       return new Implementation(
    532           sdk,
    533           minSdk,
    534           maxSdk,
    535           manifest,
    536           qualifiers,
    537           packageName,
    538           resourceDir,
    539           assetDir,
    540           shadows,
    541           instrumentedPackages,
    542           application,
    543           libraries);
    544     }
    545 
    546     public static boolean isDefaultApplication(Class<? extends Application> clazz) {
    547       return clazz == null || clazz.getCanonicalName().equals(DEFAULT_APPLICATION.getCanonicalName());
    548     }
    549   }
    550 }
    551