/* * 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.
* *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.
* * *A cursor adapter XML definition starts with the
* <cursor-adapter />
* tag and may contain one or more instances of the following tags:
<select />
<bind />
The <cursor-adapter />
element defines the beginning of the
* document and supports the following attributes:
android:layout
: Reference to the XML layout to be inflated for
* each item of the adapter. This attribute is mandatory.android:selection
: Selection expression, used when the
* android:uri
attribute is defined or when the adapter is loaded with
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* This attribute is optional.android:sortOrder
: Sort expression, used when the
* android:uri
attribute is defined or when the adapter is loaded with
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* This attribute is optional.android:uri
: URI of the content provider to query to retrieve a cursor.
* Specifying this attribute is equivalent to calling
* {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
* If you call this method, the value of the XML attribute is ignored. This attribute is
* optional.In addition, you can specify one or more instances of
* <select />
and
* <bind />
tags as children
* of <cursor-adapter />
.
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:
android:column
: Name of the column to select in the cursor during the
* query operationNote: The column named _id
is always implicitly
* selected.
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:
android:from
: The name of the column to bind from.
* This attribute is mandatory. Note that @
which are not used to reference resources
* should be backslash protected as in \@
.android:to
: The id of the view to bind to. This attribute is mandatory.android:as
: The data type
* of the binding. This attribute is mandatory.In addition, a <bind />
can contain zero or more instances of
* data transformations children
* tags.
For a binding to occur the data type of the bound column/view pair must be specified. * The following data types are currently supported:
*string
: The content of the column is interpreted as a string and must be
* bound to a {@link android.widget.TextView}image
: The content of the column is interpreted as a blob describing an
* image and must be bound to an {@link android.widget.ImageView}image-uri
: The content of the column is interpreted as a URI to an image
* and must be bound to an {@link android.widget.ImageView}drawable
: The content of the column is interpreted as a resource id to a
* drawable and must be bound to an {@link android.widget.ImageView}tag
: The content of the column is interpreted as a string and will be set as
* the tag (using {@link View#setTag(Object)} of the associated View. This can be used to
* associate meta-data to your view, that can be used for instance by a listener.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:
<map />
: Maps a constant string to a string or a resource. Use
* one instance of this tag per value you want to map<transform />
: Transforms a column's value using an expression
* or an instance of {@link Adapters.CursorTransformation}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:
*android:fromValue
: The value to match from. This attribute is mandatoryandroid:toValue
: The value to match to. This value can be either a string
* or a resource identifier. This value is interpreted as a resource identifier when the
* data binding is of type drawable
. This attribute is mandatory<transform />
*A simple transform that occurs either by calling a specified class or by performing * simple text substitution. The following attributes are supported:
*android:withExpression
: The transformation expression. The expression is
* a string containing column names surrounded with curly braces { and }. During the
* transformation each column name is replaced by its value. All columns must have been
* selected in the query. An example of expression is "First name: {first_name},
* last name: {last_name}"
. This attribute is mandatory
* if android:withClass
is not specified and ignored if android:withClass
* is specifiedandroid:withClass
: A fully qualified class name corresponding to an
* implementation of {@link Adapters.CursorTransformation}. Custom
* transformations cannot be used with
* {@link android.content.Context#isRestricted() restricted contexts}, for instance in
* an app widget This attribute is mandatory if android:withExpression
is
* not specifiedThe 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> ** *
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{([^}]+)}
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