/* * Copyright (C) 2010 The Android Open Source Project * * 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.example.android.xmladapters; import android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.util.AttributeSet; import android.util.Xml; import android.view.View; import android.widget.BaseAdapter; import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; /** *

This class can be used to load {@link android.widget.Adapter adapters} defined in * XML resources. XML-defined adapters can be used to easily create adapters in your * own application or to pass adapters to other processes.

* *

Types of adapters

*

Adapters defined using XML resources can only be one of the following supported * types. Arbitrary adapters are not supported to guarantee the safety of the loaded * code when adapters are loaded across packages.

* *

The complete XML format definition of each adapter type is available below.

* * *

Cursor adapter

*

A cursor adapter XML definition starts with the * <cursor-adapter /> * tag and may contain one or more instances of the following tags:

* * * *

<cursor-adapter />

*

The <cursor-adapter /> element defines the beginning of the * document and supports the following attributes:

* *

In addition, you can specify one or more instances of * <select /> and * <bind /> tags as children * of <cursor-adapter />.

* * *

<select />

*

The <select /> tag is used to select columns from the cursor * when doing the query. This can be very useful when using transformations in the * <bind /> elements. It can also be very useful if you are providing * your own binder or * transformation classes. * <select /> elements are ignored if you supply the cursor yourself.

*

The <select /> supports the following attributes:

* *

Note: The column named _id is always implicitly * selected.

* * *

<bind />

*

The <bind /> tag is used to bind a column from the cursor to * a {@link android.view.View}. A column bound using this tag is automatically selected * during the query and a matching * <select /> tag is therefore * not required.

* *

Each binding is declared as a one to one matching but * custom binder classes or special * data transformations can * allow you to bind several columns to a single view. In this case you must use the * <select /> tag to make * sure any required column is part of the query.

* *

The <bind /> tag supports the following attributes:

*
* *

In addition, a <bind /> can contain zero or more instances of * data transformations children * tags.

* * *

Binding data types

*

For a binding to occur the data type of the bound column/view pair must be specified. * The following data types are currently supported:

* * * *

Binding transformations

*

When defining a data binding you can specify an optional transformation by using one * of the following tags as a child of a <bind /> elements:

* *

While several <map /> tags can be used at the same time, you cannot * mix <map /> and <transform /> tags. If several * <transform /> tags are specified, only the last one is retained.

* * *

<map />

*

A map element simply specifies a value to match from and a value to match to. When * a column's value equals the value to match from, it is replaced with the value to match * to. The following attributes are supported:

* * *
*

<transform />

*

A simple transform that occurs either by calling a specified class or by performing * simple text substitution. The following attributes are supported:

* * *

Example

*

The following example defines a cursor adapter that queries all the contacts with * a phone number using the contacts content provider. Each contact is displayed with * its display name, its favorite status and its photo. To display photos, a custom data * binder is declared:

* *
 * <cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android"
 *     android:uri="content://com.android.contacts/contacts"
 *     android:selection="has_phone_number=1"
 *     android:layout="@layout/contact_item">
 *
 *     <bind android:from="display_name" android:to="@id/name" android:as="string" />
 *     <bind android:from="starred" android:to="@id/star" android:as="drawable">
 *         <map android:fromValue="0" android:toValue="@android:drawable/star_big_off" />
 *         <map android:fromValue="1" android:toValue="@android:drawable/star_big_on" />
 *     </bind>
 *     <bind android:from="_id" android:to="@id/name"
 *              android:as="com.google.android.test.adapters.ContactPhotoBinder" />
 *
 * </cursor-adapter>
 * 
* *

Related APIs

* * * @see android.widget.Adapter * @see android.content.ContentProvider * * attr ref android.R.styleable#CursorAdapter_layout * attr ref android.R.styleable#CursorAdapter_selection * attr ref android.R.styleable#CursorAdapter_sortOrder * attr ref android.R.styleable#CursorAdapter_uri * attr ref android.R.styleable#CursorAdapter_BindItem_as * attr ref android.R.styleable#CursorAdapter_BindItem_from * attr ref android.R.styleable#CursorAdapter_BindItem_to * attr ref android.R.styleable#CursorAdapter_MapItem_fromValue * attr ref android.R.styleable#CursorAdapter_MapItem_toValue * attr ref android.R.styleable#CursorAdapter_SelectItem_column * attr ref android.R.styleable#CursorAdapter_TransformItem_withClass * attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression */ public class Adapters { private static final String ADAPTER_CURSOR = "cursor-adapter"; /** *

Interface used to bind a {@link android.database.Cursor} column to a View. This * interface can be used to provide bindings for data types not supported by the * standard implementation of {@link Adapters}.

* *

A binder is provided with a cursor transformation which may or may not be used * to transform the value retrieved from the cursor. The transformation is guaranteed * to never be null so it's always safe to apply the transformation.

* *

The binder is associated with a Context but can be re-used with multiple cursors. * As such, the implementation should make no assumption about the Cursor in use.

* * @see android.view.View * @see android.database.Cursor * @see Adapters.CursorTransformation */ public static abstract class CursorBinder { /** *

The context associated with this binder.

*/ protected final Context mContext; /** *

The transformation associated with this binder. This transformation is never * null and may or may not be applied to the Cursor data during the * {@link #bind(android.view.View, android.database.Cursor, int)} operation.

* * @see #bind(android.view.View, android.database.Cursor, int) */ protected final CursorTransformation mTransformation; /** *

Creates a new Cursor binder.

* * @param context The context associated with this binder. * @param transformation The transformation associated with this binder. This * transformation may or may not be applied by the binder and is guaranteed * to not be null. */ public CursorBinder(Context context, CursorTransformation transformation) { mContext = context; mTransformation = transformation; } /** *

Binds the specified Cursor column to the supplied View. The binding operation * can query other Cursor columns as needed. During the binding operation, values * retrieved from the Cursor may or may not be transformed using this binder's * cursor transformation.

* * @param view The view to bind data to. * @param cursor The cursor to bind data from. * @param columnIndex The column index in the cursor where the data to bind resides. * * @see #mTransformation * * @return True if the column was successfully bound to the View, false otherwise. */ public abstract boolean bind(View view, Cursor cursor, int columnIndex); } /** *

Interface used to transform data coming out of a {@link android.database.Cursor} * before it is bound to a {@link android.view.View}.

* *

Transformations are used to transform text-based data (in the form of a String), * or to transform data into a resource identifier. A default implementation is provided * to generate resource identifiers.

* * @see android.database.Cursor * @see Adapters.CursorBinder */ public static abstract class CursorTransformation { /** *

The context associated with this transformation.

*/ protected final Context mContext; /** *

Creates a new Cursor transformation.

* * @param context The context associated with this transformation. */ public CursorTransformation(Context context) { mContext = context; } /** *

Transforms the specified Cursor column into a String. The transformation * can simply return the content of the column as a String (this is known * as the identity transformation) or manipulate the content. For instance, * a transformation can perform text substitutions or concatenate other * columns with the specified column.

* * @param cursor The cursor that contains the data to transform. * @param columnIndex The index of the column to transform. * * @return A String containing the transformed value of the column. */ public abstract String transform(Cursor cursor, int columnIndex); /** *

Transforms the specified Cursor column into a resource identifier. * The default implementation simply interprets the content of the column * as an integer.

* * @param cursor The cursor that contains the data to transform. * @param columnIndex The index of the column to transform. * * @return A resource identifier. */ public int transformToResource(Cursor cursor, int columnIndex) { return cursor.getInt(columnIndex); } } /** *

Loads the {@link android.widget.CursorAdapter} defined in the specified * XML resource. The content of the adapter is loaded from the content provider * identified by the supplied URI.

* *

Note: If the supplied {@link android.content.Context} is * an {@link android.app.Activity}, the cursor returned by the content provider * will be automatically managed. Otherwise, you are responsible for managing the * cursor yourself.

* *

The format of the XML definition of the cursor adapter is documented at * the top of this page.

* * @param context The context to load the XML resource from. * @param id The identifier of the XML resource declaring the adapter. * @param uri The URI of the content provider. * @param parameters Optional parameters to pass to the CursorAdapter, used * to substitute values in the selection expression. * * @return A {@link android.widget.CursorAdapter} * * @throws IllegalArgumentException If the XML resource does not contain * a valid <cursor-adapter /> definition. * * @see android.content.ContentProvider * @see android.widget.CursorAdapter * @see #loadAdapter(android.content.Context, int, Object[]) */ public static CursorAdapter loadCursorAdapter(Context context, int id, String uri, Object... parameters) { XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, parameters); if (uri != null) { adapter.setUri(uri); } adapter.load(); return adapter; } /** *

Loads the {@link android.widget.CursorAdapter} defined in the specified * XML resource. The content of the adapter is loaded from the specified cursor. * You are responsible for managing the supplied cursor.

* *

The format of the XML definition of the cursor adapter is documented at * the top of this page.

* * @param context The context to load the XML resource from. * @param id The identifier of the XML resource declaring the adapter. * @param cursor The cursor containing the data for the adapter. * @param parameters Optional parameters to pass to the CursorAdapter, used * to substitute values in the selection expression. * * @return A {@link android.widget.CursorAdapter} * * @throws IllegalArgumentException If the XML resource does not contain * a valid <cursor-adapter /> definition. * * @see android.content.ContentProvider * @see android.widget.CursorAdapter * @see android.database.Cursor * @see #loadAdapter(android.content.Context, int, Object[]) */ public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor, Object... parameters) { XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR, parameters); if (cursor != null) { adapter.changeCursor(cursor); } return adapter; } /** *

Loads the adapter defined in the specified XML resource. The XML definition of * the adapter must follow the format definition of one of the supported adapter * types described at the top of this page.

* *

Note: If the loaded adapter is a {@link android.widget.CursorAdapter} * and the supplied {@link android.content.Context} is an {@link android.app.Activity}, * the cursor returned by the content provider will be automatically managed. Otherwise, * you are responsible for managing the cursor yourself.

* * @param context The context to load the XML resource from. * @param id The identifier of the XML resource declaring the adapter. * @param parameters Optional parameters to pass to the adapter. * * @return An adapter instance. * * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[]) * @see #loadCursorAdapter(android.content.Context, int, String, Object[]) */ public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) { final BaseAdapter adapter = loadAdapter(context, id, null, parameters); if (adapter instanceof ManagedAdapter) { ((ManagedAdapter) adapter).load(); } return adapter; } /** * Loads an adapter from the specified XML resource. The optional assertName can * be used to exit early if the adapter defined in the XML resource is not of the * expected type. * * @param context The context to associate with the adapter. * @param id The resource id of the XML document defining the adapter. * @param assertName The mandatory name of the adapter in the XML document. * Ignored if null. * @param parameters Optional parameters passed to the adapter. * * @return An instance of {@link android.widget.BaseAdapter}. */ private static BaseAdapter loadAdapter(Context context, int id, String assertName, Object... parameters) { XmlResourceParser parser = null; try { parser = context.getResources().getXml(id); return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser), id, parameters, assertName); } catch (XmlPullParserException ex) { Resources.NotFoundException rnf = new Resources.NotFoundException( "Can't load adapter resource ID " + context.getResources().getResourceEntryName(id)); rnf.initCause(ex); throw rnf; } catch (IOException ex) { Resources.NotFoundException rnf = new Resources.NotFoundException( "Can't load adapter resource ID " + context.getResources().getResourceEntryName(id)); rnf.initCause(ex); throw rnf; } finally { if (parser != null) parser.close(); } } /** * Generates an adapter using the specified XML parser. This method is responsible * for choosing the type of the adapter to create based on the content of the * XML parser. * * This method will generate an {@link IllegalArgumentException} if * assertName is not null and does not match the root tag of the XML * document. */ private static BaseAdapter createAdapterFromXml(Context c, XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters, String assertName) throws XmlPullParserException, IOException { BaseAdapter adapter = null; // Make sure we are on a start tag. int type; int depth = parser.getDepth(); while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (assertName != null && !assertName.equals(name)) { throw new IllegalArgumentException("The adapter defined in " + c.getResources().getResourceEntryName(id) + " must be a <" + assertName + " />"); } if (ADAPTER_CURSOR.equals(name)) { adapter = createCursorAdapter(c, parser, attrs, id, parameters); } else { throw new IllegalArgumentException("Unknown adapter name " + parser.getName() + " in " + c.getResources().getResourceEntryName(id)); } } return adapter; } /** * Creates an XmlCursorAdapter using an XmlCursorAdapterParser. */ private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters) throws IOException, XmlPullParserException { return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters); } /** * Parser that can generate XmlCursorAdapter instances. This parser is responsible for * handling all the attributes and child nodes for a <cursor-adapter />. */ private static class XmlCursorAdapterParser { private static final String ADAPTER_CURSOR_BIND = "bind"; private static final String ADAPTER_CURSOR_SELECT = "select"; private static final String ADAPTER_CURSOR_AS_STRING = "string"; private static final String ADAPTER_CURSOR_AS_IMAGE = "image"; private static final String ADAPTER_CURSOR_AS_TAG = "tag"; private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri"; private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable"; private static final String ADAPTER_CURSOR_MAP = "map"; private static final String ADAPTER_CURSOR_TRANSFORM = "transform"; private final Context mContext; private final XmlPullParser mParser; private final AttributeSet mAttrs; private final int mId; private final HashMap mBinders; private final ArrayList mFrom; private final ArrayList mTo; private final CursorTransformation mIdentity; private final Resources mResources; public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) { mContext = c; mParser = parser; mAttrs = attrs; mId = id; mResources = mContext.getResources(); mBinders = new HashMap(); mFrom = new ArrayList(); mTo = new ArrayList(); mIdentity = new IdentityTransformation(mContext); } public XmlCursorAdapter parse(Object[] parameters) throws IOException, XmlPullParserException { Resources resources = mResources; TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter); String uri = a.getString(R.styleable.CursorAdapter_uri); String selection = a.getString(R.styleable.CursorAdapter_selection); String sortOrder = a.getString(R.styleable.CursorAdapter_sortOrder); int layout = a.getResourceId(R.styleable.CursorAdapter_layout, 0); if (layout == 0) { throw new IllegalArgumentException("The layout specified in " + resources.getResourceEntryName(mId) + " does not exist"); } a.recycle(); XmlPullParser parser = mParser; int type; int depth = parser.getDepth(); while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (ADAPTER_CURSOR_BIND.equals(name)) { parseBindTag(); } else if (ADAPTER_CURSOR_SELECT.equals(name)) { parseSelectTag(); } else { throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + resources.getResourceEntryName(mId)); } } String[] fromArray = mFrom.toArray(new String[mFrom.size()]); int[] toArray = new int[mTo.size()]; for (int i = 0; i < toArray.length; i++) { toArray[i] = mTo.get(i); } String[] selectionArgs = null; if (parameters != null) { selectionArgs = new String[parameters.length]; for (int i = 0; i < selectionArgs.length; i++) { selectionArgs[i] = (String) parameters[i]; } } return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection, selectionArgs, sortOrder, mBinders); } private void parseSelectTag() { TypedArray a = mResources.obtainAttributes(mAttrs, R.styleable.CursorAdapter_SelectItem); String fromName = a.getString(R.styleable.CursorAdapter_SelectItem_column); if (fromName == null) { throw new IllegalArgumentException("A select item in " + mResources.getResourceEntryName(mId) + " does not have a 'column' attribute"); } a.recycle(); mFrom.add(fromName); mTo.add(View.NO_ID); } private void parseBindTag() throws IOException, XmlPullParserException { Resources resources = mResources; TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter_BindItem); String fromName = a.getString(R.styleable.CursorAdapter_BindItem_from); if (fromName == null) { throw new IllegalArgumentException("A bind item in " + resources.getResourceEntryName(mId) + " does not have a 'from' attribute"); } int toName = a.getResourceId(R.styleable.CursorAdapter_BindItem_to, 0); if (toName == 0) { throw new IllegalArgumentException("A bind item in " + resources.getResourceEntryName(mId) + " does not have a 'to' attribute"); } String asType = a.getString(R.styleable.CursorAdapter_BindItem_as); if (asType == null) { throw new IllegalArgumentException("A bind item in " + resources.getResourceEntryName(mId) + " does not have an 'as' attribute"); } mFrom.add(fromName); mTo.add(toName); mBinders.put(fromName, findBinder(asType)); a.recycle(); } private CursorBinder findBinder(String type) throws IOException, XmlPullParserException { final XmlPullParser parser = mParser; final Context context = mContext; CursorTransformation transformation = mIdentity; int tagType; int depth = parser.getDepth(); final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type); while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && tagType != XmlPullParser.END_DOCUMENT) { if (tagType != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (ADAPTER_CURSOR_TRANSFORM.equals(name)) { transformation = findTransformation(); } else if (ADAPTER_CURSOR_MAP.equals(name)) { if (!(transformation instanceof MapTransformation)) { transformation = new MapTransformation(context); } findMap(((MapTransformation) transformation), isDrawable); } else { throw new RuntimeException("Unknown tag name " + parser.getName() + " in " + context.getResources().getResourceEntryName(mId)); } } if (ADAPTER_CURSOR_AS_STRING.equals(type)) { return new StringBinder(context, transformation); } else if (ADAPTER_CURSOR_AS_TAG.equals(type)) { return new TagBinder(context, transformation); } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) { return new ImageBinder(context, transformation); } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) { return new ImageUriBinder(context, transformation); } else if (isDrawable) { return new DrawableBinder(context, transformation); } else { return createBinder(type, transformation); } } private CursorBinder createBinder(String type, CursorTransformation transformation) { if (mContext.isRestricted()) return null; try { final Class klass = Class.forName(type, true, mContext.getClassLoader()); if (CursorBinder.class.isAssignableFrom(klass)) { final Constructor c = klass.getDeclaredConstructor( Context.class, CursorTransformation.class); return (CursorBinder) c.newInstance(mContext, transformation); } } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Cannot instanciate binder type in " + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Cannot instanciate binder type in " + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); } catch (InvocationTargetException e) { throw new IllegalArgumentException("Cannot instanciate binder type in " + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); } catch (InstantiationException e) { throw new IllegalArgumentException("Cannot instanciate binder type in " + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Cannot instanciate binder type in " + mContext.getResources().getResourceEntryName(mId) + ": " + type, e); } return null; } private void findMap(MapTransformation transformation, boolean drawable) { Resources resources = mResources; TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter_MapItem); String from = a.getString(R.styleable.CursorAdapter_MapItem_fromValue); if (from == null) { throw new IllegalArgumentException("A map item in " + resources.getResourceEntryName(mId) + " does not have a 'fromValue' attribute"); } if (!drawable) { String to = a.getString(R.styleable.CursorAdapter_MapItem_toValue); if (to == null) { throw new IllegalArgumentException("A map item in " + resources.getResourceEntryName(mId) + " does not have a 'toValue' attribute"); } transformation.addStringMapping(from, to); } else { int to = a.getResourceId(R.styleable.CursorAdapter_MapItem_toValue, 0); if (to == 0) { throw new IllegalArgumentException("A map item in " + resources.getResourceEntryName(mId) + " does not have a 'toValue' attribute"); } transformation.addResourceMapping(from, to); } a.recycle(); } private CursorTransformation findTransformation() { Resources resources = mResources; CursorTransformation transformation = null; TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter_TransformItem); String className = a.getString(R.styleable.CursorAdapter_TransformItem_withClass); if (className == null) { String expression = a.getString( R.styleable.CursorAdapter_TransformItem_withExpression); transformation = createExpressionTransformation(expression); } else if (!mContext.isRestricted()) { try { final Class klas = Class.forName(className, true, mContext.getClassLoader()); if (CursorTransformation.class.isAssignableFrom(klas)) { final Constructor c = klas.getDeclaredConstructor(Context.class); transformation = (CursorTransformation) c.newInstance(mContext); } } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Cannot instanciate transform type in " + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Cannot instanciate transform type in " + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); } catch (InvocationTargetException e) { throw new IllegalArgumentException("Cannot instanciate transform type in " + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); } catch (InstantiationException e) { throw new IllegalArgumentException("Cannot instanciate transform type in " + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); } catch (IllegalAccessException e) { throw new IllegalArgumentException("Cannot instanciate transform type in " + mContext.getResources().getResourceEntryName(mId) + ": " + className, e); } } a.recycle(); if (transformation == null) { throw new IllegalArgumentException("A transform item in " + resources.getResourceEntryName(mId) + " must have a 'withClass' or " + "'withExpression' attribute"); } return transformation; } private CursorTransformation createExpressionTransformation(String expression) { return new ExpressionTransformation(mContext, expression); } } /** * Interface used by adapters that require to be loaded after creation. */ private static interface ManagedAdapter { /** * Loads the content of the adapter, asynchronously. */ void load(); } /** * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders. */ private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter { private Context mContext; private String mUri; private final String mSelection; private final String[] mSelectionArgs; private final String mSortOrder; private final int[] mTo; private final String[] mFrom; private final String[] mColumns; private final CursorBinder[] mBinders; private AsyncTask mLoadTask; XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to, String selection, String[] selectionArgs, String sortOrder, HashMap binders) { super(context, layout, null, from, to); mContext = context; mUri = uri; mFrom = from; mTo = to; mSelection = selection; mSelectionArgs = selectionArgs; mSortOrder = sortOrder; mColumns = new String[from.length + 1]; // This is mandatory in CursorAdapter mColumns[0] = "_id"; System.arraycopy(from, 0, mColumns, 1, from.length); CursorBinder basic = new StringBinder(context, new IdentityTransformation(context)); final int count = from.length; mBinders = new CursorBinder[count]; for (int i = 0; i < count; i++) { CursorBinder binder = binders.get(from[i]); if (binder == null) binder = basic; mBinders[i] = binder; } } @Override public void bindView(View view, Context context, Cursor cursor) { final int count = mTo.length; final int[] to = mTo; final CursorBinder[] binders = mBinders; for (int i = 0; i < count; i++) { final View v = view.findViewById(to[i]); if (v != null) { // Not optimal, the column index could be cached binders[i].bind(v, cursor, cursor.getColumnIndex(mFrom[i])); } } } public void load() { if (mUri != null) { mLoadTask = new QueryTask().execute(); } } void setUri(String uri) { mUri = uri; } @Override public void changeCursor(Cursor c) { if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) { mLoadTask.cancel(true); mLoadTask = null; } super.changeCursor(c); } class QueryTask extends AsyncTask { @Override protected Cursor doInBackground(Void... params) { if (mContext instanceof Activity) { return ((Activity) mContext).managedQuery( Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); } else { return mContext.getContentResolver().query( Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder); } } @Override protected void onPostExecute(Cursor cursor) { if (!isCancelled()) { XmlCursorAdapter.super.changeCursor(cursor); } } } } /** * Identity transformation, returns the content of the specified column as a String, * without performing any manipulation. This is used when no transformation is specified. */ private static class IdentityTransformation extends CursorTransformation { public IdentityTransformation(Context context) { super(context); } @Override public String transform(Cursor cursor, int columnIndex) { return cursor.getString(columnIndex); } } /** * An expression transformation is a simple template based replacement utility. * In an expression, each segment of the form {([^}]+)} is replaced * with the value of the column of name $1. */ private static class ExpressionTransformation extends CursorTransformation { private final ExpressionNode mFirstNode = new ConstantExpressionNode(""); private final StringBuilder mBuilder = new StringBuilder(); public ExpressionTransformation(Context context, String expression) { super(context); parse(expression); } private void parse(String expression) { ExpressionNode node = mFirstNode; int segmentStart; int count = expression.length(); for (int i = 0; i < count; i++) { char c = expression.charAt(i); // Start a column name segment segmentStart = i; if (c == '{') { while (i < count && (c = expression.charAt(i)) != '}') { i++; } // We've reached the end, but the expression didn't close if (c != '}') { throw new IllegalStateException("The transform expression contains a " + "non-closed column name: " + expression.substring(segmentStart + 1, i)); } node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i)); } else { while (i < count && (c = expression.charAt(i)) != '{') { i++; } node.next = new ConstantExpressionNode(expression.substring(segmentStart, i)); // Rewind if we've reached a column expression if (c == '{') i--; } node = node.next; } } @Override public String transform(Cursor cursor, int columnIndex) { final StringBuilder builder = mBuilder; builder.delete(0, builder.length()); ExpressionNode node = mFirstNode; // Skip the first node while ((node = node.next) != null) { builder.append(node.asString(cursor)); } return builder.toString(); } static abstract class ExpressionNode { public ExpressionNode next; public abstract String asString(Cursor cursor); } static class ConstantExpressionNode extends ExpressionNode { private final String mConstant; ConstantExpressionNode(String constant) { mConstant = constant; } @Override public String asString(Cursor cursor) { return mConstant; } } static class ColumnExpressionNode extends ExpressionNode { private final String mColumnName; private Cursor mSignature; private int mColumnIndex = -1; ColumnExpressionNode(String columnName) { mColumnName = columnName; } @Override public String asString(Cursor cursor) { if (cursor != mSignature || mColumnIndex == -1) { mColumnIndex = cursor.getColumnIndex(mColumnName); mSignature = cursor; } return cursor.getString(mColumnIndex); } } } /** * A map transformation offers a simple mapping between specified String values * to Strings or integers. */ private static class MapTransformation extends CursorTransformation { private final HashMap mStringMappings; private final HashMap mResourceMappings; public MapTransformation(Context context) { super(context); mStringMappings = new HashMap(); mResourceMappings = new HashMap(); } void addStringMapping(String from, String to) { mStringMappings.put(from, to); } void addResourceMapping(String from, int to) { mResourceMappings.put(from, to); } @Override public String transform(Cursor cursor, int columnIndex) { final String value = cursor.getString(columnIndex); final String transformed = mStringMappings.get(value); return transformed == null ? value : transformed; } @Override public int transformToResource(Cursor cursor, int columnIndex) { final String value = cursor.getString(columnIndex); final Integer transformed = mResourceMappings.get(value); try { return transformed == null ? Integer.parseInt(value) : transformed; } catch (NumberFormatException e) { return 0; } } } /** * Binds a String to a TextView. */ private static class StringBinder extends CursorBinder { public StringBinder(Context context, CursorTransformation transformation) { super(context, transformation); } @Override public boolean bind(View view, Cursor cursor, int columnIndex) { if (view instanceof TextView) { final String text = mTransformation.transform(cursor, columnIndex); ((TextView) view).setText(text); return true; } return false; } } /** * Binds an image blob to an ImageView. */ private static class ImageBinder extends CursorBinder { public ImageBinder(Context context, CursorTransformation transformation) { super(context, transformation); } @Override public boolean bind(View view, Cursor cursor, int columnIndex) { if (view instanceof ImageView) { final byte[] data = cursor.getBlob(columnIndex); ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0, data.length)); return true; } return false; } } private static class TagBinder extends CursorBinder { public TagBinder(Context context, CursorTransformation transformation) { super(context, transformation); } @Override public boolean bind(View view, Cursor cursor, int columnIndex) { final String text = mTransformation.transform(cursor, columnIndex); view.setTag(text); return true; } } /** * Binds an image URI to an ImageView. */ private static class ImageUriBinder extends CursorBinder { public ImageUriBinder(Context context, CursorTransformation transformation) { super(context, transformation); } @Override public boolean bind(View view, Cursor cursor, int columnIndex) { if (view instanceof ImageView) { ((ImageView) view).setImageURI(Uri.parse( mTransformation.transform(cursor, columnIndex))); return true; } return false; } } /** * Binds a drawable resource identifier to an ImageView. */ private static class DrawableBinder extends CursorBinder { public DrawableBinder(Context context, CursorTransformation transformation) { super(context, transformation); } @Override public boolean bind(View view, Cursor cursor, int columnIndex) { if (view instanceof ImageView) { final int resource = mTransformation.transformToResource(cursor, columnIndex); if (resource == 0) return false; ((ImageView) view).setImageResource(resource); return true; } return false; } } }