/*
* Copyright 2009 Michael Burton
*
* 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 roboguice.inject;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Set;
import java.util.WeakHashMap;
import javax.inject.Singleton;
import roboguice.fragment.FragmentUtil;
import roboguice.fragment.FragmentUtil.f;
import com.google.inject.Guice;
import com.google.inject.MembersInjector;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.HierarchyTraversalFilter;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import android.app.Activity;
import android.content.Context;
import android.view.View;
@Singleton
@SuppressWarnings("unchecked")
public class ViewListener implements TypeListener {
private HierarchyTraversalFilter filter;
@Override
public <I> void hear(TypeLiteral<I> typeLiteral, TypeEncounter<I> typeEncounter) {
if( filter == null ) {
filter = Guice.createHierarchyTraversalFilter();
} else {
filter.reset();
}
Class<?> c = typeLiteral.getRawType();
while( isWorthScanning(c)) {
Set<Field> allFields = null;
allFields = filter.getAllFields(InjectView.class.getName(), c);
if( allFields != null ) {
for (Field field : allFields) {
prepareViewMembersInjector(typeEncounter, field);
}
}
//TODO
//right now those loops could be merged. But it would be interesting
//to see if ViewMembersInjector should not be more distinguished
//by introducing a FragmentMembersInjector
allFields = filter.getAllFields(InjectFragment.class.getName(), c);
if( allFields != null ) {
for (Field field : allFields) {
prepareViewMembersInjector(typeEncounter, field);
}
}
c = c.getSuperclass();
}
}
private <I> void prepareViewMembersInjector(TypeEncounter<I> typeEncounter, Field field) {
if (field.isAnnotationPresent(InjectView.class)) {
if (Modifier.isStatic(field.getModifiers()))
throw new UnsupportedOperationException("Views may not be statically injected");
else if (!View.class.isAssignableFrom(field.getType()))
throw new UnsupportedOperationException("You may only use @InjectView on fields that extend View");
else if (Context.class.isAssignableFrom(field.getDeclaringClass()) && !Activity.class.isAssignableFrom(field.getDeclaringClass()))
throw new UnsupportedOperationException("You may only use @InjectView in Activity contexts");
else {
final f<?,?> utils = FragmentUtil.hasSupport
&& (FragmentUtil.supportActivity.isAssignableFrom(field.getDeclaringClass())
|| FragmentUtil.supportFrag.fragmentType().isAssignableFrom(field.getDeclaringClass()))
? FragmentUtil.supportFrag : FragmentUtil.nativeFrag;
typeEncounter.register(new ViewMembersInjector<I>(
field, field.getAnnotation(InjectView.class),
typeEncounter, utils));
}
} else if (field.isAnnotationPresent(InjectFragment.class)) {
if (!FragmentUtil.hasNative && !FragmentUtil.hasSupport) {
throw new RuntimeException(new ClassNotFoundException("No fragment classes were available"));
} else if (Modifier.isStatic(field.getModifiers())) {
throw new UnsupportedOperationException("Fragments may not be statically injected");
} else {
final boolean assignableFromNative = FragmentUtil.hasNative && FragmentUtil.nativeFrag.fragmentType().isAssignableFrom(field.getType());
final boolean assignableFromSupport = FragmentUtil.hasSupport && FragmentUtil.supportFrag.fragmentType().isAssignableFrom(field.getType());
final boolean isSupportActivity = FragmentUtil.hasSupport && FragmentUtil.supportActivity.isAssignableFrom(field.getDeclaringClass());
final boolean isNativeActivity = !isSupportActivity && Activity.class.isAssignableFrom(field.getDeclaringClass());
if (isNativeActivity && assignableFromNative || isSupportActivity && assignableFromSupport) {
typeEncounter.register(new ViewMembersInjector<I>(field, field.getAnnotation(InjectFragment.class), typeEncounter, isNativeActivity ? FragmentUtil.nativeFrag:FragmentUtil.supportFrag));
} else if (isNativeActivity && !assignableFromNative) {
// Error messages - these filters are comprehensive. The
// final else block will never execute.
throw new UnsupportedOperationException(
"You may only use @InjectFragment in native activities if fields are descended from type android.app.Fragment");
} else if (!isSupportActivity && !isNativeActivity) {
throw new UnsupportedOperationException("You may only use @InjectFragment in Activity contexts");
} else if (isSupportActivity && !assignableFromSupport) {
throw new UnsupportedOperationException(
"You may only use @InjectFragment in support activities if fields are descended from type android.support.v4.app.Fragment");
} else {
throw new RuntimeException("This should never happen.");
}
}
}
}
private boolean isWorthScanning(Class<?> c) {
return filter.isWorthScanningForFields(InjectView.class.getName(), c)
|| filter.isWorthScanningForFields(InjectFragment.class.getName(), c);
}
/**
* This class gets twice as many providers as necessary to do its job, look into optimizations in the future if this is a bottleneck
*/
public static class ViewMembersInjector<T> implements MembersInjector<T> {
@edu.umd.cs.findbugs.annotations.SuppressWarnings("MS_SHOULD_BE_FINAL")
protected static WeakHashMap<Object,ArrayList<ViewMembersInjector<?>>> viewMembersInjectors = new WeakHashMap<Object, ArrayList<ViewMembersInjector<?>>>();
protected Field field;
protected Annotation annotation;
protected WeakReference<T> instanceRef;
@SuppressWarnings("rawtypes")
protected FragmentUtil.f fragUtils;
@SuppressWarnings("rawtypes")
protected Provider fragManager;
protected Provider<Activity> activityProvider;
public ViewMembersInjector(Field field, Annotation annotation, TypeEncounter<T> typeEncounter, FragmentUtil.f<?,?> utils) {
this.field = field;
this.annotation = annotation;
this.activityProvider = typeEncounter.getProvider(Activity.class);
if( utils !=null ) {
this.fragUtils = utils;
this.fragManager = typeEncounter.getProvider(utils.fragmentManagerType());
}
}
/**
* This is called when instance is injected by guice. Because the views may or may not be set up yet,
* we don't do the real view injection until later.
*
* @param instance the instance being injected by guice
*/
public void injectMembers(T instance) {
synchronized (ViewMembersInjector.class) {
final Activity activity = activityProvider.get();
boolean isValidFragment = fragUtils != null && fragUtils.fragmentType().isInstance(instance);
final Object key = isValidFragment || instance instanceof View ? instance : activity;
if( key==null )
return;
// Add a view injector for the key
ArrayList<ViewMembersInjector<?>> injectors = viewMembersInjectors.get(key);
if( injectors==null ) {
injectors = new ArrayList<ViewMembersInjector<?>>();
viewMembersInjectors.put(key, injectors);
}
injectors.add(this);
this.instanceRef = new WeakReference<T>(instance);
}
}
public void reallyInjectMembers( Object activityOrFragment ) {
if( annotation instanceof InjectView )
reallyInjectMemberViews(activityOrFragment);
else
reallyInjectMemberFragments(activityOrFragment);
}
/**
* This is when the view references are actually evaluated.
* @param target an activity or fragment or a view.
*/
protected void reallyInjectMemberViews(Object target) {
boolean isValidFragment = fragUtils != null && fragUtils.fragmentType().isInstance(target);
final T instance = isValidFragment ? (T)target : instanceRef.get();
if( instance==null )
return;
View view = null;
final InjectView injectView = (InjectView) annotation;
final int id = injectView.value();
//contains the view to inject, as a layout container, not nceseraily a data member.
View containerView = null;
containerView = extractContainerView(target, isValidFragment);
if( id>=0 ) {
view = containerView.findViewById(id);
} else {
view = containerView.findViewWithTag(injectView.tag());
}
if (view == null && Nullable.notNullable(field))
throw new NullPointerException(String.format("Can't inject null value into %s.%s when field is not @Nullable", field.getDeclaringClass(), field.getName()));
try {
field.setAccessible(true);
field.set(instance, view);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException f) {
throw new IllegalArgumentException(String.format("Can't assign %s value %s to %s field %s", view != null ? view.getClass() : "(null)", view,
field.getType(), field.getName()), f);
}
}
private View extractContainerView(Object target, boolean isValidFragment) {
View containerView;
if( isValidFragment ) {
containerView = fragUtils.getView(target);
} else if( target instanceof View ) {
containerView = (View) target;
} else if( target instanceof Activity ) {
//it must be an activity so
containerView = ((Activity)target).getWindow().getDecorView();
} else {
throw new UnsupportedOperationException("Can't inject view into something that is not a Fragment, Activity or View.");
}
return containerView;
}
/**
* This is when the view references are actually evaluated.
* @param activityOrFragment an activity or fragment
*/
protected void reallyInjectMemberFragments(Object activityOrFragment) {
final T instance = instanceRef.get();
if( instance==null )
return;
if( activityOrFragment instanceof Context && !(activityOrFragment instanceof Activity ))
throw new UnsupportedOperationException("Can't inject fragment into a non-Activity context");
Object fragment = null;
try {
final InjectFragment injectFragment = (InjectFragment) annotation;
final int id = injectFragment.value();
if( id>=0 )
fragment = fragUtils.findFragmentById(fragManager.get(), id);
else
fragment = fragUtils.findFragmentByTag(fragManager.get(),injectFragment.tag());
if (fragment == null && Nullable.notNullable(field))
throw new NullPointerException(String.format("Can't inject null value into %s.%s when field is not @Nullable", field.getDeclaringClass(), field.getName()));
field.setAccessible(true);
field.set(instance, fragment);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException f) {
throw new IllegalArgumentException(String.format("Can't assign %s value %s to %s field %s", fragment != null ? fragment.getClass() : "(null)", fragment,
field.getType(), field.getName()), f);
}
}
protected static void injectViews(Object activityOrFragment) {
synchronized ( ViewMembersInjector.class ) {
final ArrayList<ViewMembersInjector<?>> injectors = viewMembersInjectors.get(activityOrFragment);
if(injectors!=null)
for(ViewMembersInjector<?> viewMembersInjector : injectors)
viewMembersInjector.reallyInjectMembers(activityOrFragment);
}
}
}
}