1 package org.robolectric.internal.bytecode; 2 3 import com.google.common.collect.ImmutableList; 4 import com.google.common.collect.ImmutableMap; 5 import com.google.common.collect.ImmutableSet; 6 import com.google.common.collect.Sets; 7 import java.util.Collection; 8 import java.util.Collections; 9 import java.util.HashMap; 10 import java.util.HashSet; 11 import java.util.List; 12 import java.util.Map; 13 import java.util.Set; 14 import org.robolectric.annotation.internal.DoNotInstrument; 15 import org.robolectric.annotation.internal.Instrument; 16 import org.robolectric.shadow.api.Shadow; 17 18 /** 19 * Configuration rules for {@link SandboxClassLoader}. 20 */ 21 public class InstrumentationConfiguration { 22 23 public static Builder newBuilder() { 24 return new Builder(); 25 } 26 27 static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet( 28 RobolectricInternals.class.getName(), 29 InvokeDynamicSupport.class.getName(), 30 Shadow.class.getName(), 31 32 // these classes are deprecated and will be removed soon: 33 "org.robolectric.util.FragmentTestUtil", 34 "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity" 35 ); 36 37 static final Set<String> RESOURCES_TO_ALWAYS_ACQUIRE = Sets.newHashSet("build.prop"); 38 39 private final List<String> instrumentedPackages; 40 private final Set<String> instrumentedClasses; 41 private final Set<String> classesToNotInstrument; 42 private final Map<String, String> classNameTranslations; 43 private final Set<MethodRef> interceptedMethods; 44 private final Set<String> classesToNotAcquire; 45 private final Set<String> packagesToNotAcquire; 46 private final Set<String> packagesToNotInstrument; 47 private int cachedHashCode; 48 49 private InstrumentationConfiguration( 50 Map<String, String> classNameTranslations, 51 Collection<MethodRef> interceptedMethods, 52 Collection<String> instrumentedPackages, 53 Collection<String> instrumentedClasses, 54 Collection<String> classesToNotAcquire, 55 Collection<String> packagesToNotAquire, 56 Collection<String> classesToNotInstrument, 57 Collection<String> packagesToNotInstrument) { 58 this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations); 59 this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods); 60 this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages); 61 this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses); 62 this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire); 63 this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire); 64 this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument); 65 this.packagesToNotInstrument = ImmutableSet.copyOf(packagesToNotInstrument); 66 this.cachedHashCode = 0; 67 } 68 69 /** 70 * Determine if {@link SandboxClassLoader} should instrument a given class. 71 * 72 * @param classInfo The class to check. 73 * @return True if the class should be instrumented. 74 */ 75 public boolean shouldInstrument(ClassInfo classInfo) { 76 return !(classInfo.isInterface() 77 || classInfo.isAnnotation() 78 || classInfo.hasAnnotation(DoNotInstrument.class)) 79 && (isInInstrumentedPackage(classInfo) 80 || instrumentedClasses.contains(classInfo.getName()) 81 || classInfo.hasAnnotation(Instrument.class)) 82 && !(classesToNotInstrument.contains(classInfo.getName())) 83 && !(isInPackagesToNotInstrument(classInfo)); 84 } 85 86 /** 87 * Determine if {@link SandboxClassLoader} should load a given class. 88 * 89 * @param name The fully-qualified class name. 90 * @return True if the class should be loaded. 91 */ 92 public boolean shouldAcquire(String name) { 93 if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) { 94 return true; 95 } 96 97 if (name.equals("java.util.jar.StrictJarFile")) { 98 return true; 99 } 100 101 // android.R and com.android.internal.R classes must be loaded from the framework jar 102 if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) { 103 return true; 104 } 105 106 // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864 107 if (name.equals("javax.net.ssl.DistinguishedNameParser") 108 || name.equals("javax.microedition.khronos.opengles.GL")) { 109 return true; 110 } 111 112 for (String packageName : packagesToNotAcquire) { 113 if (name.startsWith(packageName)) return false; 114 } 115 116 // R classes must be loaded from system CP 117 boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$"); 118 return !isRClass && !classesToNotAcquire.contains(name); 119 } 120 121 /** 122 * Determine if {@link SandboxClassLoader} should load a given resource. 123 * 124 * @param name The fully-qualified resource name. 125 * @return True if the resource should be loaded. 126 */ 127 public boolean shouldAcquireResource(String name) { 128 return RESOURCES_TO_ALWAYS_ACQUIRE.contains(name); 129 } 130 131 public Set<MethodRef> methodsToIntercept() { 132 return Collections.unmodifiableSet(interceptedMethods); 133 } 134 135 /** 136 * Map from a requested class to an alternate stand-in, or not. 137 * 138 * @return Mapping of class name translations. 139 */ 140 public Map<String, String> classNameTranslations() { 141 return Collections.unmodifiableMap(classNameTranslations); 142 } 143 144 public boolean containsStubs(ClassInfo classInfo) { 145 return classInfo.getName().startsWith("com.google.android.maps."); 146 } 147 148 private boolean isInInstrumentedPackage(ClassInfo classInfo) { 149 final String className = classInfo.getName(); 150 for (String instrumentedPackage : instrumentedPackages) { 151 if (className.startsWith(instrumentedPackage)) { 152 return true; 153 } 154 } 155 return false; 156 } 157 158 private boolean isInPackagesToNotInstrument(ClassInfo classInfo) { 159 final String className = classInfo.getName(); 160 for (String notInstrumentedPackage : packagesToNotInstrument) { 161 if (className.startsWith(notInstrumentedPackage)) { 162 return true; 163 } 164 } 165 return false; 166 } 167 168 @Override 169 public boolean equals(Object o) { 170 if (this == o) return true; 171 if (o == null || getClass() != o.getClass()) return false; 172 173 InstrumentationConfiguration that = (InstrumentationConfiguration) o; 174 175 if (!classNameTranslations.equals(that.classNameTranslations)) return false; 176 if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false; 177 if (!instrumentedPackages.equals(that.instrumentedPackages)) return false; 178 if (!instrumentedClasses.equals(that.instrumentedClasses)) return false; 179 if (!interceptedMethods.equals(that.interceptedMethods)) return false; 180 181 182 return true; 183 } 184 185 @Override 186 public int hashCode() { 187 if (cachedHashCode != 0) { 188 return cachedHashCode; 189 } 190 191 int result = instrumentedPackages.hashCode(); 192 result = 31 * result + instrumentedClasses.hashCode(); 193 result = 31 * result + classNameTranslations.hashCode(); 194 result = 31 * result + interceptedMethods.hashCode(); 195 result = 31 * result + classesToNotAcquire.hashCode(); 196 cachedHashCode = result; 197 return result; 198 } 199 200 public static final class Builder { 201 private final Collection<String> instrumentedPackages = new HashSet<>(); 202 private final Collection<MethodRef> interceptedMethods = new HashSet<>(); 203 private final Map<String, String> classNameTranslations = new HashMap<>(); 204 private final Collection<String> classesToNotAcquire = new HashSet<>(); 205 private final Collection<String> packagesToNotAcquire = new HashSet<>(); 206 private final Collection<String> instrumentedClasses = new HashSet<>(); 207 private final Collection<String> classesToNotInstrument = new HashSet<>(); 208 private final Collection<String> packagesToNotInstrument = new HashSet<>(); 209 210 public Builder() { 211 } 212 213 public Builder(InstrumentationConfiguration classLoaderConfig) { 214 instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages); 215 interceptedMethods.addAll(classLoaderConfig.interceptedMethods); 216 classNameTranslations.putAll(classLoaderConfig.classNameTranslations); 217 classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire); 218 packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire); 219 instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses); 220 classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument); 221 packagesToNotInstrument.addAll(classLoaderConfig.packagesToNotInstrument); 222 } 223 224 public Builder doNotAcquireClass(Class<?> clazz) { 225 doNotAcquireClass(clazz.getName()); 226 return this; 227 } 228 229 public Builder doNotAcquireClass(String className) { 230 this.classesToNotAcquire.add(className); 231 return this; 232 } 233 234 public Builder doNotAcquirePackage(String packageName) { 235 this.packagesToNotAcquire.add(packageName); 236 return this; 237 } 238 239 public Builder addClassNameTranslation(String fromName, String toName) { 240 classNameTranslations.put(fromName, toName); 241 return this; 242 } 243 244 public Builder addInterceptedMethod(MethodRef methodReference) { 245 interceptedMethods.add(methodReference); 246 return this; 247 } 248 249 public Builder addInstrumentedClass(String name) { 250 instrumentedClasses.add(name); 251 return this; 252 } 253 254 public Builder addInstrumentedPackage(String packageName) { 255 instrumentedPackages.add(packageName); 256 return this; 257 } 258 259 public Builder doNotInstrumentClass(String className) { 260 this.classesToNotInstrument.add(className); 261 return this; 262 } 263 264 public Builder doNotInstrumentPackage(String packageName) { 265 this.packagesToNotInstrument.add(packageName); 266 return this; 267 } 268 269 public InstrumentationConfiguration build() { 270 return new InstrumentationConfiguration( 271 classNameTranslations, 272 interceptedMethods, 273 instrumentedPackages, 274 instrumentedClasses, 275 classesToNotAcquire, 276 packagesToNotAcquire, 277 classesToNotInstrument, 278 packagesToNotInstrument); 279 } 280 } 281 } 282