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