• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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    */
sdk()54   int[] sdk() default {};  // DEFAULT_SDK
55 
56   /**
57    * The minimum Android SDK level to emulate when running tests on multiple API versions.
58    */
minSdk()59   int minSdk() default -1;
60 
61   /**
62    * The maximum Android SDK level to emulate when running tests on multiple API versions.
63    */
maxSdk()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
manifest()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    */
application()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
packageName()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    */
qualifiers()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
resourceDir()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
assetDir()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    */
shadows()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    */
instrumentedPackages()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
libraries()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 
fromProperties(Properties properties)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 
parseClass(String className)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 
parseClasses(String input)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")
parseApplication(String className)222     private static <T extends Application> Class<T> parseApplication(String className) {
223       return (Class<T>) parseClass(className);
224     }
225 
parseStringArrayProperty(String property)226     private static String[] parseStringArrayProperty(String property) {
227       if (property.isEmpty()) return new String[0];
228       return property.split("[, ]+");
229     }
230 
parseSdkArrayProperty(String property)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 
parseSdkInt(String part)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 
validate(Config config)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 
Implementation( int[] sdk, int minSdk, int maxSdk, String manifest, String qualifiers, String packageName, String resourceDir, String assetDir, Class<?>[] shadows, String[] instrumentedPackages, Class<? extends Application> application, String[] libraries)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
sdk()301     public int[] sdk() {
302       return sdk;
303     }
304 
305     @Override
minSdk()306     public int minSdk() {
307       return minSdk;
308     }
309 
310     @Override
maxSdk()311     public int maxSdk() {
312       return maxSdk;
313     }
314 
315     @Override
manifest()316     public String manifest() {
317       return manifest;
318     }
319 
320     @Override
application()321     public Class<? extends Application> application() {
322       return application;
323     }
324 
325     @Override
qualifiers()326     public String qualifiers() {
327       return qualifiers;
328     }
329 
330     @Override
packageName()331     public String packageName() {
332       return packageName;
333     }
334 
335     @Override
resourceDir()336     public String resourceDir() {
337       return resourceDir;
338     }
339 
340     @Override
assetDir()341     public String assetDir() {
342       return assetDir;
343     }
344 
345     @Override
shadows()346     public Class<?>[] shadows() {
347       return shadows;
348     }
349 
350     @Override
instrumentedPackages()351     public String[] instrumentedPackages() {
352       return instrumentedPackages;
353     }
354 
355     @Override
libraries()356     public String[] libraries() {
357       return libraries;
358     }
359 
360     @Nonnull @Override
annotationType()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 
Builder()380     public Builder() {
381     }
382 
Builder(Config config)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 
setSdk(int... sdk)398     public Builder setSdk(int... sdk) {
399       this.sdk = sdk;
400       return this;
401     }
402 
setMinSdk(int minSdk)403     public Builder setMinSdk(int minSdk) {
404       this.minSdk = minSdk;
405       return this;
406     }
407 
setMaxSdk(int maxSdk)408     public Builder setMaxSdk(int maxSdk) {
409       this.maxSdk = maxSdk;
410       return this;
411     }
412 
setManifest(String manifest)413     public Builder setManifest(String manifest) {
414       this.manifest = manifest;
415       return this;
416     }
417 
setQualifiers(String qualifiers)418     public Builder setQualifiers(String qualifiers) {
419       this.qualifiers = qualifiers;
420       return this;
421     }
422 
setPackageName(String packageName)423     public Builder setPackageName(String packageName) {
424       this.packageName = packageName;
425       return this;
426     }
427 
setResourceDir(String resourceDir)428     public Builder setResourceDir(String resourceDir) {
429       this.resourceDir = resourceDir;
430       return this;
431     }
432 
setAssetDir(String assetDir)433     public Builder setAssetDir(String assetDir) {
434       this.assetDir = assetDir;
435       return this;
436     }
437 
setShadows(Class<?>[] shadows)438     public Builder setShadows(Class<?>[] shadows) {
439       this.shadows = shadows;
440       return this;
441     }
442 
setInstrumentedPackages(String[] instrumentedPackages)443     public Builder setInstrumentedPackages(String[] instrumentedPackages) {
444       this.instrumentedPackages = instrumentedPackages;
445       return this;
446     }
447 
setApplication(Class<? extends Application> application)448     public Builder setApplication(Class<? extends Application> application) {
449       this.application = application;
450       return this;
451     }
452 
setLibraries(String[] libraries)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      */
defaults()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 
overlay(Config overlayConfig)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 
pick(T baseValue, T overlayValue, T nullValue)522     private <T> T pick(T baseValue, T overlayValue, T nullValue) {
523       return overlayValue != null ? (overlayValue.equals(nullValue) ? baseValue : overlayValue) : null;
524     }
525 
pickSdk(int[] baseValue, int[] overlayValue, int[] nullValue)526     private int[] pickSdk(int[] baseValue, int[] overlayValue, int[] nullValue) {
527       return Arrays.equals(overlayValue, nullValue) ? baseValue : overlayValue;
528     }
529 
build()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 
isDefaultApplication(Class<? extends Application> clazz)546     public static boolean isDefaultApplication(Class<? extends Application> clazz) {
547       return clazz == null || clazz.getCanonicalName().equals(DEFAULT_APPLICATION.getCanonicalName());
548     }
549   }
550 }
551