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