package org.robolectric.internal.bytecode; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.robolectric.annotation.internal.DoNotInstrument; import org.robolectric.annotation.internal.Instrument; import org.robolectric.internal.ShadowExtractor; import org.robolectric.shadow.api.Shadow; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Configuration rules for {@link SandboxClassLoader}. */ public class InstrumentationConfiguration { public static Builder newBuilder() { return new Builder(); } static final Set<String> CLASSES_TO_ALWAYS_ACQUIRE = Sets.newHashSet( RobolectricInternals.class.getName(), InvokeDynamicSupport.class.getName(), Shadow.class.getName(), // these classes are deprecated and will be removed soon: ShadowExtractor.class.getName(), "org.robolectric.internal.Shadow", "org.robolectric.res.builder.DefaultPackageManager", "org.robolectric.res.builder.DefaultPackageManager$1", "org.robolectric.res.builder.DefaultPackageManager$IntentComparator", "org.robolectric.res.builder.DefaultPackageManager$RoboPackageInstaller", "org.robolectric.res.builder.DefaultPackageManager$RoboPackageInstaller$1", "org.robolectric.res.builder.RobolectricPackageManager", "org.robolectric.res.builder.StubPackageManager", "org.robolectric.util.AccessibilityUtil", "org.robolectric.util.ActivityController", "org.robolectric.util.ApplicationTestUtil", "org.robolectric.util.ContentProviderController", "org.robolectric.util.FragmentController", "org.robolectric.util.FragmentTestUtil", "org.robolectric.util.FragmentTestUtil$FragmentUtilActivity", "org.robolectric.util.IntentServiceController", "org.robolectric.util.ServiceController", "org.robolectric.util.concurrent.RoboExecutorService" ); private final List<String> instrumentedPackages; private final Set<String> instrumentedClasses; private final Set<String> classesToNotInstrument; private final Map<String, String> classNameTranslations; private final Set<MethodRef> interceptedMethods; private final Set<String> classesToNotAcquire; private final Set<String> packagesToNotAcquire; private int cachedHashCode; private InstrumentationConfiguration(Map<String, String> classNameTranslations, Collection<MethodRef> interceptedMethods, Collection<String> instrumentedPackages, Collection<String> instrumentedClasses, Collection<String> classesToNotAcquire, Collection<String> packagesToNotAquire, Collection<String> classesToNotInstrument) { this.classNameTranslations = ImmutableMap.copyOf(classNameTranslations); this.interceptedMethods = ImmutableSet.copyOf(interceptedMethods); this.instrumentedPackages = ImmutableList.copyOf(instrumentedPackages); this.instrumentedClasses = ImmutableSet.copyOf(instrumentedClasses); this.classesToNotAcquire = ImmutableSet.copyOf(classesToNotAcquire); this.packagesToNotAcquire = ImmutableSet.copyOf(packagesToNotAquire); this.classesToNotInstrument = ImmutableSet.copyOf(classesToNotInstrument); this.cachedHashCode = 0; } /** * Determine if {@link SandboxClassLoader} should instrument a given class. * * @param classInfo The class to check. * @return True if the class should be instrumented. */ public boolean shouldInstrument(ClassInfo classInfo) { return !(classInfo.isInterface() || classInfo.isAnnotation() || classInfo.hasAnnotation(DoNotInstrument.class)) && (isInInstrumentedPackage(classInfo) || instrumentedClasses.contains(classInfo.getName()) || classInfo.hasAnnotation(Instrument.class)) && !(classesToNotInstrument.contains(classInfo.getName())); } /** * Determine if {@link SandboxClassLoader} should load a given class. * * @param name The fully-qualified class name. * @return True if the class should be loaded. */ public boolean shouldAcquire(String name) { if (CLASSES_TO_ALWAYS_ACQUIRE.contains(name)) { return true; } // android.R and com.android.internal.R classes must be loaded from the framework jar if (name.matches("(android|com\\.android\\.internal)\\.R(\\$.+)?")) { return true; } // Hack. Fixes https://github.com/robolectric/robolectric/issues/1864 if (name.equals("javax.net.ssl.DistinguishedNameParser") || name.equals("javax.microedition.khronos.opengles.GL")) { return true; } for (String packageName : packagesToNotAcquire) { if (name.startsWith(packageName)) return false; } // R classes must be loaded from system CP boolean isRClass = name.matches(".*\\.R(|\\$[a-z]+)$"); return !isRClass && !classesToNotAcquire.contains(name); } public Set<MethodRef> methodsToIntercept() { return Collections.unmodifiableSet(interceptedMethods); } /** * Map from a requested class to an alternate stand-in, or not. * * @return Mapping of class name translations. */ public Map<String, String> classNameTranslations() { return Collections.unmodifiableMap(classNameTranslations); } public boolean containsStubs(ClassInfo classInfo) { return classInfo.getName().startsWith("com.google.android.maps."); } private boolean isInInstrumentedPackage(ClassInfo classInfo) { final String className = classInfo.getName(); for (String instrumentedPackage : instrumentedPackages) { if (className.startsWith(instrumentedPackage)) { return true; } } return false; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; InstrumentationConfiguration that = (InstrumentationConfiguration) o; if (!classNameTranslations.equals(that.classNameTranslations)) return false; if (!classesToNotAcquire.equals(that.classesToNotAcquire)) return false; if (!instrumentedPackages.equals(that.instrumentedPackages)) return false; if (!instrumentedClasses.equals(that.instrumentedClasses)) return false; if (!interceptedMethods.equals(that.interceptedMethods)) return false; return true; } @Override public int hashCode() { if (cachedHashCode != 0) { return cachedHashCode; } int result = instrumentedPackages.hashCode(); result = 31 * result + instrumentedClasses.hashCode(); result = 31 * result + classNameTranslations.hashCode(); result = 31 * result + interceptedMethods.hashCode(); result = 31 * result + classesToNotAcquire.hashCode(); cachedHashCode = result; return result; } public static final class Builder { private final Collection<String> instrumentedPackages = new HashSet<>(); private final Collection<MethodRef> interceptedMethods = new HashSet<>(); private final Map<String, String> classNameTranslations = new HashMap<>(); private final Collection<String> classesToNotAcquire = new HashSet<>(); private final Collection<String> packagesToNotAcquire = new HashSet<>(); private final Collection<String> instrumentedClasses = new HashSet<>(); private final Collection<String> classesToNotInstrument = new HashSet<>(); public Builder() { } public Builder(InstrumentationConfiguration classLoaderConfig) { instrumentedPackages.addAll(classLoaderConfig.instrumentedPackages); interceptedMethods.addAll(classLoaderConfig.interceptedMethods); classNameTranslations.putAll(classLoaderConfig.classNameTranslations); classesToNotAcquire.addAll(classLoaderConfig.classesToNotAcquire); packagesToNotAcquire.addAll(classLoaderConfig.packagesToNotAcquire); instrumentedClasses.addAll(classLoaderConfig.instrumentedClasses); classesToNotInstrument.addAll(classLoaderConfig.classesToNotInstrument); } public Builder doNotAcquireClass(Class<?> clazz) { doNotAcquireClass(clazz.getName()); return this; } public Builder doNotAcquireClass(String className) { this.classesToNotAcquire.add(className); return this; } public Builder doNotAcquirePackage(String packageName) { this.packagesToNotAcquire.add(packageName); return this; } public Builder addClassNameTranslation(String fromName, String toName) { classNameTranslations.put(fromName, toName); return this; } public Builder addInterceptedMethod(MethodRef methodReference) { interceptedMethods.add(methodReference); return this; } public Builder addInstrumentedClass(String name) { instrumentedClasses.add(name); return this; } public Builder addInstrumentedPackage(String packageName) { instrumentedPackages.add(packageName); return this; } public Builder doNotInstrumentClass(String className) { this.classesToNotInstrument.add(className); return this; } public InstrumentationConfiguration build() { return new InstrumentationConfiguration( classNameTranslations, interceptedMethods, instrumentedPackages, instrumentedClasses, classesToNotAcquire, packagesToNotAcquire, classesToNotInstrument); } } }