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 
17 package com.example.android.xmladapters;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.content.res.TypedArray;
23 import android.content.res.XmlResourceParser;
24 import android.database.Cursor;
25 import android.graphics.BitmapFactory;
26 import android.net.Uri;
27 import android.os.AsyncTask;
28 import android.util.AttributeSet;
29 import android.util.Xml;
30 import android.view.View;
31 import android.widget.BaseAdapter;
32 import android.widget.CursorAdapter;
33 import android.widget.ImageView;
34 import android.widget.SimpleCursorAdapter;
35 import android.widget.TextView;
36 
37 import org.xmlpull.v1.XmlPullParser;
38 import org.xmlpull.v1.XmlPullParserException;
39 
40 import java.io.IOException;
41 import java.lang.reflect.Constructor;
42 import java.lang.reflect.InvocationTargetException;
43 import java.util.ArrayList;
44 import 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  */
260 public 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          */
CursorBinder(Context context, CursorTransformation transformation)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          */
bind(View view, Cursor cursor, int columnIndex)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          */
CursorTransformation(Context context)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          */
transform(Cursor cursor, int columnIndex)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          */
transformToResource(Cursor cursor, int columnIndex)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      */
loadCursorAdapter(Context context, int id, String uri, Object... parameters)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      */
loadCursorAdapter(Context context, int id, Cursor cursor, Object... parameters)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      */
loadAdapter(Context context, int id, Object... parameters)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      */
loadAdapter(Context context, int id, String assertName, Object... parameters)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      */
createAdapterFromXml(Context c, XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters, String assertName)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      */
createCursorAdapter(Context c, XmlPullParser parser, AttributeSet attrs, int id, Object[] parameters)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 
XmlCursorAdapterParser(Context c, XmlPullParser parser, AttributeSet attrs, int id)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 
parse(Object[] parameters)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 
parseSelectTag()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 
parseBindTag()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 
findBinder(String type)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 
createBinder(String type, CursorTransformation transformation)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 
findMap(MapTransformation transformation, boolean drawable)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 
findTransformation()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 
createExpressionTransformation(String expression)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          */
load()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 
XmlCursorAdapter(Context context, int layout, String uri, String[] from, int[] to, String selection, String[] selectionArgs, String sortOrder, HashMap<String, CursorBinder> binders)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
bindView(View view, Context context, Cursor cursor)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 
load()954         public void load() {
955             if (mUri != null) {
956                 mLoadTask = new QueryTask().execute();
957             }
958         }
959 
setUri(String uri)960         void setUri(String uri) {
961             mUri = uri;
962         }
963 
964         @Override
changeCursor(Cursor c)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
doInBackground(Void... params)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
onPostExecute(Cursor cursor)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 {
IdentityTransformation(Context context)999         public IdentityTransformation(Context context) {
1000             super(context);
1001         }
1002 
1003         @Override
transform(Cursor cursor, int columnIndex)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 
ExpressionTransformation(Context context, String expression)1018         public ExpressionTransformation(Context context, String expression) {
1019             super(context);
1020 
1021             parse(expression);
1022         }
1023 
parse(String expression)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
transform(Cursor cursor, int columnIndex)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 
asString(Cursor cursor)1073             public abstract String asString(Cursor cursor);
1074         }
1075 
1076         static class ConstantExpressionNode extends ExpressionNode {
1077             private final String mConstant;
1078 
ConstantExpressionNode(String constant)1079             ConstantExpressionNode(String constant) {
1080                 mConstant = constant;
1081             }
1082 
1083             @Override
asString(Cursor cursor)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 
ColumnExpressionNode(String columnName)1094             ColumnExpressionNode(String columnName) {
1095                 mColumnName = columnName;
1096             }
1097 
1098             @Override
asString(Cursor cursor)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 
MapTransformation(Context context)1118         public MapTransformation(Context context) {
1119             super(context);
1120             mStringMappings = new HashMap<String, String>();
1121             mResourceMappings = new HashMap<String, Integer>();
1122         }
1123 
addStringMapping(String from, String to)1124         void addStringMapping(String from, String to) {
1125             mStringMappings.put(from, to);
1126         }
1127 
addResourceMapping(String from, int to)1128         void addResourceMapping(String from, int to) {
1129             mResourceMappings.put(from, to);
1130         }
1131 
1132         @Override
transform(Cursor cursor, int columnIndex)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
transformToResource(Cursor cursor, int columnIndex)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 {
StringBinder(Context context, CursorTransformation transformation)1155         public StringBinder(Context context, CursorTransformation transformation) {
1156             super(context, transformation);
1157         }
1158 
1159         @Override
bind(View view, Cursor cursor, int columnIndex)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 {
ImageBinder(Context context, CursorTransformation transformation)1174         public ImageBinder(Context context, CursorTransformation transformation) {
1175             super(context, transformation);
1176         }
1177 
1178         @Override
bind(View view, Cursor cursor, int columnIndex)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 {
TagBinder(Context context, CursorTransformation transformation)1191         public TagBinder(Context context, CursorTransformation transformation) {
1192             super(context, transformation);
1193         }
1194 
1195         @Override
bind(View view, Cursor cursor, int columnIndex)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 {
ImageUriBinder(Context context, CursorTransformation transformation)1207         public ImageUriBinder(Context context, CursorTransformation transformation) {
1208             super(context, transformation);
1209         }
1210 
1211         @Override
bind(View view, Cursor cursor, int columnIndex)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 {
DrawableBinder(Context context, CursorTransformation transformation)1226         public DrawableBinder(Context context, CursorTransformation transformation) {
1227             super(context, transformation);
1228         }
1229 
1230         @Override
bind(View view, Cursor cursor, int columnIndex)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