blob: 9b6518066f22bb5150e1a4f2e0ba5d0d6a4859b9 [file] [log] [blame]
/*
* Copyright (C) 2016 The Dagger Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dagger.android;
import static dagger.internal.DaggerCollections.newLinkedHashMapWithExpectedSize;
import static dagger.internal.Preconditions.checkNotNull;
import android.app.Activity;
import android.app.Fragment;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import dagger.internal.Beta;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.inject.Inject;
import javax.inject.Provider;
/**
* Performs members-injection on instances of core Android types (e.g. {@link Activity}, {@link
* Fragment}) that are constructed by the Android framework and not by Dagger. This class relies on
* an injected mapping from each concrete class to an {@link AndroidInjector.Factory} for an {@link
* AndroidInjector} of that class. Each concrete class must have its own entry in the map, even if
* it extends another class which is already present in the map. Calls {@link Object#getClass()} on
* the instance in order to find the appropriate {@link AndroidInjector.Factory}.
*
* @param <T> the core Android type to be injected
*/
@Beta
public final class DispatchingAndroidInjector<T> implements AndroidInjector<T> {
private static final String NO_SUPERTYPES_BOUND_FORMAT =
"No injector factory bound for Class<%s>";
private static final String SUPERTYPES_BOUND_FORMAT =
"No injector factory bound for Class<%1$s>. Injector factories were bound for supertypes "
+ "of %1$s: %2$s. Did you mean to bind an injector factory for the subtype?";
private final Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactories;
@Inject
DispatchingAndroidInjector(
Map<Class<?>, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithClassKeys,
Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithStringKeys) {
this.injectorFactories = merge(injectorFactoriesWithClassKeys, injectorFactoriesWithStringKeys);
}
/**
* Merges the two maps into one by transforming the values of the {@code classKeyedMap} with
* {@link Class#getName()}.
*
* <p>An SPI plugin verifies the logical uniqueness of the keysets of these two maps so we're
* assured there's no overlap.
*
* <p>Ideally we could achieve this with a generic {@code @Provides} method, but we'd need to have
* <i>N</i> modules that each extend one base module.
*/
private static <C, V> Map<String, Provider<AndroidInjector.Factory<?>>> merge(
Map<Class<? extends C>, V> classKeyedMap, Map<String, V> stringKeyedMap) {
if (classKeyedMap.isEmpty()) {
@SuppressWarnings({"unchecked", "rawtypes"})
Map<String, Provider<AndroidInjector.Factory<?>>> safeCast = (Map) stringKeyedMap;
return safeCast;
}
Map<String, V> merged =
newLinkedHashMapWithExpectedSize(classKeyedMap.size() + stringKeyedMap.size());
merged.putAll(stringKeyedMap);
for (Entry<Class<? extends C>, V> entry : classKeyedMap.entrySet()) {
merged.put(entry.getKey().getName(), entry.getValue());
}
@SuppressWarnings({"unchecked", "rawtypes"})
Map<String, Provider<AndroidInjector.Factory<?>>> safeCast = (Map) merged;
return Collections.unmodifiableMap(safeCast);
}
/**
* Attempts to perform members-injection on {@code instance}, returning {@code true} if
* successful, {@code false} otherwise.
*
* @throws InvalidInjectorBindingException if the injector factory bound for a class does not
* inject instances of that class
*/
@CanIgnoreReturnValue
public boolean maybeInject(T instance) {
Provider<AndroidInjector.Factory<?>> factoryProvider =
injectorFactories.get(instance.getClass().getName());
if (factoryProvider == null) {
return false;
}
@SuppressWarnings("unchecked")
AndroidInjector.Factory<T> factory = (AndroidInjector.Factory<T>) factoryProvider.get();
try {
AndroidInjector<T> injector =
checkNotNull(
factory.create(instance), "%s.create(I) should not return null.", factory.getClass());
injector.inject(instance);
return true;
} catch (ClassCastException e) {
throw new InvalidInjectorBindingException(
String.format(
"%s does not implement AndroidInjector.Factory<%s>",
factory.getClass().getCanonicalName(), instance.getClass().getCanonicalName()),
e);
}
}
/**
* Performs members-injection on {@code instance}.
*
* @throws InvalidInjectorBindingException if the injector factory bound for a class does not
* inject instances of that class
* @throws IllegalArgumentException if no {@link AndroidInjector.Factory} is bound for {@code
* instance}
*/
@Override
public void inject(T instance) {
boolean wasInjected = maybeInject(instance);
if (!wasInjected) {
throw new IllegalArgumentException(errorMessageSuggestions(instance));
}
}
/**
* Exception thrown if an incorrect binding is made for a {@link AndroidInjector.Factory}. If you
* see this exception, make sure the value in your {@code @ActivityKey(YourActivity.class)} or
* {@code @FragmentKey(YourFragment.class)} matches the type argument of the injector factory.
*/
@Beta
public static final class InvalidInjectorBindingException extends RuntimeException {
InvalidInjectorBindingException(String message, ClassCastException cause) {
super(message, cause);
}
}
/** Returns an error message with the class names that are supertypes of {@code instance}. */
private String errorMessageSuggestions(T instance) {
List<String> suggestions = new ArrayList<>();
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
if (injectorFactories.containsKey(clazz.getCanonicalName())) {
suggestions.add(clazz.getCanonicalName());
}
}
return suggestions.isEmpty()
? String.format(NO_SUPERTYPES_BOUND_FORMAT, instance.getClass().getCanonicalName())
: String.format(
SUPERTYPES_BOUND_FORMAT, instance.getClass().getCanonicalName(), suggestions);
}
}