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