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