Home | History | Annotate | Download | only in xmladapters
      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          */
    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