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 <clinit> 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