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
17package com.xtremelabs.robolectric.shadows;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.net.Uri;
22import android.view.View;
23import android.view.ViewGroup;
24import android.widget.ImageView;
25import android.widget.SimpleCursorAdapter;
26import android.widget.SimpleCursorAdapter.CursorToStringConverter;
27import android.widget.SimpleCursorAdapter.ViewBinder;
28import android.widget.TextView;
29
30import com.xtremelabs.robolectric.internal.Implementation;
31import com.xtremelabs.robolectric.internal.Implements;
32import 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)
59public 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}