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