167 lines
6.6 KiB
Java
167 lines
6.6 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
}
|