1 /*
2  * Copyright (C) 2016 The Dagger Authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package dagger.android;
18 
19 import static dagger.internal.DaggerCollections.newLinkedHashMapWithExpectedSize;
20 import static dagger.internal.Preconditions.checkNotNull;
21 
22 import android.app.Activity;
23 import android.app.Fragment;
24 import com.google.errorprone.annotations.CanIgnoreReturnValue;
25 import dagger.internal.Beta;
26 import java.util.ArrayList;
27 import java.util.Collections;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Map.Entry;
31 import javax.inject.Inject;
32 import javax.inject.Provider;
33 
34 /**
35  * Performs members-injection on instances of core Android types (e.g. {@link Activity}, {@link
36  * Fragment}) that are constructed by the Android framework and not by Dagger. This class relies on
37  * an injected mapping from each concrete class to an {@link AndroidInjector.Factory} for an {@link
38  * AndroidInjector} of that class. Each concrete class must have its own entry in the map, even if
39  * it extends another class which is already present in the map. Calls {@link Object#getClass()} on
40  * the instance in order to find the appropriate {@link AndroidInjector.Factory}.
41  *
42  * @param <T> the core Android type to be injected
43  */
44 @Beta
45 public final class DispatchingAndroidInjector<T> implements AndroidInjector<T> {
46   private static final String NO_SUPERTYPES_BOUND_FORMAT =
47       "No injector factory bound for Class<%s>";
48   private static final String SUPERTYPES_BOUND_FORMAT =
49       "No injector factory bound for Class<%1$s>. Injector factories were bound for supertypes "
50           + "of %1$s: %2$s. Did you mean to bind an injector factory for the subtype?";
51 
52   private final Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactories;
53 
54   @Inject
DispatchingAndroidInjector( Map<Class<?>, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithClassKeys, Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithStringKeys)55   DispatchingAndroidInjector(
56       Map<Class<?>, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithClassKeys,
57       Map<String, Provider<AndroidInjector.Factory<?>>> injectorFactoriesWithStringKeys) {
58     this.injectorFactories = merge(injectorFactoriesWithClassKeys, injectorFactoriesWithStringKeys);
59   }
60 
61   /**
62    * Merges the two maps into one by transforming the values of the {@code classKeyedMap} with
63    * {@link Class#getName()}.
64    *
65    * <p>An SPI plugin verifies the logical uniqueness of the keysets of these two maps so we're
66    * assured there's no overlap.
67    *
68    * <p>Ideally we could achieve this with a generic {@code @Provides} method, but we'd need to have
69    * <i>N</i> modules that each extend one base module.
70    */
merge( Map<Class<? extends C>, V> classKeyedMap, Map<String, V> stringKeyedMap)71   private static <C, V> Map<String, Provider<AndroidInjector.Factory<?>>> merge(
72       Map<Class<? extends C>, V> classKeyedMap, Map<String, V> stringKeyedMap) {
73     if (classKeyedMap.isEmpty()) {
74       @SuppressWarnings({"unchecked", "rawtypes"})
75       Map<String, Provider<AndroidInjector.Factory<?>>> safeCast = (Map) stringKeyedMap;
76       return safeCast;
77     }
78 
79     Map<String, V> merged =
80         newLinkedHashMapWithExpectedSize(classKeyedMap.size() + stringKeyedMap.size());
81     merged.putAll(stringKeyedMap);
82     for (Entry<Class<? extends C>, V> entry : classKeyedMap.entrySet()) {
83       merged.put(entry.getKey().getName(), entry.getValue());
84     }
85 
86     @SuppressWarnings({"unchecked", "rawtypes"})
87     Map<String, Provider<AndroidInjector.Factory<?>>> safeCast = (Map) merged;
88     return Collections.unmodifiableMap(safeCast);
89   }
90 
91   /**
92    * Attempts to perform members-injection on {@code instance}, returning {@code true} if
93    * successful, {@code false} otherwise.
94    *
95    * @throws InvalidInjectorBindingException if the injector factory bound for a class does not
96    *     inject instances of that class
97    */
98   @CanIgnoreReturnValue
maybeInject(T instance)99   public boolean maybeInject(T instance) {
100     Provider<AndroidInjector.Factory<?>> factoryProvider =
101         injectorFactories.get(instance.getClass().getName());
102     if (factoryProvider == null) {
103       return false;
104     }
105 
106     @SuppressWarnings("unchecked")
107     AndroidInjector.Factory<T> factory = (AndroidInjector.Factory<T>) factoryProvider.get();
108     try {
109       AndroidInjector<T> injector =
110           checkNotNull(
111               factory.create(instance), "%s.create(I) should not return null.", factory.getClass());
112 
113       injector.inject(instance);
114       return true;
115     } catch (ClassCastException e) {
116       throw new InvalidInjectorBindingException(
117           String.format(
118               "%s does not implement AndroidInjector.Factory<%s>",
119               factory.getClass().getCanonicalName(), instance.getClass().getCanonicalName()),
120           e);
121     }
122   }
123 
124   /**
125    * Performs members-injection on {@code instance}.
126    *
127    * @throws InvalidInjectorBindingException if the injector factory bound for a class does not
128    *     inject instances of that class
129    * @throws IllegalArgumentException if no {@link AndroidInjector.Factory} is bound for {@code
130    *     instance}
131    */
132   @Override
inject(T instance)133   public void inject(T instance) {
134     boolean wasInjected = maybeInject(instance);
135     if (!wasInjected) {
136       throw new IllegalArgumentException(errorMessageSuggestions(instance));
137     }
138   }
139 
140   /**
141    * Exception thrown if an incorrect binding is made for a {@link AndroidInjector.Factory}. If you
142    * see this exception, make sure the value in your {@code @ActivityKey(YourActivity.class)} or
143    * {@code @FragmentKey(YourFragment.class)} matches the type argument of the injector factory.
144    */
145   @Beta
146   public static final class InvalidInjectorBindingException extends RuntimeException {
InvalidInjectorBindingException(String message, ClassCastException cause)147     InvalidInjectorBindingException(String message, ClassCastException cause) {
148       super(message, cause);
149     }
150   }
151 
152   /** Returns an error message with the class names that are supertypes of {@code instance}. */
errorMessageSuggestions(T instance)153   private String errorMessageSuggestions(T instance) {
154     List<String> suggestions = new ArrayList<>();
155     for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
156       if (injectorFactories.containsKey(clazz.getCanonicalName())) {
157         suggestions.add(clazz.getCanonicalName());
158       }
159     }
160 
161     return suggestions.isEmpty()
162         ? String.format(NO_SUPERTYPES_BOUND_FORMAT, instance.getClass().getCanonicalName())
163         : String.format(
164             SUPERTYPES_BOUND_FORMAT, instance.getClass().getCanonicalName(), suggestions);
165   }
166 }
167