| package org.robolectric; |
| |
| import android.app.Application; |
| import com.google.common.collect.ImmutableMap; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.net.URL; |
| import java.security.SecureRandom; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Properties; |
| import java.util.ServiceLoader; |
| import javax.annotation.Nonnull; |
| import org.junit.Ignore; |
| import org.junit.runners.model.FrameworkMethod; |
| import org.junit.runners.model.InitializationError; |
| import org.junit.runners.model.Statement; |
| import org.robolectric.android.AndroidInterceptors; |
| import org.robolectric.android.internal.ParallelUniverse; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.internal.AndroidConfigurer; |
| import org.robolectric.internal.BuckManifestFactory; |
| import org.robolectric.internal.DefaultManifestFactory; |
| import org.robolectric.internal.GradleManifestFactory; |
| import org.robolectric.internal.ManifestFactory; |
| import org.robolectric.internal.ManifestIdentifier; |
| import org.robolectric.internal.MavenManifestFactory; |
| import org.robolectric.internal.ParallelUniverseInterface; |
| import org.robolectric.internal.SandboxFactory; |
| import org.robolectric.internal.SandboxTestRunner; |
| import org.robolectric.internal.SdkConfig; |
| import org.robolectric.internal.SdkEnvironment; |
| import org.robolectric.internal.ShadowProvider; |
| import org.robolectric.internal.bytecode.ClassHandler; |
| import org.robolectric.internal.bytecode.InstrumentationConfiguration; |
| import org.robolectric.internal.bytecode.InstrumentationConfiguration.Builder; |
| import org.robolectric.internal.bytecode.Interceptor; |
| import org.robolectric.internal.bytecode.Sandbox; |
| import org.robolectric.internal.bytecode.SandboxClassLoader; |
| import org.robolectric.internal.bytecode.ShadowMap; |
| import org.robolectric.internal.bytecode.ShadowWrangler; |
| import org.robolectric.internal.dependency.CachedDependencyResolver; |
| import org.robolectric.internal.dependency.DependencyResolver; |
| import org.robolectric.internal.dependency.LocalDependencyResolver; |
| import org.robolectric.internal.dependency.PropertiesDependencyResolver; |
| import org.robolectric.manifest.AndroidManifest; |
| import org.robolectric.res.Fs; |
| import org.robolectric.res.FsFile; |
| import org.robolectric.res.PackageResourceTable; |
| import org.robolectric.res.ResourceMerger; |
| import org.robolectric.res.ResourcePath; |
| import org.robolectric.res.ResourceTable; |
| import org.robolectric.res.ResourceTableFactory; |
| import org.robolectric.res.RoutingResourceTable; |
| import org.robolectric.util.Logger; |
| import org.robolectric.util.PerfStatsCollector; |
| import org.robolectric.util.ReflectionHelpers; |
| |
| /** |
| * Installs a {@link SandboxClassLoader} and {@link ResourceTable} in order to |
| * provide a simulation of the Android runtime environment. |
| */ |
| public class RobolectricTestRunner extends SandboxTestRunner { |
| |
| public static final String CONFIG_PROPERTIES = "robolectric.properties"; |
| |
| private static final Map<AndroidManifest, PackageResourceTable> appResourceTableCache = new HashMap<>(); |
| private static final Map<ManifestIdentifier, AndroidManifest> appManifestsCache = new HashMap<>(); |
| private static PackageResourceTable compiletimeSdkResourceTable; |
| |
| private final SdkPicker sdkPicker; |
| private final ConfigMerger configMerger; |
| private ServiceLoader<ShadowProvider> providers; |
| private transient DependencyResolver dependencyResolver; |
| |
| static { |
| new SecureRandom(); // this starts up the Poller SunPKCS11-Darwin thread early, outside of any Robolectric classloader |
| } |
| |
| /** |
| * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file |
| * and res directory by default. Use the {@link Config} annotation to configure. |
| * |
| * @param testClass the test class to be run |
| * @throws InitializationError if junit says so |
| */ |
| public RobolectricTestRunner(final Class<?> testClass) throws InitializationError { |
| super(testClass); |
| this.configMerger = createConfigMerger(); |
| this.sdkPicker = createSdkPicker(); |
| } |
| |
| protected DependencyResolver getJarResolver() { |
| if (dependencyResolver == null) { |
| if (Boolean.getBoolean("robolectric.offline")) { |
| String propPath = System.getProperty("robolectric-deps.properties"); |
| if (propPath != null) { |
| try { |
| dependencyResolver = new PropertiesDependencyResolver( |
| Fs.newFile(propPath), |
| null); |
| } catch (IOException e) { |
| throw new RuntimeException("couldn't read dependencies" , e); |
| } |
| } else { |
| String dependencyDir = System.getProperty("robolectric.dependency.dir", "."); |
| dependencyResolver = new LocalDependencyResolver(new File(dependencyDir)); |
| } |
| } else { |
| File cacheDir = new File(new File(System.getProperty("java.io.tmpdir")), "robolectric"); |
| |
| Class<?> mavenDependencyResolverClass = ReflectionHelpers.loadClass(RobolectricTestRunner.class.getClassLoader(), |
| "org.robolectric.internal.dependency.MavenDependencyResolver"); |
| DependencyResolver dependencyResolver = (DependencyResolver) ReflectionHelpers.callConstructor(mavenDependencyResolverClass); |
| if (cacheDir.exists() || cacheDir.mkdir()) { |
| Logger.info("Dependency cache location: %s", cacheDir.getAbsolutePath()); |
| this.dependencyResolver = new CachedDependencyResolver(dependencyResolver, cacheDir, 60 * 60 * 24 * 1000); |
| } else { |
| this.dependencyResolver = dependencyResolver; |
| } |
| } |
| |
| URL buildPathPropertiesUrl = getClass().getClassLoader().getResource("robolectric-deps.properties"); |
| if (buildPathPropertiesUrl != null) { |
| Logger.info("Using Robolectric classes from %s", buildPathPropertiesUrl.getPath()); |
| |
| FsFile propertiesFile = Fs.fileFromPath(buildPathPropertiesUrl.getFile()); |
| try { |
| dependencyResolver = new PropertiesDependencyResolver(propertiesFile, dependencyResolver); |
| } catch (IOException e) { |
| throw new RuntimeException("couldn't read " + buildPathPropertiesUrl, e); |
| } |
| } |
| } |
| |
| return dependencyResolver; |
| } |
| |
| /** |
| * Create a {@link ClassHandler} appropriate for the given arguments. |
| * |
| * Robolectric may chose to cache the returned instance, keyed by <tt>shadowMap</tt> and <tt>sdkConfig</tt>. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @param shadowMap the {@link ShadowMap} in effect for this test |
| * @param sandbox the {@link SdkConfig} in effect for this test |
| * @return an appropriate {@link ClassHandler}. This implementation returns a {@link ShadowWrangler}. |
| * @since 2.3 |
| */ |
| @Override |
| @Nonnull |
| protected ClassHandler createClassHandler(ShadowMap shadowMap, Sandbox sandbox) { |
| return new ShadowWrangler(shadowMap, ((SdkEnvironment) sandbox).getSdkConfig().getApiLevel(), getInterceptors()); |
| } |
| |
| /** |
| * Create a {@link ConfigMerger} for calculating the {@link Config} tests. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @return an {@link ConfigMerger}. |
| * @since 3.2 |
| */ |
| @Nonnull |
| private ConfigMerger createConfigMerger() { |
| return new ConfigMerger(); |
| } |
| |
| /** |
| * Create a {@link SdkPicker} for determining which SDKs will be tested. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @return an {@link SdkPicker}. |
| * @since 3.2 |
| */ |
| @Nonnull |
| protected SdkPicker createSdkPicker() { |
| return new SdkPicker(); |
| } |
| |
| @Override |
| @Nonnull // todo |
| protected Collection<Interceptor> findInterceptors() { |
| return AndroidInterceptors.all(); |
| } |
| |
| /** |
| * Create an {@link InstrumentationConfiguration} suitable for the provided {@link Config}. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @param config the merged configuration for the test that's about to run -- todo |
| * @return an {@link InstrumentationConfiguration} |
| * @deprecated Override {@link #createClassLoaderConfig(FrameworkMethod)} instead |
| */ |
| @Deprecated |
| @Nonnull |
| public InstrumentationConfiguration createClassLoaderConfig(Config config) { |
| FrameworkMethod method = ((MethodPassThrough) config).method; |
| Builder builder = new InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method)); |
| AndroidConfigurer.configure(builder, getInterceptors()); |
| AndroidConfigurer.withConfig(builder, config); |
| return builder.build(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override @Nonnull |
| protected InstrumentationConfiguration createClassLoaderConfig(final FrameworkMethod method) { |
| return createClassLoaderConfig(new Config.Builder(((RobolectricFrameworkMethod) method).config) { |
| @Override |
| public Config.Implementation build() { |
| return new MethodPassThrough(method, sdk, minSdk, maxSdk, manifest, qualifiers, packageName, abiSplit, resourceDir, assetDir, buildDir, shadows, instrumentedPackages, application, libraries, constants); |
| } |
| }.build()); |
| } |
| |
| /** |
| * An instance of the returned class will be created for each test invocation. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @return a class which implements {@link TestLifecycle}. This implementation returns a {@link DefaultTestLifecycle}. |
| */ |
| @Nonnull |
| protected Class<? extends TestLifecycle> getTestLifecycleClass() { |
| return DefaultTestLifecycle.class; |
| } |
| |
| @Override |
| protected List<FrameworkMethod> getChildren() { |
| List<FrameworkMethod> children = new ArrayList<>(); |
| for (FrameworkMethod frameworkMethod : super.getChildren()) { |
| try { |
| Config config = getConfig(frameworkMethod.getMethod()); |
| AndroidManifest appManifest = getAppManifest(config); |
| |
| List<SdkConfig> sdksToRun = sdkPicker.selectSdks(config, appManifest); |
| RobolectricFrameworkMethod last = null; |
| for (SdkConfig sdkConfig : sdksToRun) { |
| last = new RobolectricFrameworkMethod(frameworkMethod.getMethod(), appManifest, sdkConfig, config); |
| children.add(last); |
| } |
| if (last != null) { |
| last.dontIncludeApiLevelInName(); |
| } |
| } catch (IllegalArgumentException e) { |
| throw new IllegalArgumentException("failed to configure " + |
| getTestClass().getName() + "." + frameworkMethod.getMethod().getName() + |
| ": " + e.getMessage(), e); |
| } |
| } |
| return children; |
| } |
| |
| /** |
| * Returns the ResourceProvider for the compile time SDK. |
| */ |
| @Nonnull |
| private static PackageResourceTable getCompiletimeSdkResourceTable() { |
| if (compiletimeSdkResourceTable == null) { |
| ResourceTableFactory resourceTableFactory = new ResourceTableFactory(); |
| compiletimeSdkResourceTable = resourceTableFactory.newFrameworkResourceTable(new ResourcePath(android.R.class, null, null)); |
| } |
| return compiletimeSdkResourceTable; |
| } |
| |
| /** |
| * @deprecated Override {@link #shouldIgnore(FrameworkMethod)} instead. |
| */ |
| @Deprecated |
| protected boolean shouldIgnore(FrameworkMethod method, Config config) { |
| return method.getAnnotation(Ignore.class) != null; |
| } |
| |
| @Override protected boolean shouldIgnore(FrameworkMethod method) { |
| return shouldIgnore(method, ((RobolectricFrameworkMethod) method).config); |
| } |
| |
| @Override |
| @Nonnull |
| protected SdkEnvironment getSandbox(FrameworkMethod method) { |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; |
| SdkConfig sdkConfig = roboMethod.sdkConfig; |
| return SandboxFactory.INSTANCE.getSdkEnvironment( |
| createClassLoaderConfig(method), getJarResolver(), sdkConfig); |
| } |
| |
| @Override |
| protected void beforeTest(Sandbox sandbox, FrameworkMethod method, Method bootstrappedMethod) throws Throwable { |
| SdkEnvironment sdkEnvironment = (SdkEnvironment) sandbox; |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; |
| |
| PerfStatsCollector perfStatsCollector = PerfStatsCollector.getInstance(); |
| SdkConfig sdkConfig = roboMethod.sdkConfig; |
| perfStatsCollector.putMetadata(AndroidMetadata.class, |
| new AndroidMetadata( |
| ImmutableMap.of("ro.build.version.sdk", "" + sdkConfig.getApiLevel()))); |
| |
| roboMethod.parallelUniverseInterface = getHooksInterface(sdkEnvironment); |
| Class<TestLifecycle> cl = sdkEnvironment.bootstrappedClass(getTestLifecycleClass()); |
| roboMethod.testLifecycle = ReflectionHelpers.newInstance(cl); |
| |
| providers = ServiceLoader.load(ShadowProvider.class, sdkEnvironment.getRobolectricClassLoader()); |
| |
| roboMethod.parallelUniverseInterface.setSdkConfig(sdkConfig); |
| perfStatsCollector.measure("reset Android state (before test)", |
| () -> resetStaticState()); |
| |
| AndroidManifest appManifest = roboMethod.getAppManifest(); |
| PackageResourceTable systemResourceTable = sdkEnvironment.getSystemResourceTable(getJarResolver()); |
| PackageResourceTable appResourceTable = getAppResourceTable(appManifest); |
| |
| roboMethod.parallelUniverseInterface.setUpApplicationState( |
| bootstrappedMethod, |
| appManifest, |
| roboMethod.config, |
| new RoutingResourceTable(appResourceTable, getCompiletimeSdkResourceTable()), |
| new RoutingResourceTable(appResourceTable, systemResourceTable), |
| new RoutingResourceTable(systemResourceTable)); |
| roboMethod.testLifecycle.beforeTest(bootstrappedMethod); |
| } |
| |
| @Override |
| protected void afterTest(FrameworkMethod method, Method bootstrappedMethod) { |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; |
| |
| try { |
| roboMethod.parallelUniverseInterface.tearDownApplication(); |
| } finally { |
| try { |
| internalAfterTest(method, bootstrappedMethod); |
| } finally { |
| // reset static state afterward too, so statics don't defeat GC? |
| PerfStatsCollector.getInstance().measure("reset Android state (after test)", |
| () -> resetStaticState()); |
| } |
| } |
| } |
| |
| private void resetStaticState() { |
| for (ShadowProvider provider : providers) { |
| provider.reset(); |
| } |
| } |
| |
| @Override |
| protected void finallyAfterTest(FrameworkMethod method) { |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) method; |
| |
| roboMethod.testLifecycle = null; |
| roboMethod.parallelUniverseInterface = null; |
| } |
| |
| @Override protected SandboxTestRunner.HelperTestRunner getHelperTestRunner(Class bootstrappedTestClass) { |
| try { |
| return new HelperTestRunner(bootstrappedTestClass); |
| } catch (InitializationError initializationError) { |
| throw new RuntimeException(initializationError); |
| } |
| } |
| |
| /** |
| * Detects which build system is in use and returns the appropriate ManifestFactory implementation. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @param config Specification of the SDK version, manifest file, package name, etc. |
| */ |
| protected ManifestFactory getManifestFactory(Config config) { |
| Properties buildSystemApiProperties = getBuildSystemApiProperties(); |
| if (buildSystemApiProperties != null) { |
| return new DefaultManifestFactory(buildSystemApiProperties); |
| } |
| |
| Class<?> buildConstants = config.constants(); |
| //noinspection ConstantConditions |
| if (BuckManifestFactory.isBuck()) { |
| return new BuckManifestFactory(); |
| } else if (buildConstants != null && buildConstants != Void.class) { |
| return new GradleManifestFactory(); |
| } else { |
| return new MavenManifestFactory(); |
| } |
| } |
| |
| Properties getBuildSystemApiProperties() { |
| InputStream resourceAsStream = getClass().getResourceAsStream("/com/android/tools/test_config.properties"); |
| if (resourceAsStream == null) { |
| return null; |
| } |
| |
| try { |
| Properties properties = new Properties(); |
| properties.load(resourceAsStream); |
| return properties; |
| } catch (IOException e) { |
| return null; |
| } finally { |
| try { |
| resourceAsStream.close(); |
| } catch (IOException e) { |
| // ignore |
| } |
| } |
| } |
| |
| protected AndroidManifest getAppManifest(Config config) { |
| ManifestFactory manifestFactory = getManifestFactory(config); |
| ManifestIdentifier identifier = manifestFactory.identify(config); |
| |
| synchronized (appManifestsCache) { |
| AndroidManifest appManifest; |
| appManifest = appManifestsCache.get(identifier); |
| if (appManifest == null) { |
| appManifest = manifestFactory.create(identifier); |
| appManifestsCache.put(identifier, appManifest); |
| } |
| |
| return appManifest; |
| } |
| } |
| |
| /** |
| * Compute the effective Robolectric configuration for a given test method. |
| * |
| * Configuration information is collected from package-level <tt>robolectric.properties</tt> files |
| * and {@link Config} annotations on test classes, superclasses, and methods. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide alternate configuration. |
| * |
| * @param method the test method |
| * @return the effective Robolectric configuration for the given test method |
| * @since 2.0 |
| */ |
| public Config getConfig(Method method) { |
| return configMerger.getConfig(getTestClass().getJavaClass(), method, buildGlobalConfig()); |
| } |
| |
| /** |
| * Provides the base Robolectric configuration {@link Config} used for all tests. |
| * |
| * Configuration provided for specific packages, test classes, and test method |
| * configurations will override values provided here. |
| * |
| * Custom TestRunner subclasses may wish to override this method to provide |
| * alternate configuration. Consider using a {@link Config.Builder}. |
| * |
| * The default implementation has appropriate values for most use cases. |
| * |
| * @return global {@link Config} object |
| * @since 3.1.3 |
| */ |
| protected Config buildGlobalConfig() { |
| return new Config.Builder().build(); |
| } |
| |
| @Override @Nonnull |
| protected Class<?>[] getExtraShadows(FrameworkMethod frameworkMethod) { |
| Config config = ((RobolectricFrameworkMethod) frameworkMethod).config; |
| return config.shadows(); |
| } |
| |
| ParallelUniverseInterface getHooksInterface(SdkEnvironment sdkEnvironment) { |
| ClassLoader robolectricClassLoader = sdkEnvironment.getRobolectricClassLoader(); |
| try { |
| Class<?> clazz = robolectricClassLoader.loadClass(ParallelUniverse.class.getName()); |
| Class<? extends ParallelUniverseInterface> typedClazz = clazz.asSubclass(ParallelUniverseInterface.class); |
| Constructor<? extends ParallelUniverseInterface> constructor = typedClazz.getConstructor(); |
| return constructor.newInstance(); |
| } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| protected void internalAfterTest(FrameworkMethod frameworkMethod, Method method) { |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) frameworkMethod; |
| roboMethod.testLifecycle.afterTest(method); |
| } |
| |
| @Override |
| protected void afterClass() { |
| } |
| |
| @Override |
| public Object createTest() throws Exception { |
| throw new UnsupportedOperationException("this should always be invoked on the HelperTestRunner!"); |
| } |
| |
| private PackageResourceTable getAppResourceTable(final AndroidManifest appManifest) { |
| PackageResourceTable resourceTable = appResourceTableCache.get(appManifest); |
| if (resourceTable == null) { |
| resourceTable = new ResourceMerger().buildResourceTable(appManifest); |
| |
| appResourceTableCache.put(appManifest, resourceTable); |
| } |
| return resourceTable; |
| } |
| |
| @SuppressWarnings(value = {"ImmutableAnnotationChecker", "BadAnnotationImplementation"}) |
| private static class MethodPassThrough extends Config.Implementation { |
| private final FrameworkMethod method; |
| |
| private MethodPassThrough(FrameworkMethod method, 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) { |
| super(sdk, minSdk, maxSdk, manifest, qualifiers, packageName, abiSplit, resourceDir, assetDir, buildDir, shadows, instrumentedPackages, application, libraries, constants); |
| this.method = method; |
| } |
| } |
| |
| public static class HelperTestRunner extends SandboxTestRunner.HelperTestRunner { |
| public HelperTestRunner(Class bootstrappedTestClass) throws InitializationError { |
| super(bootstrappedTestClass); |
| } |
| |
| @Override protected Object createTest() throws Exception { |
| Object test = super.createTest(); |
| RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod; |
| roboMethod.testLifecycle.prepareTest(test); |
| return test; |
| } |
| |
| @Override |
| protected Statement methodInvoker(FrameworkMethod method, Object test) { |
| final Statement invoker = super.methodInvoker(method, test); |
| final RobolectricFrameworkMethod roboMethod = (RobolectricFrameworkMethod) this.frameworkMethod; |
| return new Statement() { |
| @Override |
| public void evaluate() throws Throwable { |
| Thread orig = roboMethod.parallelUniverseInterface.getMainThread(); |
| roboMethod.parallelUniverseInterface.setMainThread(Thread.currentThread()); |
| try { |
| invoker.evaluate(); |
| } finally { |
| roboMethod.parallelUniverseInterface.setMainThread(orig); |
| } |
| } |
| }; |
| } |
| } |
| |
| static class RobolectricFrameworkMethod extends FrameworkMethod { |
| private final @Nonnull AndroidManifest appManifest; |
| final @Nonnull SdkConfig sdkConfig; |
| final @Nonnull Config config; |
| private boolean includeApiLevelInName = true; |
| TestLifecycle testLifecycle; |
| ParallelUniverseInterface parallelUniverseInterface; |
| |
| RobolectricFrameworkMethod(@Nonnull Method method, @Nonnull AndroidManifest appManifest, @Nonnull SdkConfig sdkConfig, @Nonnull Config config) { |
| super(method); |
| this.appManifest = appManifest; |
| this.sdkConfig = sdkConfig; |
| this.config = config; |
| } |
| |
| @Override |
| public String getName() { |
| // IDE focused test runs rely on preservation of the test name; we'll use the |
| // latest supported SDK for focused test runs |
| return super.getName() + |
| (includeApiLevelInName ? "[" + sdkConfig.getApiLevel() + "]" : ""); |
| } |
| |
| void dontIncludeApiLevelInName() { |
| includeApiLevelInName = false; |
| } |
| |
| @Nonnull |
| public AndroidManifest getAppManifest() { |
| return appManifest; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| if (!super.equals(o)) return false; |
| |
| RobolectricFrameworkMethod that = (RobolectricFrameworkMethod) o; |
| |
| return sdkConfig.equals(that.sdkConfig); |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = super.hashCode(); |
| result = 31 * result + sdkConfig.hashCode(); |
| return result; |
| } |
| } |
| } |