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