/* * Copyright (c) 2013 Mobs & Geeks * * 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 com.mobsandgeeks.adapters; import android.content.Context; import android.text.Html; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListAdapter; import android.widget.TextView; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; import java.util.HashSet; import java.util.Locale; import java.util.Set; /** * {@link InstantAdapterCore} does all the heavy lifting behind the scenes for * {@link InstantAdapter} and {@link InstantCursorAdapter}. We use composition * instead of inheritance because {@link InstantAdapter} already extends the {@link ArrayAdapter}. * * @author Ragunath Jawahar <[email protected]> * * @param <T> The model that is being backed by the {@link InstantAdapter} or * {@link InstantCursorAdapter}. */ class InstantAdapterCore<T> { // Debug static final String LOG_TAG = InstantAdapterCore.class.getSimpleName(); static final boolean DEBUG = false; // Constants private static final String EMPTY_STRING = ""; // Attributes private Context mContext; private ListAdapter mAdapter; private int mLayoutResourceId; private LayoutInflater mLayoutInflater; private Class<?> mDataType; private Set<Integer> mAnnotatedViewIds; private SparseArray<ViewHandler<T>> mViewHandlers; // Caches private SparseArray<Meta> mViewIdsAndMetaCache; private SparseArray<SimpleDateFormat> mDateFormatCache; private SparseArray<String> mFormatStringCache; /** * Constructs a new {@link InstantAdapterCore} for your {@link InstantAdapter} and * {@link InstantCursorAdapter}. * * @param context The {@link Context} to use. * @param adapter The adapter using this instance of {@link InstantAdapterCore}. * @param layoutResourceId The resource id of your XML layout. * @param dataType The data type backed by your adapter. * * @throws IllegalArgumentException If {@code context} is null or {@code layoutResourceId} is * invalid or {@code type} is {@code null}. */ public InstantAdapterCore(final Context context, final ListAdapter adapter, final int layoutResourceId, final Class<?> dataType) { if (context == null) { throw new IllegalArgumentException("'context' cannot be null."); } else if (layoutResourceId == View.NO_ID || layoutResourceId == 0) { throw new IllegalArgumentException("Invalid 'layoutResourceId', please check again."); } else if (dataType == null) { throw new IllegalArgumentException("'dataType' cannot be null."); } mContext = context; mLayoutResourceId = layoutResourceId; mLayoutInflater = LayoutInflater.from(context); mDataType = dataType; mViewHandlers = new SparseArray<ViewHandler<T>>(); mAnnotatedViewIds = new HashSet<Integer>(); mViewIdsAndMetaCache = new SparseArray<Meta>(); mDateFormatCache = new SparseArray<SimpleDateFormat>(); mFormatStringCache = new SparseArray<String>(); // Setup findAnnotatedMethods(); } /** * Method binds a POJO to the inflated View. * * @param parent The {@link View}'s parent, usually an {@link AdapterView} such as a * {@link ListView}. * @param view The associated view. * @param instance Instance backed by the adapter at the given position. * @param position The list item's position. */ public final void bindToView(final ViewGroup parent, final View view, final T instance, final int position) { SparseArray<Holder> holders = (SparseArray<Holder>) view.getTag(mLayoutResourceId); updateAnnotatedViews(holders, view, instance, position); executeViewHandlers(holders, parent, view, instance, position); } /** * Create a new view by inflating the associated XML layout. * * @param context The {@link Context} to use. * @param parent The inflated view's parent. * @return The {@link View} that was inflated from the layout. */ public final View createNewView(final Context context, final ViewGroup parent) { View view = mLayoutInflater.inflate(mLayoutResourceId, parent, false); SparseArray<Holder> holders = new SparseArray<Holder>(); int size = mViewIdsAndMetaCache.size(); for (int i = 0; i < size; i++) { int viewId = mViewIdsAndMetaCache.keyAt(i); Meta meta = mViewIdsAndMetaCache.get(viewId); View viewFromLayout = view.findViewById(viewId); if (viewFromLayout == null) { String message = String.format("Cannot find View, check the 'viewId' " + "attribute on method %s.%s()", mDataType.getName(), meta.method.getName()); throw new IllegalStateException(message); } holders.append(viewId, new Holder(viewFromLayout, meta)); mAnnotatedViewIds.add(viewId); } view.setTag(mLayoutResourceId, holders); return view; } /** * Sets an {@link ViewHandler} for a given View id. * * @param viewId Designated View id. * @param viewHandler A {@link ViewHandler} for the corresponding View. * * @throws IllegalArgumentException If evaluator is {@code null}. */ public void setViewHandler(final int viewId, final ViewHandler<T> viewHandler) { if (viewHandler == null) { throw new IllegalArgumentException("'viewHandler' cannot be null."); } mViewHandlers.put(viewId, viewHandler); } /** * Gets the {@link ViewHandler} associated with the View id. * * @param viewId The {@link View} id whose {@link ViewHandler} we are looking for. * * @return The {@link ViewHandler} or {@code null} if one does not exist. */ public ViewHandler<T> getViewHandler(final int viewId) { return mViewHandlers.get(viewId); } /** * Gets all {@link ViewHandler}s associated with this {@link InstantAdapter}. * * @return A {@link SparseArray} containing the all {@link ViewHandler}s. */ public SparseArray<ViewHandler<T>> getViewHandlers() { return mViewHandlers; } /** * Remove a {@link ViewHandler} associated with the given view id. * * @param viewId The View id whose {@link ViewHandler} has to be removed. */ public void removeViewHandler(final int viewId) { mViewHandlers.remove(viewId); } /** * Removes all {@link ViewHandler}s that are associated with this {@link InstantAdapter}. */ public void removeAllViewHandlers() { mViewHandlers.clear(); } /** * You should have used this a zillion times if you were doing it right. In case you * didn't, check this 2009 Google IO video - http://www.youtube.com/watch?v=N6YdwzAvwOA */ private static class Holder { View view; Meta meta; Holder(final View view, final Meta meta) { this.view = view; this.meta = meta; } } /** * Class holds reference to a View's annotation and it's annotated method. */ private static class Meta { Annotation annotation; Method method; Meta(final Annotation annotation, final Method method) { this.annotation = annotation; this.method = method; } } private void findAnnotatedMethods() { Class<?> clazz = mDataType; do { findAnnotatedMethods(clazz); clazz = clazz.getSuperclass(); } while (!clazz.equals(Object.class)); if (DEBUG) { Log.d(LOG_TAG, String.format("Found %d method(s)", mViewIdsAndMetaCache.size())); } } private void findAnnotatedMethods(Class<?> clazz) { if (DEBUG) { Log.d(LOG_TAG, "Looking for methods in " + clazz.getName()); } Method[] declaredMethods = clazz.getDeclaredMethods(); for (Method method : declaredMethods) { Annotation[] annotations = method.getAnnotations(); for (Annotation annotation : annotations) { if (isInstantAnnotation(annotation)) { // Assertions assertMethodIsPublic(method); assertNoParamsOrSingleContextParam(method); assertNonVoidReturnType(method); // TODO Check if view type is compatible with the annotation Meta meta = new Meta(annotation, method); if (annotation instanceof InstantText) { mViewIdsAndMetaCache.append(((InstantText) annotation).viewId(), meta); } } } } } private boolean isInstantAnnotation(final Annotation annotation) { return annotation.annotationType().equals(InstantText.class); } private void assertMethodIsPublic(final Method method) { if (!Modifier.isPublic(method.getModifiers())) { throw new IllegalStateException(String.format("%s.%s() should be public", mDataType.getSimpleName(), method.getName())); } } private void assertNoParamsOrSingleContextParam(final Method method) { Class<?>[] parameters = method.getParameterTypes(); final int nParameters = parameters.length; if (nParameters > 0) { String errorMessage = String.format("%s.%s() can have a single Context " + "parameter or should have no parameters.", mDataType.getSimpleName(), method.getName()); if (parameters.length == 1) { Class<?> parameterType = parameters[0]; if (!parameterType.isAssignableFrom(Context.class)) { throw new IllegalStateException(errorMessage); } } else if (parameters.length > 1) { throw new IllegalStateException(errorMessage); } } } private void assertNonVoidReturnType(final Method method) { if (method.getReturnType().equals(Void.TYPE)) { throw new UnsupportedOperationException( String.format("Methods with void return types cannot be annotated, " + "check %s.%s()", mDataType.getSimpleName(), method.getName())); } } private void updateAnnotatedViews(final SparseArray<Holder> holders, final View parent, final T instance, final int position) { int nHolders = holders.size(); for (int i = 0; i < nHolders; i++) { Holder holder = holders.valueAt(i); Meta meta = holder.meta; if (meta == null) continue; // ViewHandler-only views will have a null meta Object returnValue = invokeReflectedMethod(meta.method, instance); // Update view from data Class<? extends View> viewType = holder.view.getClass(); if (TextView.class.isAssignableFrom(viewType)) { updateTextView(holder, returnValue); } // Evaluators for child views ViewHandler<T> viewHandler = mViewHandlers.get(holder.view.getId()); if (viewHandler != null) { viewHandler.handleView(mAdapter, parent, holder.view, instance, position); } } } private Object invokeReflectedMethod(final Method method, final T instance) { Object returnValue = null; try { Class<?>[] parameterTypes = method.getParameterTypes(); int nParameters = parameterTypes.length; if (nParameters == 0) { returnValue = method.invoke(instance); } else if (nParameters == 1) { returnValue = method.invoke(instance, mContext); } } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return returnValue; } private void updateTextView(final Holder holder, final Object returnValue) { InstantText instantText = (InstantText) holder.meta.annotation; TextView textView = (TextView) holder.view; int viewId = textView.getId(); String text = null; if (returnValue != null) { text = applyDatePattern(viewId, instantText, returnValue); text = applyFormatString(viewId, instantText, text, returnValue); if (text == null) { text = returnValue.toString(); } } textView.setText(instantText.isHtml() ? Html.fromHtml(text) : text); } private String applyDatePattern(final int viewId, final InstantText instantText, final Object returnValue) { int index = mDateFormatCache.indexOfKey(viewId); SimpleDateFormat simpleDateFormat = null; String text = null; if (index > -1) { simpleDateFormat = mDateFormatCache.get(viewId); } else { int datePatternRes = instantText.datePatternResId(); String datePattern = datePatternRes != 0 ? mContext.getString(datePatternRes) : instantText.datePattern(); if (datePattern != null && !EMPTY_STRING.equals(datePattern)) { simpleDateFormat = new SimpleDateFormat(datePattern, Locale.getDefault()); mDateFormatCache.put(viewId, simpleDateFormat); } } if (simpleDateFormat != null) { text = simpleDateFormat.format(returnValue); } return text; } private String applyFormatString(final int viewId, final InstantText instantText, final String dateFormattedString, final Object returnValue) { int index = mFormatStringCache.indexOfKey(viewId); String formatString = null; String formatted = dateFormattedString; if (index > -1) { formatString = mFormatStringCache.get(viewId); } else { int formatStringRes = instantText.formatStringResId(); formatString = formatStringRes != 0 ? mContext.getString(formatStringRes) : instantText.formatString(); mFormatStringCache.put(viewId, formatString); } if (formatString != null && !EMPTY_STRING.equals(formatString)) { formatted = String.format(formatString, dateFormattedString != null ? dateFormattedString : returnValue); } return formatted; } private void executeViewHandlers(final SparseArray<Holder> holders, final View parent, final View view, final T instance, final int position) { int nViewHandlers = mViewHandlers.size(); for (int i = 0; i < nViewHandlers; i++) { int viewId = mViewHandlers.keyAt(i); ViewHandler<T> viewHandler = mViewHandlers.get(viewId); if (viewHandler == null) continue; if (viewId == mLayoutResourceId) { viewHandler.handleView(mAdapter, parent, view, instance, position); } else { Holder holder = holders.get(viewId); View viewWithId = null; if (holder != null) { viewWithId = holder.view; } else { viewWithId = view.findViewById(viewId); holders.append(viewId, new Holder(viewWithId, null)); } viewHandler.handleView(mAdapter, view, viewWithId, instance, position); } } } }