| package org.robolectric.annotation; |
| |
| import android.app.Application; |
| import java.lang.annotation.Annotation; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Inherited; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.Set; |
| import javax.annotation.Nonnull; |
| |
| /** Configuration settings that can be used on a per-class or per-test basis. */ |
| @Documented |
| @Inherited |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target({ElementType.TYPE, ElementType.METHOD}) |
| @SuppressWarnings(value = {"BadAnnotationImplementation", "ImmutableAnnotationChecker"}) |
| public @interface Config { |
| /** |
| * TODO(vnayar): Create named constants for default values instead of magic numbers. Array named |
| * constants must be avoided in order to dodge a JDK 1.7 bug. error: annotation Config is missing |
| * value for the attribute <clinit> See <a |
| * href="https://bugs.openjdk.java.net/browse/JDK-8013485">JDK-8013485</a>. |
| */ |
| String NONE = "--none"; |
| |
| String DEFAULT_VALUE_STRING = "--default"; |
| int DEFAULT_VALUE_INT = -1; |
| |
| String DEFAULT_MANIFEST_NAME = "AndroidManifest.xml"; |
| Class<? extends Application> DEFAULT_APPLICATION = DefaultApplication.class; |
| String DEFAULT_PACKAGE_NAME = ""; |
| String DEFAULT_QUALIFIERS = ""; |
| String DEFAULT_RES_FOLDER = "res"; |
| String DEFAULT_ASSET_FOLDER = "assets"; |
| |
| int ALL_SDKS = -2; |
| int TARGET_SDK = -3; |
| int OLDEST_SDK = -4; |
| int NEWEST_SDK = -5; |
| |
| /** The Android SDK level to emulate. This value will also be set as Build.VERSION.SDK_INT. */ |
| int[] sdk() default {}; // DEFAULT_SDK |
| |
| /** The minimum Android SDK level to emulate when running tests on multiple API versions. */ |
| int minSdk() default -1; |
| |
| /** The maximum Android SDK level to emulate when running tests on multiple API versions. */ |
| int maxSdk() default -1; |
| |
| /** |
| * The Android manifest file to load; Robolectric will look relative to the current directory. |
| * Resources and assets will be loaded relative to the manifest. |
| * |
| * <p>If not specified, Robolectric defaults to {@code AndroidManifest.xml}. |
| * |
| * <p>If your project has no manifest or resources, use {@link Config#NONE}. |
| * |
| * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test |
| * please migrate to the preferred way to configure builds |
| * http://robolectric.org/getting-started/ |
| * @return The Android manifest file to load. |
| */ |
| @Deprecated |
| String manifest() default DEFAULT_VALUE_STRING; |
| |
| /** |
| * The {@link android.app.Application} class to use in the test, this takes precedence over any |
| * application specified in the AndroidManifest.xml. |
| * |
| * @return The {@link android.app.Application} class to use in the test. |
| */ |
| Class<? extends Application> application() default |
| DefaultApplication.class; // DEFAULT_APPLICATION |
| |
| /** |
| * Java package name where the "R.class" file is located. This only needs to be specified if you |
| * define an {@code applicationId} associated with {@code productFlavors} or specify {@code |
| * applicationIdSuffix} in your build.gradle. |
| * |
| * <p>If not specified, Robolectric defaults to the {@code applicationId}. |
| * |
| * @return The java package name for R.class. |
| * @deprecated To change your package name please override the applicationId in your build system. |
| * Changing package name here is broken as the package name will no longer match the package |
| * name encoded in the arsc resources file. If you are looking to simulate another application |
| * you can create another applications Context using {@link |
| * android.content.Context#createPackageContext(String, int)}. Note that you must add this |
| * package to {@link |
| * org.robolectric.shadows.ShadowPackageManager#addPackage(android.content.pm.PackageInfo)} |
| * first. |
| */ |
| @Deprecated |
| String packageName() default DEFAULT_PACKAGE_NAME; |
| |
| /** |
| * Qualifiers specifying device configuration for this test, such as "fr-normal-port-hdpi". |
| * |
| * <p>If the string is prefixed with '+', the qualifiers that follow are overlayed on any more |
| * broadly-scoped qualifiers. |
| * |
| * @see <a href="http://robolectric.org/device-configuration">Device Configuration</a> for |
| * details. |
| * @return Qualifiers used for device configuration and resource resolution. |
| */ |
| String qualifiers() default DEFAULT_QUALIFIERS; |
| |
| /** |
| * The directory from which to load resources. This should be relative to the directory containing |
| * AndroidManifest.xml. |
| * |
| * <p>If not specified, Robolectric defaults to {@code res}. |
| * |
| * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test |
| * please migrate to the preferred way to configure |
| * @return Android resource directory. |
| */ |
| @Deprecated |
| String resourceDir() default DEFAULT_RES_FOLDER; |
| |
| /** |
| * The directory from which to load assets. This should be relative to the directory containing |
| * AndroidManifest.xml. |
| * |
| * <p>If not specified, Robolectric defaults to {@code assets}. |
| * |
| * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test |
| * please migrate to the preferred way to configure |
| * @return Android asset directory. |
| */ |
| @Deprecated |
| String assetDir() default DEFAULT_ASSET_FOLDER; |
| |
| /** |
| * A list of shadow classes to enable, in addition to those that are already present. |
| * |
| * @return A list of additional shadow classes to enable. |
| */ |
| Class<?>[] shadows() default {}; // DEFAULT_SHADOWS |
| |
| /** |
| * A list of instrumented packages, in addition to those that are already instrumented. |
| * |
| * @return A list of additional instrumented packages. |
| */ |
| String[] instrumentedPackages() default {}; // DEFAULT_INSTRUMENTED_PACKAGES |
| |
| /** |
| * A list of folders containing Android Libraries on which this project depends. |
| * |
| * @deprecated If you are using at least Android Studio 3.0 alpha 5 or Bazel's android_local_test |
| * please migrate to the preferred way to configure |
| * @return A list of Android Libraries. |
| */ |
| @Deprecated |
| String[] libraries() default {}; // DEFAULT_LIBRARIES; |
| |
| class Implementation implements Config { |
| private final int[] sdk; |
| private final int minSdk; |
| private final int maxSdk; |
| private final String manifest; |
| private final String qualifiers; |
| private final String resourceDir; |
| private final String assetDir; |
| private final String packageName; |
| private final Class<?>[] shadows; |
| private final String[] instrumentedPackages; |
| private final Class<? extends Application> application; |
| private final String[] libraries; |
| |
| public static Config fromProperties(Properties properties) { |
| if (properties == null || properties.size() == 0) return null; |
| return new Implementation( |
| parseSdkArrayProperty(properties.getProperty("sdk", "")), |
| parseSdkInt(properties.getProperty("minSdk", "-1")), |
| parseSdkInt(properties.getProperty("maxSdk", "-1")), |
| properties.getProperty("manifest", DEFAULT_VALUE_STRING), |
| properties.getProperty("qualifiers", DEFAULT_QUALIFIERS), |
| properties.getProperty("packageName", DEFAULT_PACKAGE_NAME), |
| properties.getProperty("resourceDir", DEFAULT_RES_FOLDER), |
| properties.getProperty("assetDir", DEFAULT_ASSET_FOLDER), |
| parseClasses(properties.getProperty("shadows", "")), |
| parseStringArrayProperty(properties.getProperty("instrumentedPackages", "")), |
| parseApplication( |
| properties.getProperty("application", DEFAULT_APPLICATION.getCanonicalName())), |
| parseStringArrayProperty(properties.getProperty("libraries", ""))); |
| } |
| |
| private static Class<?> parseClass(String className) { |
| if (className.isEmpty()) return null; |
| try { |
| return Implementation.class.getClassLoader().loadClass(className); |
| } catch (ClassNotFoundException e) { |
| throw new RuntimeException("Could not load class: " + className); |
| } |
| } |
| |
| private static Class<?>[] parseClasses(String input) { |
| if (input.isEmpty()) return new Class[0]; |
| final String[] classNames = input.split("[, ]+", 0); |
| final Class[] classes = new Class[classNames.length]; |
| for (int i = 0; i < classNames.length; i++) { |
| classes[i] = parseClass(classNames[i]); |
| } |
| return classes; |
| } |
| |
| @SuppressWarnings("unchecked") |
| private static <T extends Application> Class<T> parseApplication(String className) { |
| return (Class<T>) parseClass(className); |
| } |
| |
| private static String[] parseStringArrayProperty(String property) { |
| if (property.isEmpty()) return new String[0]; |
| return property.split("[, ]+"); |
| } |
| |
| private static int[] parseSdkArrayProperty(String property) { |
| String[] parts = parseStringArrayProperty(property); |
| int[] result = new int[parts.length]; |
| for (int i = 0; i < parts.length; i++) { |
| result[i] = parseSdkInt(parts[i]); |
| } |
| |
| return result; |
| } |
| |
| private static int parseSdkInt(String part) { |
| String spec = part.trim(); |
| switch (spec) { |
| case "ALL_SDKS": |
| return Config.ALL_SDKS; |
| case "TARGET_SDK": |
| return Config.TARGET_SDK; |
| case "OLDEST_SDK": |
| return Config.OLDEST_SDK; |
| case "NEWEST_SDK": |
| return Config.NEWEST_SDK; |
| default: |
| return Integer.parseInt(spec); |
| } |
| } |
| |
| private static void validate(Config config) { |
| //noinspection ConstantConditions |
| if (config.sdk() != null |
| && config.sdk().length > 0 |
| && (config.minSdk() != DEFAULT_VALUE_INT || config.maxSdk() != DEFAULT_VALUE_INT)) { |
| throw new IllegalArgumentException( |
| "sdk and minSdk/maxSdk may not be specified together" |
| + " (sdk=" |
| + Arrays.toString(config.sdk()) |
| + ", minSdk=" |
| + config.minSdk() |
| + ", maxSdk=" |
| + config.maxSdk() |
| + ")"); |
| } |
| |
| if (config.minSdk() > DEFAULT_VALUE_INT |
| && config.maxSdk() > DEFAULT_VALUE_INT |
| && config.minSdk() > config.maxSdk()) { |
| throw new IllegalArgumentException( |
| "minSdk may not be larger than maxSdk" |
| + " (minSdk=" |
| + config.minSdk() |
| + ", maxSdk=" |
| + config.maxSdk() |
| + ")"); |
| } |
| } |
| |
| public 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) { |
| this.sdk = sdk; |
| this.minSdk = minSdk; |
| this.maxSdk = maxSdk; |
| this.manifest = manifest; |
| this.qualifiers = qualifiers; |
| this.packageName = packageName; |
| this.resourceDir = resourceDir; |
| this.assetDir = assetDir; |
| this.shadows = shadows; |
| this.instrumentedPackages = instrumentedPackages; |
| this.application = application; |
| this.libraries = libraries; |
| |
| validate(this); |
| } |
| |
| @Override |
| public int[] sdk() { |
| return sdk; |
| } |
| |
| @Override |
| public int minSdk() { |
| return minSdk; |
| } |
| |
| @Override |
| public int maxSdk() { |
| return maxSdk; |
| } |
| |
| @Override |
| public String manifest() { |
| return manifest; |
| } |
| |
| @Override |
| public Class<? extends Application> application() { |
| return application; |
| } |
| |
| @Override |
| public String qualifiers() { |
| return qualifiers; |
| } |
| |
| @Override |
| public String packageName() { |
| return packageName; |
| } |
| |
| @Override |
| public String resourceDir() { |
| return resourceDir; |
| } |
| |
| @Override |
| public String assetDir() { |
| return assetDir; |
| } |
| |
| @Override |
| public Class<?>[] shadows() { |
| return shadows; |
| } |
| |
| @Override |
| public String[] instrumentedPackages() { |
| return instrumentedPackages; |
| } |
| |
| @Override |
| public String[] libraries() { |
| return libraries; |
| } |
| |
| @Nonnull |
| @Override |
| public Class<? extends Annotation> annotationType() { |
| return Config.class; |
| } |
| |
| @Override |
| public String toString() { |
| return "Implementation{" |
| + "sdk=" |
| + Arrays.toString(sdk) |
| + ", minSdk=" |
| + minSdk |
| + ", maxSdk=" |
| + maxSdk |
| + ", manifest='" |
| + manifest |
| + '\'' |
| + ", qualifiers='" |
| + qualifiers |
| + '\'' |
| + ", resourceDir='" |
| + resourceDir |
| + '\'' |
| + ", assetDir='" |
| + assetDir |
| + '\'' |
| + ", packageName='" |
| + packageName |
| + '\'' |
| + ", shadows=" |
| + Arrays.toString(shadows) |
| + ", instrumentedPackages=" |
| + Arrays.toString(instrumentedPackages) |
| + ", application=" |
| + application |
| + ", libraries=" |
| + Arrays.toString(libraries) |
| + '}'; |
| } |
| } |
| |
| class Builder { |
| protected int[] sdk = new int[0]; |
| protected int minSdk = -1; |
| protected int maxSdk = -1; |
| protected String manifest = Config.DEFAULT_VALUE_STRING; |
| protected String qualifiers = Config.DEFAULT_QUALIFIERS; |
| protected String packageName = Config.DEFAULT_PACKAGE_NAME; |
| protected String resourceDir = Config.DEFAULT_RES_FOLDER; |
| protected String assetDir = Config.DEFAULT_ASSET_FOLDER; |
| protected Class<?>[] shadows = new Class[0]; |
| protected String[] instrumentedPackages = new String[0]; |
| protected Class<? extends Application> application = DEFAULT_APPLICATION; |
| protected String[] libraries = new String[0]; |
| |
| public Builder() {} |
| |
| public Builder(Config config) { |
| sdk = config.sdk(); |
| minSdk = config.minSdk(); |
| maxSdk = config.maxSdk(); |
| manifest = config.manifest(); |
| qualifiers = config.qualifiers(); |
| packageName = config.packageName(); |
| resourceDir = config.resourceDir(); |
| assetDir = config.assetDir(); |
| shadows = config.shadows(); |
| instrumentedPackages = config.instrumentedPackages(); |
| application = config.application(); |
| libraries = config.libraries(); |
| } |
| |
| public Builder setSdk(int... sdk) { |
| this.sdk = sdk; |
| return this; |
| } |
| |
| public Builder setMinSdk(int minSdk) { |
| this.minSdk = minSdk; |
| return this; |
| } |
| |
| public Builder setMaxSdk(int maxSdk) { |
| this.maxSdk = maxSdk; |
| return this; |
| } |
| |
| public Builder setManifest(String manifest) { |
| this.manifest = manifest; |
| return this; |
| } |
| |
| public Builder setQualifiers(String qualifiers) { |
| this.qualifiers = qualifiers; |
| return this; |
| } |
| |
| public Builder setPackageName(String packageName) { |
| this.packageName = packageName; |
| return this; |
| } |
| |
| public Builder setResourceDir(String resourceDir) { |
| this.resourceDir = resourceDir; |
| return this; |
| } |
| |
| public Builder setAssetDir(String assetDir) { |
| this.assetDir = assetDir; |
| return this; |
| } |
| |
| public Builder setShadows(Class<?>... shadows) { |
| this.shadows = shadows; |
| return this; |
| } |
| |
| public Builder setInstrumentedPackages(String... instrumentedPackages) { |
| this.instrumentedPackages = instrumentedPackages; |
| return this; |
| } |
| |
| public Builder setApplication(Class<? extends Application> application) { |
| this.application = application; |
| return this; |
| } |
| |
| public Builder setLibraries(String... libraries) { |
| this.libraries = libraries; |
| return this; |
| } |
| |
| /** |
| * This returns actual default values where they exist, in the sense that we could use the |
| * values, rather than markers like {@code -1} or {@code --default}. |
| */ |
| public static Builder defaults() { |
| return new Builder() |
| .setManifest(DEFAULT_MANIFEST_NAME) |
| .setResourceDir(DEFAULT_RES_FOLDER) |
| .setAssetDir(DEFAULT_ASSET_FOLDER); |
| } |
| |
| public Builder overlay(Config overlayConfig) { |
| int[] overlaySdk = overlayConfig.sdk(); |
| int overlayMinSdk = overlayConfig.minSdk(); |
| int overlayMaxSdk = overlayConfig.maxSdk(); |
| |
| //noinspection ConstantConditions |
| if (overlaySdk != null && overlaySdk.length > 0) { |
| this.sdk = overlaySdk; |
| this.minSdk = overlayMinSdk; |
| this.maxSdk = overlayMaxSdk; |
| } else { |
| if (overlayMinSdk != DEFAULT_VALUE_INT || overlayMaxSdk != DEFAULT_VALUE_INT) { |
| this.sdk = new int[0]; |
| } else { |
| this.sdk = pickSdk(this.sdk, overlaySdk, new int[0]); |
| } |
| this.minSdk = pick(this.minSdk, overlayMinSdk, DEFAULT_VALUE_INT); |
| this.maxSdk = pick(this.maxSdk, overlayMaxSdk, DEFAULT_VALUE_INT); |
| } |
| this.manifest = pick(this.manifest, overlayConfig.manifest(), DEFAULT_VALUE_STRING); |
| |
| String qualifiersOverlayValue = overlayConfig.qualifiers(); |
| if (qualifiersOverlayValue != null && !qualifiersOverlayValue.equals("")) { |
| if (qualifiersOverlayValue.startsWith("+")) { |
| this.qualifiers = this.qualifiers + " " + qualifiersOverlayValue; |
| } else { |
| this.qualifiers = qualifiersOverlayValue; |
| } |
| } |
| |
| this.packageName = pick(this.packageName, overlayConfig.packageName(), ""); |
| this.resourceDir = |
| pick(this.resourceDir, overlayConfig.resourceDir(), Config.DEFAULT_RES_FOLDER); |
| this.assetDir = pick(this.assetDir, overlayConfig.assetDir(), Config.DEFAULT_ASSET_FOLDER); |
| |
| List<Class<?>> shadows = new ArrayList<>(Arrays.asList(this.shadows)); |
| shadows.addAll(Arrays.asList(overlayConfig.shadows())); |
| this.shadows = shadows.toArray(new Class[shadows.size()]); |
| |
| Set<String> instrumentedPackages = new HashSet<>(); |
| instrumentedPackages.addAll(Arrays.asList(this.instrumentedPackages)); |
| instrumentedPackages.addAll(Arrays.asList(overlayConfig.instrumentedPackages())); |
| this.instrumentedPackages = |
| instrumentedPackages.toArray(new String[instrumentedPackages.size()]); |
| |
| this.application = pick(this.application, overlayConfig.application(), DEFAULT_APPLICATION); |
| |
| Set<String> libraries = new HashSet<>(); |
| libraries.addAll(Arrays.asList(this.libraries)); |
| libraries.addAll(Arrays.asList(overlayConfig.libraries())); |
| this.libraries = libraries.toArray(new String[libraries.size()]); |
| |
| return this; |
| } |
| |
| private <T> T pick(T baseValue, T overlayValue, T nullValue) { |
| return overlayValue != null |
| ? (overlayValue.equals(nullValue) ? baseValue : overlayValue) |
| : null; |
| } |
| |
| private int[] pickSdk(int[] baseValue, int[] overlayValue, int[] nullValue) { |
| return Arrays.equals(overlayValue, nullValue) ? baseValue : overlayValue; |
| } |
| |
| public Implementation build() { |
| return new Implementation( |
| sdk, |
| minSdk, |
| maxSdk, |
| manifest, |
| qualifiers, |
| packageName, |
| resourceDir, |
| assetDir, |
| shadows, |
| instrumentedPackages, |
| application, |
| libraries); |
| } |
| |
| public static boolean isDefaultApplication(Class<? extends Application> clazz) { |
| return clazz == null |
| || clazz.getCanonicalName().equals(DEFAULT_APPLICATION.getCanonicalName()); |
| } |
| } |
| } |