Home | History | Annotate | Download | only in shadows
      1 /*
      2  * Copyright (C) 2006 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.xtremelabs.robolectric.shadows;
     18 
     19 import android.content.Context;
     20 import android.database.Cursor;
     21 import android.net.Uri;
     22 import android.view.View;
     23 import android.view.ViewGroup;
     24 import android.widget.ImageView;
     25 import android.widget.SimpleCursorAdapter;
     26 import android.widget.SimpleCursorAdapter.CursorToStringConverter;
     27 import android.widget.SimpleCursorAdapter.ViewBinder;
     28 import android.widget.TextView;
     29 
     30 import com.xtremelabs.robolectric.internal.Implementation;
     31 import com.xtremelabs.robolectric.internal.Implements;
     32 import com.xtremelabs.robolectric.internal.RealObject;
     33 
     34 /**
     35  * An easy adapter to map columns from a cursor to TextViews or ImageViews
     36  * defined in an XML file. You can specify which columns you want, which
     37  * views you want to display the columns, and the XML file that defines
     38  * the appearance of these views.
     39  *
     40  * Binding occurs in two phases. First, if a
     41  * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder} is available,
     42  * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
     43  * is invoked. If the returned value is true, binding has occured. If the
     44  * returned value is false and the view to bind is a TextView,
     45  * {@link #setViewText(TextView, String)} is invoked. If the returned value
     46  * is false and the view to bind is an ImageView,
     47  * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
     48  * binding can be found, an {@link IllegalStateException} is thrown.
     49  *
     50  * If this adapter is used with filtering, for instance in an
     51  * {@link android.widget.AutoCompleteTextView}, you can use the
     52  * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter} and the
     53  * {@link android.widget.FilterQueryProvider} interfaces
     54  * to get control over the filtering process. You can refer to
     55  * {@link #convertToString(android.database.Cursor)} and
     56  * {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
     57  */
     58 @Implements(SimpleCursorAdapter.class)
     59 public class ShadowSimpleCursorAdapter extends ShadowResourceCursorAdapter {
     60 	@RealObject private SimpleCursorAdapter realSimpleCursorAdapter;
     61 
     62     /**
     63      * A list of columns containing the data to bind to the UI.
     64      * This field should be made private, so it is hidden from the SDK.
     65      * {@hide}
     66      */
     67     protected int[] mFrom;
     68     /**
     69      * A list of View ids representing the views to which the data must be bound.
     70      * This field should be made private, so it is hidden from the SDK.
     71      * {@hide}
     72      */
     73     protected int[] mTo;
     74 
     75     private int mStringConversionColumn = -1;
     76     private CursorToStringConverter mCursorToStringConverter;
     77     private ViewBinder mViewBinder;
     78     private String[] mOriginalFrom;
     79 
     80     /**
     81      * Constructor.
     82      *
     83      * @param context The context where the ListView associated with this
     84      *            SimpleListItemFactory is running
     85      * @param layout resource identifier of a layout file that defines the views
     86      *            for this list item. The layout file should include at least
     87      *            those named views defined in "to"
     88      * @param c The database cursor.  Can be null if the cursor is not available yet.
     89      * @param from A list of column names representing the data to bind to the UI.  Can be null
     90      *            if the cursor is not available yet.
     91      * @param to The views that should display column in the "from" parameter.
     92      *            These should all be TextViews. The first N views in this list
     93      *            are given the values of the first N columns in the from
     94      *            parameter.  Can be null if the cursor is not available yet.
     95      */
     96     public void __constructor__(Context context, int layout, Cursor c, String[] from, int[] to) {
     97     	super.__constructor__(context, layout, c);
     98         mTo = to;
     99         mOriginalFrom = from;
    100         findColumns(from);
    101     }
    102 
    103     /**
    104      * Binds all of the field names passed into the "to" parameter of the
    105      * constructor with their corresponding cursor columns as specified in the
    106      * "from" parameter.
    107      *
    108      * Binding occurs in two phases. First, if a
    109      * {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder} is available,
    110      * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
    111      * is invoked. If the returned value is true, binding has occured. If the
    112      * returned value is false and the view to bind is a TextView,
    113      * {@link #setViewText(TextView, String)} is invoked. If the returned value is
    114      * false and the view to bind is an ImageView,
    115      * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
    116      * binding can be found, an {@link IllegalStateException} is thrown.
    117      *
    118      * @throws IllegalStateException if binding cannot occur
    119      *
    120      * @see android.widget.CursorAdapter#bindView(android.view.View,
    121      *      android.content.Context, android.database.Cursor)
    122      * @see #getViewBinder()
    123      * @see #setViewBinder(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder)
    124      * @see #setViewImage(ImageView, String)
    125      * @see #setViewText(TextView, String)
    126      */
    127     @Implementation
    128     public void bindView(View view, Context context, Cursor cursor) {
    129         final ViewBinder binder = mViewBinder;
    130         final int count = mTo.length;
    131         final int[] from = mFrom;
    132         final int[] to = mTo;
    133 
    134         for (int i = 0; i < count; i++) {
    135             final View v = view.findViewById(to[i]);
    136             if (v != null) {
    137                 boolean bound = false;
    138                 if (binder != null) {
    139                     bound = binder.setViewValue(v, cursor, from[i]);
    140                 }
    141 
    142                 if (!bound) {
    143                     String text = cursor.getString(from[i]);
    144                     if (text == null) {
    145                         text = "";
    146                     }
    147 
    148                     if (v instanceof TextView) {
    149                         setViewText((TextView) v, text);
    150                     } else if (v instanceof ImageView) {
    151                         setViewImage((ImageView) v, text);
    152                     } else {
    153                         throw new IllegalStateException(v.getClass().getName() + " is not a " +
    154                                 " view that can be bounds by this SimpleCursorAdapter");
    155                     }
    156                 }
    157             }
    158         }
    159     }
    160 
    161     /**
    162      * Returns the {@link ViewBinder} used to bind data to views.
    163      *
    164      * @return a ViewBinder or null if the binder does not exist
    165      *
    166      * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
    167      * @see #setViewBinder(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.ViewBinder)
    168      */
    169     @Implementation
    170     public ViewBinder getViewBinder() {
    171         return mViewBinder;
    172     }
    173 
    174     /**
    175      * Sets the binder used to bind data to views.
    176      *
    177      * @param viewBinder the binder used to bind data to views, can be null to
    178      *        remove the existing binder
    179      *
    180      * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
    181      * @see #getViewBinder()
    182      */
    183     @Implementation
    184     public void setViewBinder(ViewBinder viewBinder) {
    185         mViewBinder = viewBinder;
    186     }
    187 
    188     /**
    189      * Called by bindView() to set the image for an ImageView but only if
    190      * there is no existing ViewBinder or if the existing ViewBinder cannot
    191      * handle binding to an ImageView.
    192      *
    193      * By default, the value will be treated as an image resource. If the
    194      * value cannot be used as an image resource, the value is used as an
    195      * image Uri.
    196      *
    197      * Intended to be overridden by Adapters that need to filter strings
    198      * retrieved from the database.
    199      *
    200      * @param v ImageView to receive an image
    201      * @param value the value retrieved from the cursor
    202      */
    203     @Implementation
    204     public void setViewImage(ImageView v, String value) {
    205         try {
    206             v.setImageResource(Integer.parseInt(value));
    207         } catch (NumberFormatException nfe) {
    208             v.setImageURI(Uri.parse(value));
    209         }
    210     }
    211 
    212     /**
    213      * Called by bindView() to set the text for a TextView but only if
    214      * there is no existing ViewBinder or if the existing ViewBinder cannot
    215      * handle binding to an TextView.
    216      *
    217      * Intended to be overridden by Adapters that need to filter strings
    218      * retrieved from the database.
    219      *
    220      * @param v TextView to receive text
    221      * @param text the text to be set for the TextView
    222      */
    223     @Implementation
    224     public void setViewText(TextView v, String text) {
    225         v.setText(text);
    226     }
    227 
    228     /**
    229      * Return the index of the column used to get a String representation
    230      * of the Cursor.
    231      *
    232      * @return a valid index in the current Cursor or -1
    233      *
    234      * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
    235      * @see #setStringConversionColumn(int)
    236      * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter)
    237      * @see #getCursorToStringConverter()
    238      */
    239     @Implementation
    240     public int getStringConversionColumn() {
    241         return mStringConversionColumn;
    242     }
    243 
    244     /**
    245      * Defines the index of the column in the Cursor used to get a String
    246      * representation of that Cursor. The column is used to convert the
    247      * Cursor to a String only when the current CursorToStringConverter
    248      * is null.
    249      *
    250      * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
    251      *        conversion mechanism
    252      *
    253      * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
    254      * @see #getStringConversionColumn()
    255      * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter)
    256      * @see #getCursorToStringConverter()
    257      */
    258     @Implementation
    259     public void setStringConversionColumn(int stringConversionColumn) {
    260         mStringConversionColumn = stringConversionColumn;
    261     }
    262 
    263     /**
    264      * Returns the converter used to convert the filtering Cursor
    265      * into a String.
    266      *
    267      * @return null if the converter does not exist or an instance of
    268      *         {@link com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter}
    269      *
    270      * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter)
    271      * @see #getStringConversionColumn()
    272      * @see #setStringConversionColumn(int)
    273      * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
    274      */
    275     @Implementation
    276     public CursorToStringConverter getCursorToStringConverter() {
    277         return mCursorToStringConverter;
    278     }
    279 
    280     /**
    281      * Sets the converter  used to convert the filtering Cursor
    282      * into a String.
    283      *
    284      * @param cursorToStringConverter the Cursor to String converter, or
    285      *        null to remove the converter
    286      *
    287      * @see #setCursorToStringConverter(com.xtremelabs.robolectric.shadows.ShadowSimpleCursorAdapter.CursorToStringConverter)
    288      * @see #getStringConversionColumn()
    289      * @see #setStringConversionColumn(int)
    290      * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
    291      */
    292     @Implementation
    293     public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
    294         mCursorToStringConverter = cursorToStringConverter;
    295     }
    296 
    297     /**
    298      * Returns a CharSequence representation of the specified Cursor as defined
    299      * by the current CursorToStringConverter. If no CursorToStringConverter
    300      * has been set, the String conversion column is used instead. If the
    301      * conversion column is -1, the returned String is empty if the cursor
    302      * is null or Cursor.toString().
    303      *
    304      * @param cursor the Cursor to convert to a CharSequence
    305      *
    306      * @return a non-null CharSequence representing the cursor
    307      */
    308     @Implementation
    309     public CharSequence convertToString(Cursor cursor) {
    310         if (mCursorToStringConverter != null) {
    311             return mCursorToStringConverter.convertToString(cursor);
    312         } else if (mStringConversionColumn > -1) {
    313             return cursor.getString(mStringConversionColumn);
    314         }
    315 
    316         return realSimpleCursorAdapter.convertToString(cursor);
    317     }
    318 
    319     /**
    320      * Create a map from an array of strings to an array of column-id integers in mCursor.
    321      * If mCursor is null, the array will be discarded.
    322      *
    323      * @param from the Strings naming the columns of interest
    324      */
    325     private void findColumns(String[] from) {
    326         if (mCursor != null) {
    327             int i;
    328             int count = from.length;
    329             if (mFrom == null || mFrom.length != count) {
    330                 mFrom = new int[count];
    331             }
    332             for (i = 0; i < count; i++) {
    333                 mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]);
    334             }
    335         } else {
    336             mFrom = null;
    337         }
    338     }
    339 
    340     @Implementation
    341     public void changeCursor(Cursor c) {
    342         realSimpleCursorAdapter.changeCursor(c);
    343         // rescan columns in case cursor layout is different
    344         findColumns(mOriginalFrom);
    345     }
    346 
    347     /**
    348      * Change the cursor and change the column-to-view mappings at the same time.
    349      *
    350      * @param c The database cursor.  Can be null if the cursor is not available yet.
    351      * @param from A list of column names representing the data to bind to the UI.  Can be null
    352      *            if the cursor is not available yet.
    353      * @param to The views that should display column in the "from" parameter.
    354      *            These should all be TextViews. The first N views in this list
    355      *            are given the values of the first N columns in the from
    356      *            parameter.  Can be null if the cursor is not available yet.
    357      */
    358     @Implementation
    359     public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
    360         mOriginalFrom = from;
    361         mTo = to;
    362         realSimpleCursorAdapter.changeCursor(c);
    363         findColumns(mOriginalFrom);
    364     }
    365 
    366     ///////////////////////////////////////////////////////////////////////////////////////////////
    367     // Implementation from CursorAdapter
    368 
    369     /**
    370      * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
    371      */
    372     @Implementation
    373     public View getView(int position, View convertView, ViewGroup parent) {
    374         if (!mDataValid) {
    375             throw new IllegalStateException("this should only be called when the cursor is valid");
    376         }
    377         if (!mCursor.moveToPosition(position)) {
    378             throw new IllegalStateException("couldn't move cursor to position " + position);
    379         }
    380         View v;
    381         if (convertView == null) {
    382             v = newView(mContext, mCursor, parent);
    383         } else {
    384             v = convertView;
    385         }
    386         bindView(v, mContext, mCursor);
    387         return v;
    388     }
    389 
    390     @Implementation
    391     public View getDropDownView(int position, View convertView, ViewGroup parent) {
    392         if (mDataValid) {
    393             mCursor.moveToPosition(position);
    394             View v;
    395             if (convertView == null) {
    396                 v = newDropDownView(mContext, mCursor, parent);
    397             } else {
    398                 v = convertView;
    399             }
    400             bindView(v, mContext, mCursor);
    401             return v;
    402         } else {
    403             return null;
    404         }
    405     }
    406 
    407 }