1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.example.android.xmladapters;
18
19import android.app.Activity;
20import android.content.Context;
21import android.content.res.Resources;
22import android.content.res.TypedArray;
23import android.content.res.XmlResourceParser;
24import android.database.Cursor;
25import android.graphics.BitmapFactory;
26import android.net.Uri;
27import android.os.AsyncTask;
28import android.util.AttributeSet;
29import android.util.Xml;
30import android.view.View;
31import android.widget.BaseAdapter;
32import android.widget.CursorAdapter;
33import android.widget.ImageView;
34import android.widget.SimpleCursorAdapter;
35import android.widget.TextView;
36
37import org.xmlpull.v1.XmlPullParser;
38import org.xmlpull.v1.XmlPullParserException;
39
40import java.io.IOException;
41import java.lang.reflect.Constructor;
42import java.lang.reflect.InvocationTargetException;
43import java.util.ArrayList;
44import java.util.HashMap;
45
46/**
47 * <p>This class can be used to load {@link android.widget.Adapter adapters} defined in
48 * XML resources. XML-defined adapters can be used to easily create adapters in your
49 * own application or to pass adapters to other processes.</p>
50 *
51 * <h2>Types of adapters</h2>
52 * <p>Adapters defined using XML resources can only be one of the following supported
53 * types. Arbitrary adapters are not supported to guarantee the safety of the loaded
54 * code when adapters are loaded across packages.</p>
55 * <ul>
56 *  <li><a href="#xml-cursor-adapter">Cursor adapter</a>: a cursor adapter can be used
57 *  to display the content of a cursor, most often coming from a content provider</li>
58 * </ul>
59 * <p>The complete XML format definition of each adapter type is available below.</p>
60 *
61 * <a name="xml-cursor-adapter"></a>
62 * <h2>Cursor adapter</h2>
63 * <p>A cursor adapter XML definition starts with the
64 * <a href="#xml-cursor-adapter-tag"><code>&lt;cursor-adapter /&gt;</code></a>
65 * tag and may contain one or more instances of the following tags:</p>
66 * <ul>
67 *  <li><a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code></a></li>
68 *  <li><a href="#xml-cursor-adapter-bind-tag"><code>&lt;bind /&gt;</code></a></li>
69 * </ul>
70 *
71 * <a name="xml-cursor-adapter-tag"></a>
72 * <h3>&lt;cursor-adapter /&gt;</h3>
73 * <p>The <code>&lt;cursor-adapter /&gt;</code> element defines the beginning of the
74 * document and supports the following attributes:</p>
75 * <ul>
76 *  <li><code>android:layout</code>: Reference to the XML layout to be inflated for
77 *  each item of the adapter. This attribute is mandatory.</li>
78 *  <li><code>android:selection</code>: Selection expression, used when the
79 *  <code>android:uri</code> attribute is defined or when the adapter is loaded with
80 *  {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
81 *  This attribute is optional.</li>
82 *  <li><code>android:sortOrder</code>: Sort expression, used when the
83 *  <code>android:uri</code> attribute is defined or when the adapter is loaded with
84 *  {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
85 *  This attribute is optional.</li>
86 *  <li><code>android:uri</code>: URI of the content provider to query to retrieve a cursor.
87 *  Specifying this attribute is equivalent to calling
88 *  {@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}.
89 *  If you call this method, the value of the XML attribute is ignored. This attribute is
90 *  optional.</li>
91 * </ul>
92 * <p>In addition, you can specify one or more instances of
93 * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code></a> and
94 * <a href="#xml-cursor-adapter-bind-tag"><code>&lt;bind /&gt;</code></a> tags as children
95 * of <code>&lt;cursor-adapter /&gt;</code>.</p>
96 *
97 * <a name="xml-cursor-adapter-select-tag"></a>
98 * <h3>&lt;select /&gt;</h3>
99 * <p>The <code>&lt;select /&gt;</code> tag is used to select columns from the cursor
100 * when doing the query. This can be very useful when using transformations in the
101 * <code>&lt;bind /&gt;</code> elements. It can also be very useful if you are providing
102 * your own <a href="#xml-cursor-adapter-bind-data-types">binder</a> or
103 * <a href="#xml-cursor-adapter-bind-data-types">transformation</a> classes.
104 * <code>&lt;select /&gt;</code> elements are ignored if you supply the cursor yourself.</p>
105 * <p>The <code>&lt;select /&gt;</code> supports the following attributes:</p>
106 * <ul>
107 *  <li><code>android:column</code>: Name of the column to select in the cursor during the
108 *  query operation</li>
109 * </ul>
110 * <p><strong>Note:</strong> The column named <code>_id</code> is always implicitly
111 * selected.</p>
112 *
113 * <a name="xml-cursor-adapter-bind-tag"></a>
114 * <h3>&lt;bind /&gt;</h3>
115 * <p>The <code>&lt;bind /&gt;</code> tag is used to bind a column from the cursor to
116 * a {@link android.view.View}. A column bound using this tag is automatically selected
117 * during the query and a matching
118 * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code> tag is therefore
119 * not required.</p>
120 *
121 * <p>Each binding is declared as a one to one matching but
122 * custom binder classes or special
123 * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> can
124 * allow you to bind several columns to a single view. In this case you must use the
125 * <a href="#xml-cursor-adapter-select-tag"><code>&lt;select /&gt;</code> tag to make
126 * sure any required column is part of the query.</p>
127 *
128 * <p>The <code>&lt;bind /&gt;</code> tag supports the following attributes:</p>
129 * <ul>
130 *  <li><code>android:from</code>: The name of the column to bind from.
131 *  This attribute is mandatory. Note that <code>@</code> which are not used to reference resources
132 *  should be backslash protected as in <code>\@</code>.</li>
133 *  <li><code>android:to</code>: The id of the view to bind to. This attribute is mandatory.</li>
134 *  <li><code>android:as</code>: The <a href="#xml-cursor-adapter-bind-data-types">data type</a>
135 *  of the binding. This attribute is mandatory.</li>
136 * </ul>
137 *
138 * <p>In addition, a <code>&lt;bind /&gt;</code> can contain zero or more instances of
139 * <a href="#xml-cursor-adapter-bind-data-transformation">data transformations</a> children
140 * tags.</p>
141 *
142 * <a name="xml-cursor-adapter-bind-data-types"></a>
143 * <h4>Binding data types</h4>
144 * <p>For a binding to occur the data type of the bound column/view pair must be specified.
145 * The following data types are currently supported:</p>
146 * <ul>
147 *  <li><code>string</code>: The content of the column is interpreted as a string and must be
148 *  bound to a {@link android.widget.TextView}</li>
149 *  <li><code>image</code>: The content of the column is interpreted as a blob describing an
150 *  image and must be bound to an {@link android.widget.ImageView}</li>
151 *  <li><code>image-uri</code>: The content of the column is interpreted as a URI to an image
152 *  and must be bound to an {@link android.widget.ImageView}</li>
153 *  <li><code>drawable</code>: The content of the column is interpreted as a resource id to a
154 *  drawable and must be bound to an {@link android.widget.ImageView}</li>
155 *  <li><code>tag</code>: The content of the column is interpreted as a string and will be set as
156 *  the tag (using {@link View#setTag(Object)} of the associated View. This can be used to
157 *  associate meta-data to your view, that can be used for instance by a listener.</li>
158 *  <li>A fully qualified class name: The name of a class corresponding to an implementation of
159 *  {@link Adapters.CursorBinder}. Cursor binders can be used to provide
160 *  bindings not supported by default. Custom binders cannot be used with
161 *  {@link android.content.Context#isRestricted() restricted contexts}, for instance in an
162 *  application widget</li>
163 * </ul>
164 *
165 * <a name="xml-cursor-adapter-bind-transformation"></a>
166 * <h4>Binding transformations</h4>
167 * <p>When defining a data binding you can specify an optional transformation by using one
168 * of the following tags as a child of a <code>&lt;bind /&gt;</code> elements:</p>
169 * <ul>
170 *  <li><code>&lt;map /&gt;</code>: Maps a constant string to a string or a resource. Use
171 *  one instance of this tag per value you want to map</li>
172 *  <li><code>&lt;transform /&gt;</code>: Transforms a column's value using an expression
173 *  or an instance of {@link Adapters.CursorTransformation}</li>
174 * </ul>
175 * <p>While several <code>&lt;map /&gt;</code> tags can be used at the same time, you cannot
176 * mix <code>&lt;map /&gt;</code> and <code>&lt;transform /&gt;</code> tags. If several
177 * <code>&lt;transform /&gt;</code> tags are specified, only the last one is retained.</p>
178 *
179 * <a name="xml-cursor-adapter-bind-transformation-map" />
180 * <p><strong>&lt;map /&gt;</strong></p>
181 * <p>A map element simply specifies a value to match from and a value to match to. When
182 * a column's value equals the value to match from, it is replaced with the value to match
183 * to. The following attributes are supported:</p>
184 * <ul>
185 *  <li><code>android:fromValue</code>: The value to match from. This attribute is mandatory</li>
186 *  <li><code>android:toValue</code>: The value to match to. This value can be either a string
187 *  or a resource identifier. This value is interpreted as a resource identifier when the
188 *  data binding is of type <code>drawable</code>. This attribute is mandatory</li>
189 * </ul>
190 *
191 * <a name="xml-cursor-adapter-bind-transformation-transform"></a>
192 * <p><strong>&lt;transform /&gt;</strong></p>
193 * <p>A simple transform that occurs either by calling a specified class or by performing
194 * simple text substitution. The following attributes are supported:</p>
195 * <ul>
196 *  <li><code>android:withExpression</code>: The transformation expression. The expression is
197 *  a string containing column names surrounded with curly braces { and }. During the
198 *  transformation each column name is replaced by its value. All columns must have been
199 *  selected in the query. An example of expression is <code>"First name: {first_name},
200 *  last name: {last_name}"</code>. This attribute is mandatory
201 *  if <code>android:withClass</code> is not specified and ignored if <code>android:withClass</code>
202 *  is specified</li>
203 *  <li><code>android:withClass</code>: A fully qualified class name corresponding to an
204 *  implementation of {@link Adapters.CursorTransformation}. Custom
205 *  transformations cannot be used with
206 *  {@link android.content.Context#isRestricted() restricted contexts}, for instance in
207 *  an app widget This attribute is mandatory if <code>android:withExpression</code> is
208 *  not specified</li>
209 * </ul>
210 *
211 * <h3>Example</h3>
212 * <p>The following example defines a cursor adapter that queries all the contacts with
213 * a phone number using the contacts content provider. Each contact is displayed with
214 * its display name, its favorite status and its photo. To display photos, a custom data
215 * binder is declared:</p>
216 *
217 * <pre class="prettyprint">
218 * &lt;cursor-adapter xmlns:android="http://schemas.android.com/apk/res/android"
219 *     android:uri="content://com.android.contacts/contacts"
220 *     android:selection="has_phone_number=1"
221 *     android:layout="@layout/contact_item"&gt;
222 *
223 *     &lt;bind android:from="display_name" android:to="@id/name" android:as="string" /&gt;
224 *     &lt;bind android:from="starred" android:to="@id/star" android:as="drawable"&gt;
225 *         &lt;map android:fromValue="0" android:toValue="@android:drawable/star_big_off" /&gt;
226 *         &lt;map android:fromValue="1" android:toValue="@android:drawable/star_big_on" /&gt;
227 *     &lt;/bind&gt;
228 *     &lt;bind android:from="_id" android:to="@id/name"
229 *              android:as="com.google.android.test.adapters.ContactPhotoBinder" /&gt;
230 *
231 * &lt;/cursor-adapter&gt;
232 * </pre>
233 *
234 * <h3>Related APIs</h3>
235 * <ul>
236 *  <li>{@link Adapters#loadAdapter(android.content.Context, int, Object[])}</li>
237 *  <li>{@link Adapters#loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])}</li>
238 *  <li>{@link Adapters#loadCursorAdapter(android.content.Context, int, String, Object[])}</li>
239 *  <li>{@link Adapters.CursorBinder}</li>
240 *  <li>{@link Adapters.CursorTransformation}</li>
241 *  <li>{@link android.widget.CursorAdapter}</li>
242 * </ul>
243 *
244 * @see android.widget.Adapter
245 * @see android.content.ContentProvider
246 *
247 * attr ref android.R.styleable#CursorAdapter_layout
248 * attr ref android.R.styleable#CursorAdapter_selection
249 * attr ref android.R.styleable#CursorAdapter_sortOrder
250 * attr ref android.R.styleable#CursorAdapter_uri
251 * attr ref android.R.styleable#CursorAdapter_BindItem_as
252 * attr ref android.R.styleable#CursorAdapter_BindItem_from
253 * attr ref android.R.styleable#CursorAdapter_BindItem_to
254 * attr ref android.R.styleable#CursorAdapter_MapItem_fromValue
255 * attr ref android.R.styleable#CursorAdapter_MapItem_toValue
256 * attr ref android.R.styleable#CursorAdapter_SelectItem_column
257 * attr ref android.R.styleable#CursorAdapter_TransformItem_withClass
258 * attr ref android.R.styleable#CursorAdapter_TransformItem_withExpression
259 */
260public class Adapters {
261    private static final String ADAPTER_CURSOR = "cursor-adapter";
262
263    /**
264     * <p>Interface used to bind a {@link android.database.Cursor} column to a View. This
265     * interface can be used to provide bindings for data types not supported by the
266     * standard implementation of {@link Adapters}.</p>
267     *
268     * <p>A binder is provided with a cursor transformation which may or may not be used
269     * to transform the value retrieved from the cursor. The transformation is guaranteed
270     * to never be null so it's always safe to apply the transformation.</p>
271     *
272     * <p>The binder is associated with a Context but can be re-used with multiple cursors.
273     * As such, the implementation should make no assumption about the Cursor in use.</p>
274     *
275     * @see android.view.View
276     * @see android.database.Cursor
277     * @see Adapters.CursorTransformation
278     */
279    public static abstract class CursorBinder {
280        /**
281         * <p>The context associated with this binder.</p>
282         */
283        protected final Context mContext;
284
285        /**
286         * <p>The transformation associated with this binder. This transformation is never
287         * null and may or may not be applied to the Cursor data during the
288         * {@link #bind(android.view.View, android.database.Cursor, int)} operation.</p>
289         *
290         * @see #bind(android.view.View, android.database.Cursor, int)
291         */
292        protected final CursorTransformation mTransformation;
293
294        /**
295         * <p>Creates a new Cursor binder.</p>
296         *
297         * @param context The context associated with this binder.
298         * @param transformation The transformation associated with this binder. This
299         *        transformation may or may not be applied by the binder and is guaranteed
300         *        to not be null.
301         */
302        public CursorBinder(Context context, CursorTransformation transformation) {
303            mContext = context;
304            mTransformation = transformation;
305        }
306
307        /**
308         * <p>Binds the specified Cursor column to the supplied View. The binding operation
309         * can query other Cursor columns as needed. During the binding operation, values
310         * retrieved from the Cursor may or may not be transformed using this binder's
311         * cursor transformation.</p>
312         *
313         * @param view The view to bind data to.
314         * @param cursor The cursor to bind data from.
315         * @param columnIndex The column index in the cursor where the data to bind resides.
316         *
317         * @see #mTransformation
318         *
319         * @return True if the column was successfully bound to the View, false otherwise.
320         */
321        public abstract boolean bind(View view, Cursor cursor, int columnIndex);
322    }
323
324    /**
325     * <p>Interface used to transform data coming out of a {@link android.database.Cursor}
326     * before it is bound to a {@link android.view.View}.</p>
327     *
328     * <p>Transformations are used to transform text-based data (in the form of a String),
329     * or to transform data into a resource identifier. A default implementation is provided
330     * to generate resource identifiers.</p>
331     *
332     * @see android.database.Cursor
333     * @see Adapters.CursorBinder
334     */
335    public static abstract class CursorTransformation {
336        /**
337         * <p>The context associated with this transformation.</p>
338         */
339        protected final Context mContext;
340
341        /**
342         * <p>Creates a new Cursor transformation.</p>
343         *
344         * @param context The context associated with this transformation.
345         */
346        public CursorTransformation(Context context) {
347            mContext = context;
348        }
349
350        /**
351         * <p>Transforms the specified Cursor column into a String. The transformation
352         * can simply return the content of the column as a String (this is known
353         * as the identity transformation) or manipulate the content. For instance,
354         * a transformation can perform text substitutions or concatenate other
355         * columns with the specified column.</p>
356         *
357         * @param cursor The cursor that contains the data to transform.
358         * @param columnIndex The index of the column to transform.
359         *
360         * @return A String containing the transformed value of the column.
361         */
362        public abstract String transform(Cursor cursor, int columnIndex);
363
364        /**
365         * <p>Transforms the specified Cursor column into a resource identifier.
366         * The default implementation simply interprets the content of the column
367         * as an integer.</p>
368         *
369         * @param cursor The cursor that contains the data to transform.
370         * @param columnIndex The index of the column to transform.
371         *
372         * @return A resource identifier.
373         */
374        public int transformToResource(Cursor cursor, int columnIndex) {
375            return cursor.getInt(columnIndex);
376        }
377    }
378
379    /**
380     * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified
381     * XML resource. The content of the adapter is loaded from the content provider
382     * identified by the supplied URI.</p>
383     *
384     * <p><strong>Note:</strong> If the supplied {@link android.content.Context} is
385     * an {@link android.app.Activity}, the cursor returned by the content provider
386     * will be automatically managed. Otherwise, you are responsible for managing the
387     * cursor yourself.</p>
388     *
389     * <p>The format of the XML definition of the cursor adapter is documented at
390     * the top of this page.</p>
391     *
392     * @param context The context to load the XML resource from.
393     * @param id The identifier of the XML resource declaring the adapter.
394     * @param uri The URI of the content provider.
395     * @param parameters Optional parameters to pass to the CursorAdapter, used
396     *        to substitute values in the selection expression.
397     *
398     * @return A {@link android.widget.CursorAdapter}
399     *
400     * @throws IllegalArgumentException If the XML resource does not contain
401     *         a valid &lt;cursor-adapter /&gt; definition.
402     *
403     * @see android.content.ContentProvider
404     * @see android.widget.CursorAdapter
405     * @see #loadAdapter(android.content.Context, int, Object[])
406     */
407    public static CursorAdapter loadCursorAdapter(Context context, int id, String uri,
408            Object... parameters) {
409
410        XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
411                parameters);
412
413        if (uri != null) {
414            adapter.setUri(uri);
415        }
416        adapter.load();
417
418        return adapter;
419    }
420
421    /**
422     * <p>Loads the {@link android.widget.CursorAdapter} defined in the specified
423     * XML resource. The content of the adapter is loaded from the specified cursor.
424     * You are responsible for managing the supplied cursor.</p>
425     *
426     * <p>The format of the XML definition of the cursor adapter is documented at
427     * the top of this page.</p>
428     *
429     * @param context The context to load the XML resource from.
430     * @param id The identifier of the XML resource declaring the adapter.
431     * @param cursor The cursor containing the data for the adapter.
432     * @param parameters Optional parameters to pass to the CursorAdapter, used
433     *        to substitute values in the selection expression.
434     *
435     * @return A {@link android.widget.CursorAdapter}
436     *
437     * @throws IllegalArgumentException If the XML resource does not contain
438     *         a valid &lt;cursor-adapter /&gt; definition.
439     *
440     * @see android.content.ContentProvider
441     * @see android.widget.CursorAdapter
442     * @see android.database.Cursor
443     * @see #loadAdapter(android.content.Context, int, Object[])
444     */
445    public static CursorAdapter loadCursorAdapter(Context context, int id, Cursor cursor,
446            Object... parameters) {
447
448        XmlCursorAdapter adapter = (XmlCursorAdapter) loadAdapter(context, id, ADAPTER_CURSOR,
449                parameters);
450
451        if (cursor != null) {
452            adapter.changeCursor(cursor);
453        }
454
455        return adapter;
456    }
457
458    /**
459     * <p>Loads the adapter defined in the specified XML resource. The XML definition of
460     * the adapter must follow the format definition of one of the supported adapter
461     * types described at the top of this page.</p>
462     *
463     * <p><strong>Note:</strong> If the loaded adapter is a {@link android.widget.CursorAdapter}
464     * and the supplied {@link android.content.Context} is an {@link android.app.Activity},
465     * the cursor returned by the content provider will be automatically managed. Otherwise,
466     * you are responsible for managing the cursor yourself.</p>
467     *
468     * @param context The context to load the XML resource from.
469     * @param id The identifier of the XML resource declaring the adapter.
470     * @param parameters Optional parameters to pass to the adapter.
471     *
472     * @return An adapter instance.
473     *
474     * @see #loadCursorAdapter(android.content.Context, int, android.database.Cursor, Object[])
475     * @see #loadCursorAdapter(android.content.Context, int, String, Object[])
476     */
477    public static BaseAdapter loadAdapter(Context context, int id, Object... parameters) {
478        final BaseAdapter adapter = loadAdapter(context, id, null, parameters);
479        if (adapter instanceof ManagedAdapter) {
480            ((ManagedAdapter) adapter).load();
481        }
482        return adapter;
483    }
484
485    /**
486     * Loads an adapter from the specified XML resource. The optional assertName can
487     * be used to exit early if the adapter defined in the XML resource is not of the
488     * expected type.
489     *
490     * @param context The context to associate with the adapter.
491     * @param id The resource id of the XML document defining the adapter.
492     * @param assertName The mandatory name of the adapter in the XML document.
493     *        Ignored if null.
494     * @param parameters Optional parameters passed to the adapter.
495     *
496     * @return An instance of {@link android.widget.BaseAdapter}.
497     */
498    private static BaseAdapter loadAdapter(Context context, int id, String assertName,
499            Object... parameters) {
500
501        XmlResourceParser parser = null;
502        try {
503            parser = context.getResources().getXml(id);
504            return createAdapterFromXml(context, parser, Xml.asAttributeSet(parser),
505                    id, parameters, assertName);
506        } catch (XmlPullParserException ex) {
507            Resources.NotFoundException rnf = new Resources.NotFoundException(
508                    "Can't load adapter resource ID " +
509                    context.getResources().getResourceEntryName(id));
510            rnf.initCause(ex);
511            throw rnf;
512        } catch (IOException ex) {
513            Resources.NotFoundException rnf = new Resources.NotFoundException(
514                    "Can't load adapter resource ID " +
515                    context.getResources().getResourceEntryName(id));
516            rnf.initCause(ex);
517            throw rnf;
518        } finally {
519            if (parser != null) parser.close();
520        }
521    }
522
523    /**
524     * Generates an adapter using the specified XML parser. This method is responsible
525     * for choosing the type of the adapter to create based on the content of the
526     * XML parser.
527     *
528     * This method will generate an {@link IllegalArgumentException} if
529     * <code>assertName</code> is not null and does not match the root tag of the XML
530     * document.
531     */
532    private static BaseAdapter createAdapterFromXml(Context c,
533            XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters,
534            String assertName) throws XmlPullParserException, IOException {
535
536        BaseAdapter adapter = null;
537
538        // Make sure we are on a start tag.
539        int type;
540        int depth = parser.getDepth();
541
542        while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
543                type != XmlPullParser.END_DOCUMENT) {
544
545            if (type != XmlPullParser.START_TAG) {
546                continue;
547            }
548
549            String name = parser.getName();
550            if (assertName != null && !assertName.equals(name)) {
551                throw new IllegalArgumentException("The adapter defined in " +
552                        c.getResources().getResourceEntryName(id) + " must be a <" +
553                        assertName + " />");
554            }
555
556            if (ADAPTER_CURSOR.equals(name)) {
557                adapter = createCursorAdapter(c, parser, attrs, id, parameters);
558            } else {
559                throw new IllegalArgumentException("Unknown adapter name " + parser.getName() +
560                        " in " + c.getResources().getResourceEntryName(id));
561            }
562        }
563
564        return adapter;
565
566    }
567
568    /**
569     * Creates an XmlCursorAdapter using an XmlCursorAdapterParser.
570     */
571    private static XmlCursorAdapter createCursorAdapter(Context c, XmlPullParser parser,
572            AttributeSet attrs, int id, Object[] parameters)
573            throws IOException, XmlPullParserException {
574
575        return new XmlCursorAdapterParser(c, parser, attrs, id).parse(parameters);
576    }
577
578    /**
579     * Parser that can generate XmlCursorAdapter instances. This parser is responsible for
580     * handling all the attributes and child nodes for a &lt;cursor-adapter /&gt;.
581     */
582    private static class XmlCursorAdapterParser {
583        private static final String ADAPTER_CURSOR_BIND = "bind";
584        private static final String ADAPTER_CURSOR_SELECT = "select";
585        private static final String ADAPTER_CURSOR_AS_STRING = "string";
586        private static final String ADAPTER_CURSOR_AS_IMAGE = "image";
587        private static final String ADAPTER_CURSOR_AS_TAG = "tag";
588        private static final String ADAPTER_CURSOR_AS_IMAGE_URI = "image-uri";
589        private static final String ADAPTER_CURSOR_AS_DRAWABLE = "drawable";
590        private static final String ADAPTER_CURSOR_MAP = "map";
591        private static final String ADAPTER_CURSOR_TRANSFORM = "transform";
592
593        private final Context mContext;
594        private final XmlPullParser mParser;
595        private final AttributeSet mAttrs;
596        private final int mId;
597
598        private final HashMap<String, CursorBinder> mBinders;
599        private final ArrayList<String> mFrom;
600        private final ArrayList<Integer> mTo;
601        private final CursorTransformation mIdentity;
602        private final Resources mResources;
603
604        public XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id) {
605            mContext = c;
606            mParser = parser;
607            mAttrs = attrs;
608            mId = id;
609
610            mResources = mContext.getResources();
611            mBinders = new HashMap<String, CursorBinder>();
612            mFrom = new ArrayList<String>();
613            mTo = new ArrayList<Integer>();
614            mIdentity = new IdentityTransformation(mContext);
615        }
616
617        public XmlCursorAdapter parse(Object[] parameters)
618               throws IOException, XmlPullParserException {
619
620            Resources resources = mResources;
621            TypedArray a = resources.obtainAttributes(mAttrs, R.styleable.CursorAdapter);
622
623            String uri = a.getString(R.styleable.CursorAdapter_uri);
624            String selection = a.getString(R.styleable.CursorAdapter_selection);
625            String sortOrder = a.getString(R.styleable.CursorAdapter_sortOrder);
626            int layout = a.getResourceId(R.styleable.CursorAdapter_layout, 0);
627            if (layout == 0) {
628                throw new IllegalArgumentException("The layout specified in " +
629                        resources.getResourceEntryName(mId) + " does not exist");
630            }
631
632            a.recycle();
633
634            XmlPullParser parser = mParser;
635            int type;
636            int depth = parser.getDepth();
637
638            while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) &&
639                    type != XmlPullParser.END_DOCUMENT) {
640
641                if (type != XmlPullParser.START_TAG) {
642                    continue;
643                }
644
645                String name = parser.getName();
646
647                if (ADAPTER_CURSOR_BIND.equals(name)) {
648                    parseBindTag();
649                } else if (ADAPTER_CURSOR_SELECT.equals(name)) {
650                    parseSelectTag();
651                } else {
652                    throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
653                            resources.getResourceEntryName(mId));
654                }
655            }
656
657            String[] fromArray = mFrom.toArray(new String[mFrom.size()]);
658            int[] toArray = new int[mTo.size()];
659            for (int i = 0; i < toArray.length; i++) {
660                toArray[i] = mTo.get(i);
661            }
662
663            String[] selectionArgs = null;
664            if (parameters != null) {
665                selectionArgs = new String[parameters.length];
666                for (int i = 0; i < selectionArgs.length; i++) {
667                    selectionArgs[i] = (String) parameters[i];
668                }
669            }
670
671            return new XmlCursorAdapter(mContext, layout, uri, fromArray, toArray, selection,
672                    selectionArgs, sortOrder, mBinders);
673        }
674
675        private void parseSelectTag() {
676            TypedArray a = mResources.obtainAttributes(mAttrs,
677                    R.styleable.CursorAdapter_SelectItem);
678
679            String fromName = a.getString(R.styleable.CursorAdapter_SelectItem_column);
680            if (fromName == null) {
681                throw new IllegalArgumentException("A select item in " +
682                        mResources.getResourceEntryName(mId) +
683                        " does not have a 'column' attribute");
684            }
685
686            a.recycle();
687
688            mFrom.add(fromName);
689            mTo.add(View.NO_ID);
690        }
691
692        private void parseBindTag() throws IOException, XmlPullParserException {
693            Resources resources = mResources;
694            TypedArray a = resources.obtainAttributes(mAttrs,
695                    R.styleable.CursorAdapter_BindItem);
696
697            String fromName = a.getString(R.styleable.CursorAdapter_BindItem_from);
698            if (fromName == null) {
699                throw new IllegalArgumentException("A bind item in " +
700                        resources.getResourceEntryName(mId) + " does not have a 'from' attribute");
701            }
702
703            int toName = a.getResourceId(R.styleable.CursorAdapter_BindItem_to, 0);
704            if (toName == 0) {
705                throw new IllegalArgumentException("A bind item in " +
706                        resources.getResourceEntryName(mId) + " does not have a 'to' attribute");
707            }
708
709            String asType = a.getString(R.styleable.CursorAdapter_BindItem_as);
710            if (asType == null) {
711                throw new IllegalArgumentException("A bind item in " +
712                        resources.getResourceEntryName(mId) + " does not have an 'as' attribute");
713            }
714
715            mFrom.add(fromName);
716            mTo.add(toName);
717            mBinders.put(fromName, findBinder(asType));
718
719            a.recycle();
720        }
721
722        private CursorBinder findBinder(String type) throws IOException, XmlPullParserException {
723            final XmlPullParser parser = mParser;
724            final Context context = mContext;
725            CursorTransformation transformation = mIdentity;
726
727            int tagType;
728            int depth = parser.getDepth();
729
730            final boolean isDrawable = ADAPTER_CURSOR_AS_DRAWABLE.equals(type);
731
732            while (((tagType = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
733                    && tagType != XmlPullParser.END_DOCUMENT) {
734
735                if (tagType != XmlPullParser.START_TAG) {
736                    continue;
737                }
738
739                String name = parser.getName();
740
741                if (ADAPTER_CURSOR_TRANSFORM.equals(name)) {
742                    transformation = findTransformation();
743                } else if (ADAPTER_CURSOR_MAP.equals(name)) {
744                    if (!(transformation instanceof MapTransformation)) {
745                        transformation = new MapTransformation(context);
746                    }
747                    findMap(((MapTransformation) transformation), isDrawable);
748                } else {
749                    throw new RuntimeException("Unknown tag name " + parser.getName() + " in " +
750                            context.getResources().getResourceEntryName(mId));
751                }
752            }
753
754            if (ADAPTER_CURSOR_AS_STRING.equals(type)) {
755                return new StringBinder(context, transformation);
756            } else if (ADAPTER_CURSOR_AS_TAG.equals(type)) {
757                return new TagBinder(context, transformation);
758            } else if (ADAPTER_CURSOR_AS_IMAGE.equals(type)) {
759                return new ImageBinder(context, transformation);
760            } else if (ADAPTER_CURSOR_AS_IMAGE_URI.equals(type)) {
761                return new ImageUriBinder(context, transformation);
762            } else if (isDrawable) {
763                return new DrawableBinder(context, transformation);
764            } else {
765                return createBinder(type, transformation);
766            }
767        }
768
769        private CursorBinder createBinder(String type, CursorTransformation transformation) {
770            if (mContext.isRestricted()) return null;
771
772            try {
773                final Class<?> klass = Class.forName(type, true, mContext.getClassLoader());
774                if (CursorBinder.class.isAssignableFrom(klass)) {
775                    final Constructor<?> c = klass.getDeclaredConstructor(
776                            Context.class, CursorTransformation.class);
777                    return (CursorBinder) c.newInstance(mContext, transformation);
778                }
779            } catch (ClassNotFoundException e) {
780                throw new IllegalArgumentException("Cannot instanciate binder type in " +
781                        mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
782            } catch (NoSuchMethodException e) {
783                throw new IllegalArgumentException("Cannot instanciate binder type in " +
784                        mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
785            } catch (InvocationTargetException e) {
786                throw new IllegalArgumentException("Cannot instanciate binder type in " +
787                        mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
788            } catch (InstantiationException e) {
789                throw new IllegalArgumentException("Cannot instanciate binder type in " +
790                        mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
791            } catch (IllegalAccessException e) {
792                throw new IllegalArgumentException("Cannot instanciate binder type in " +
793                        mContext.getResources().getResourceEntryName(mId) + ": " + type, e);
794            }
795
796            return null;
797        }
798
799        private void findMap(MapTransformation transformation, boolean drawable) {
800            Resources resources = mResources;
801
802            TypedArray a = resources.obtainAttributes(mAttrs,
803                    R.styleable.CursorAdapter_MapItem);
804
805            String from = a.getString(R.styleable.CursorAdapter_MapItem_fromValue);
806            if (from == null) {
807                throw new IllegalArgumentException("A map item in " +
808                        resources.getResourceEntryName(mId) +
809                        " does not have a 'fromValue' attribute");
810            }
811
812            if (!drawable) {
813                String to = a.getString(R.styleable.CursorAdapter_MapItem_toValue);
814                if (to == null) {
815                    throw new IllegalArgumentException("A map item in " +
816                            resources.getResourceEntryName(mId) +
817                            " does not have a 'toValue' attribute");
818                }
819                transformation.addStringMapping(from, to);
820            } else {
821                int to = a.getResourceId(R.styleable.CursorAdapter_MapItem_toValue, 0);
822                if (to == 0) {
823                    throw new IllegalArgumentException("A map item in " +
824                            resources.getResourceEntryName(mId) +
825                            " does not have a 'toValue' attribute");
826                }
827                transformation.addResourceMapping(from, to);
828            }
829
830            a.recycle();
831        }
832
833        private CursorTransformation findTransformation() {
834            Resources resources = mResources;
835            CursorTransformation transformation = null;
836            TypedArray a = resources.obtainAttributes(mAttrs,
837                    R.styleable.CursorAdapter_TransformItem);
838
839            String className = a.getString(R.styleable.CursorAdapter_TransformItem_withClass);
840            if (className == null) {
841                String expression = a.getString(
842                        R.styleable.CursorAdapter_TransformItem_withExpression);
843                transformation = createExpressionTransformation(expression);
844            } else if (!mContext.isRestricted()) {
845                try {
846                    final Class<?> klas = Class.forName(className, true, mContext.getClassLoader());
847                    if (CursorTransformation.class.isAssignableFrom(klas)) {
848                        final Constructor<?> c = klas.getDeclaredConstructor(Context.class);
849                        transformation = (CursorTransformation) c.newInstance(mContext);
850                    }
851                } catch (ClassNotFoundException e) {
852                    throw new IllegalArgumentException("Cannot instanciate transform type in " +
853                           mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
854                } catch (NoSuchMethodException e) {
855                    throw new IllegalArgumentException("Cannot instanciate transform type in " +
856                           mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
857                } catch (InvocationTargetException e) {
858                    throw new IllegalArgumentException("Cannot instanciate transform type in " +
859                           mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
860                } catch (InstantiationException e) {
861                    throw new IllegalArgumentException("Cannot instanciate transform type in " +
862                           mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
863                } catch (IllegalAccessException e) {
864                    throw new IllegalArgumentException("Cannot instanciate transform type in " +
865                           mContext.getResources().getResourceEntryName(mId) + ": " + className, e);
866                }
867            }
868
869            a.recycle();
870
871            if (transformation == null) {
872                throw new IllegalArgumentException("A transform item in " +
873                    resources.getResourceEntryName(mId) + " must have a 'withClass' or " +
874                    "'withExpression' attribute");
875            }
876
877            return transformation;
878        }
879
880        private CursorTransformation createExpressionTransformation(String expression) {
881            return new ExpressionTransformation(mContext, expression);
882        }
883    }
884
885    /**
886     * Interface used by adapters that require to be loaded after creation.
887     */
888    private static interface ManagedAdapter {
889        /**
890         * Loads the content of the adapter, asynchronously.
891         */
892        void load();
893    }
894
895    /**
896     * Implementation of a Cursor adapter defined in XML. This class is a thin wrapper
897     * of a SimpleCursorAdapter. The main difference is the ability to handle CursorBinders.
898     */
899    private static class XmlCursorAdapter extends SimpleCursorAdapter implements ManagedAdapter {
900        private Context mContext;
901        private String mUri;
902        private final String mSelection;
903        private final String[] mSelectionArgs;
904        private final String mSortOrder;
905        private final int[] mTo;
906        private final String[] mFrom;
907        private final String[] mColumns;
908        private final CursorBinder[] mBinders;
909        private AsyncTask<Void,Void,Cursor> mLoadTask;
910
911        XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to,
912                String selection, String[] selectionArgs, String sortOrder,
913                HashMap<String, CursorBinder> binders) {
914
915            super(context, layout, null, from, to);
916            mContext = context;
917            mUri = uri;
918            mFrom = from;
919            mTo = to;
920            mSelection = selection;
921            mSelectionArgs = selectionArgs;
922            mSortOrder = sortOrder;
923            mColumns = new String[from.length + 1];
924            // This is mandatory in CursorAdapter
925            mColumns[0] = "_id";
926            System.arraycopy(from, 0, mColumns, 1, from.length);
927
928            CursorBinder basic = new StringBinder(context, new IdentityTransformation(context));
929            final int count = from.length;
930            mBinders = new CursorBinder[count];
931
932            for (int i = 0; i < count; i++) {
933                CursorBinder binder = binders.get(from[i]);
934                if (binder == null) binder = basic;
935                mBinders[i] = binder;
936            }
937        }
938
939        @Override
940        public void bindView(View view, Context context, Cursor cursor) {
941            final int count = mTo.length;
942            final int[] to = mTo;
943            final CursorBinder[] binders = mBinders;
944
945            for (int i = 0; i < count; i++) {
946                final View v = view.findViewById(to[i]);
947                if (v != null) {
948                    // Not optimal, the column index could be cached
949                    binders[i].bind(v, cursor, cursor.getColumnIndex(mFrom[i]));
950                }
951            }
952        }
953
954        public void load() {
955            if (mUri != null) {
956                mLoadTask = new QueryTask().execute();
957            }
958        }
959
960        void setUri(String uri) {
961            mUri = uri;
962        }
963
964        @Override
965        public void changeCursor(Cursor c) {
966            if (mLoadTask != null && mLoadTask.getStatus() != QueryTask.Status.FINISHED) {
967                mLoadTask.cancel(true);
968                mLoadTask = null;
969            }
970            super.changeCursor(c);
971        }
972
973        class QueryTask extends AsyncTask<Void, Void, Cursor> {
974            @Override
975            protected Cursor doInBackground(Void... params) {
976                if (mContext instanceof Activity) {
977                    return ((Activity) mContext).managedQuery(
978                            Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
979                } else {
980                    return mContext.getContentResolver().query(
981                            Uri.parse(mUri), mColumns, mSelection, mSelectionArgs, mSortOrder);
982                }
983            }
984
985            @Override
986            protected void onPostExecute(Cursor cursor) {
987                if (!isCancelled()) {
988                    XmlCursorAdapter.super.changeCursor(cursor);
989                }
990            }
991        }
992    }
993
994    /**
995     * Identity transformation, returns the content of the specified column as a String,
996     * without performing any manipulation. This is used when no transformation is specified.
997     */
998    private static class IdentityTransformation extends CursorTransformation {
999        public IdentityTransformation(Context context) {
1000            super(context);
1001        }
1002
1003        @Override
1004        public String transform(Cursor cursor, int columnIndex) {
1005            return cursor.getString(columnIndex);
1006        }
1007    }
1008
1009    /**
1010     * An expression transformation is a simple template based replacement utility.
1011     * In an expression, each segment of the form <code>{([^}]+)}</code> is replaced
1012     * with the value of the column of name $1.
1013     */
1014    private static class ExpressionTransformation extends CursorTransformation {
1015        private final ExpressionNode mFirstNode = new ConstantExpressionNode("");
1016        private final StringBuilder mBuilder = new StringBuilder();
1017
1018        public ExpressionTransformation(Context context, String expression) {
1019            super(context);
1020
1021            parse(expression);
1022        }
1023
1024        private void parse(String expression) {
1025            ExpressionNode node = mFirstNode;
1026            int segmentStart;
1027            int count = expression.length();
1028
1029            for (int i = 0; i < count; i++) {
1030                char c = expression.charAt(i);
1031                // Start a column name segment
1032                segmentStart = i;
1033                if (c == '{') {
1034                    while (i < count && (c = expression.charAt(i)) != '}') {
1035                        i++;
1036                    }
1037                    // We've reached the end, but the expression didn't close
1038                    if (c != '}') {
1039                        throw new IllegalStateException("The transform expression contains a " +
1040                                "non-closed column name: " +
1041                                expression.substring(segmentStart + 1, i));
1042                    }
1043                    node.next = new ColumnExpressionNode(expression.substring(segmentStart + 1, i));
1044                } else {
1045                    while (i < count && (c = expression.charAt(i)) != '{') {
1046                        i++;
1047                    }
1048                    node.next = new ConstantExpressionNode(expression.substring(segmentStart, i));
1049                    // Rewind if we've reached a column expression
1050                    if (c == '{') i--;
1051                }
1052                node = node.next;
1053            }
1054        }
1055
1056        @Override
1057        public String transform(Cursor cursor, int columnIndex) {
1058            final StringBuilder builder = mBuilder;
1059            builder.delete(0, builder.length());
1060
1061            ExpressionNode node = mFirstNode;
1062            // Skip the first node
1063            while ((node = node.next) != null) {
1064                builder.append(node.asString(cursor));
1065            }
1066
1067            return builder.toString();
1068        }
1069
1070        static abstract class ExpressionNode {
1071            public ExpressionNode next;
1072
1073            public abstract String asString(Cursor cursor);
1074        }
1075
1076        static class ConstantExpressionNode extends ExpressionNode {
1077            private final String mConstant;
1078
1079            ConstantExpressionNode(String constant) {
1080                mConstant = constant;
1081            }
1082
1083            @Override
1084            public String asString(Cursor cursor) {
1085                return mConstant;
1086            }
1087        }
1088
1089        static class ColumnExpressionNode extends ExpressionNode {
1090            private final String mColumnName;
1091            private Cursor mSignature;
1092            private int mColumnIndex = -1;
1093
1094            ColumnExpressionNode(String columnName) {
1095                mColumnName = columnName;
1096            }
1097
1098            @Override
1099            public String asString(Cursor cursor) {
1100                if (cursor != mSignature || mColumnIndex == -1) {
1101                    mColumnIndex = cursor.getColumnIndex(mColumnName);
1102                    mSignature = cursor;
1103                }
1104
1105                return cursor.getString(mColumnIndex);
1106            }
1107        }
1108    }
1109
1110    /**
1111     * A map transformation offers a simple mapping between specified String values
1112     * to Strings or integers.
1113     */
1114    private static class MapTransformation extends CursorTransformation {
1115        private final HashMap<String, String> mStringMappings;
1116        private final HashMap<String, Integer> mResourceMappings;
1117
1118        public MapTransformation(Context context) {
1119            super(context);
1120            mStringMappings = new HashMap<String, String>();
1121            mResourceMappings = new HashMap<String, Integer>();
1122        }
1123
1124        void addStringMapping(String from, String to) {
1125            mStringMappings.put(from, to);
1126        }
1127
1128        void addResourceMapping(String from, int to) {
1129            mResourceMappings.put(from, to);
1130        }
1131
1132        @Override
1133        public String transform(Cursor cursor, int columnIndex) {
1134            final String value = cursor.getString(columnIndex);
1135            final String transformed = mStringMappings.get(value);
1136            return transformed == null ? value : transformed;
1137        }
1138
1139        @Override
1140        public int transformToResource(Cursor cursor, int columnIndex) {
1141            final String value = cursor.getString(columnIndex);
1142            final Integer transformed = mResourceMappings.get(value);
1143            try {
1144                return transformed == null ? Integer.parseInt(value) : transformed;
1145            } catch (NumberFormatException e) {
1146                return 0;
1147            }
1148        }
1149    }
1150
1151    /**
1152     * Binds a String to a TextView.
1153     */
1154    private static class StringBinder extends CursorBinder {
1155        public StringBinder(Context context, CursorTransformation transformation) {
1156            super(context, transformation);
1157        }
1158
1159        @Override
1160        public boolean bind(View view, Cursor cursor, int columnIndex) {
1161            if (view instanceof TextView) {
1162                final String text = mTransformation.transform(cursor, columnIndex);
1163                ((TextView) view).setText(text);
1164                return true;
1165            }
1166            return false;
1167        }
1168    }
1169
1170    /**
1171     * Binds an image blob to an ImageView.
1172     */
1173    private static class ImageBinder extends CursorBinder {
1174        public ImageBinder(Context context, CursorTransformation transformation) {
1175            super(context, transformation);
1176        }
1177
1178        @Override
1179        public boolean bind(View view, Cursor cursor, int columnIndex) {
1180            if (view instanceof ImageView) {
1181                final byte[] data = cursor.getBlob(columnIndex);
1182                ((ImageView) view).setImageBitmap(BitmapFactory.decodeByteArray(data, 0,
1183                        data.length));
1184                return true;
1185            }
1186            return false;
1187        }
1188    }
1189
1190    private static class TagBinder extends CursorBinder {
1191        public TagBinder(Context context, CursorTransformation transformation) {
1192            super(context, transformation);
1193        }
1194
1195        @Override
1196        public boolean bind(View view, Cursor cursor, int columnIndex) {
1197            final String text = mTransformation.transform(cursor, columnIndex);
1198            view.setTag(text);
1199            return true;
1200        }
1201    }
1202
1203    /**
1204     * Binds an image URI to an ImageView.
1205     */
1206    private static class ImageUriBinder extends CursorBinder {
1207        public ImageUriBinder(Context context, CursorTransformation transformation) {
1208            super(context, transformation);
1209        }
1210
1211        @Override
1212        public boolean bind(View view, Cursor cursor, int columnIndex) {
1213            if (view instanceof ImageView) {
1214                ((ImageView) view).setImageURI(Uri.parse(
1215                        mTransformation.transform(cursor, columnIndex)));
1216                return true;
1217            }
1218            return false;
1219        }
1220    }
1221
1222    /**
1223     * Binds a drawable resource identifier to an ImageView.
1224     */
1225    private static class DrawableBinder extends CursorBinder {
1226        public DrawableBinder(Context context, CursorTransformation transformation) {
1227            super(context, transformation);
1228        }
1229
1230        @Override
1231        public boolean bind(View view, Cursor cursor, int columnIndex) {
1232            if (view instanceof ImageView) {
1233                final int resource = mTransformation.transformToResource(cursor, columnIndex);
1234                if (resource == 0) return false;
1235
1236                ((ImageView) view).setImageResource(resource);
1237                return true;
1238            }
1239            return false;
1240        }
1241    }
1242}
1243