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