diff --git a/samples/XmlAdapters/Android.mk b/samples/XmlAdapters/Android.mk
index 3224bbead..a2cad3a3a 100644
--- a/samples/XmlAdapters/Android.mk
+++ b/samples/XmlAdapters/Android.mk
@@ -6,12 +6,11 @@ LOCAL_MODULE_TAGS := samples
# Only compile source java files in this apk.
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_PACKAGE_NAME := XmlAdaptersSample
+LOCAL_PACKAGE_NAME := xmladapters
LOCAL_PROGUARD_ENABLED := disabled
-# XXX These APIs are not yet available in the platform.
-#include $(BUILD_PACKAGE)
+include $(BUILD_PACKAGE)
# Use the following include to make our test apk.
#include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/samples/XmlAdapters/res/drawable-mdpi/ic_contact_picture.png b/samples/XmlAdapters/res/drawable-mdpi/ic_contact_picture.png
new file mode 100644
index 000000000..f6032f1fa
Binary files /dev/null and b/samples/XmlAdapters/res/drawable-mdpi/ic_contact_picture.png differ
diff --git a/samples/XmlAdapters/res/layout/contacts_list.xml b/samples/XmlAdapters/res/layout/contacts_list.xml
index 973fe4be1..0dcc0194b 100644
--- a/samples/XmlAdapters/res/layout/contacts_list.xml
+++ b/samples/XmlAdapters/res/layout/contacts_list.xml
@@ -19,7 +19,6 @@
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