• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.annotation.WorkerThread;
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.database.ContentObserver;
23 import android.database.Cursor;
24 import android.database.DataSetObserver;
25 import android.os.Handler;
26 import android.util.Log;
27 import android.view.ContextThemeWrapper;
28 import android.view.View;
29 import android.view.ViewGroup;
30 
31 /**
32  * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
33  * {@link android.widget.ListView ListView} widget.
34  * <p>
35  * The Cursor must include a column named "_id" or this class will not work.
36  * Additionally, using {@link android.database.MergeCursor} with this class will
37  * not work if the merged Cursors have overlapping values in their "_id"
38  * columns.
39  */
40 public abstract class CursorAdapter extends BaseAdapter implements Filterable,
41         CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {
42     /**
43      * This field should be made private, so it is hidden from the SDK.
44      * {@hide}
45      */
46     protected boolean mDataValid;
47     /**
48      * This field should be made private, so it is hidden from the SDK.
49      * {@hide}
50      */
51     protected boolean mAutoRequery;
52     /**
53      * This field should be made private, so it is hidden from the SDK.
54      * {@hide}
55      */
56     protected Cursor mCursor;
57     /**
58      * This field should be made private, so it is hidden from the SDK.
59      * {@hide}
60      */
61     protected Context mContext;
62     /**
63      * Context used for {@link #getDropDownView(int, View, ViewGroup)}.
64      * {@hide}
65      */
66     protected Context mDropDownContext;
67     /**
68      * This field should be made private, so it is hidden from the SDK.
69      * {@hide}
70      */
71     protected int mRowIDColumn;
72     /**
73      * This field should be made private, so it is hidden from the SDK.
74      * {@hide}
75      */
76     protected ChangeObserver mChangeObserver;
77     /**
78      * This field should be made private, so it is hidden from the SDK.
79      * {@hide}
80      */
81     protected DataSetObserver mDataSetObserver;
82     /**
83      * This field should be made private, so it is hidden from the SDK.
84      * {@hide}
85      */
86     protected CursorFilter mCursorFilter;
87     /**
88      * This field should be made private, so it is hidden from the SDK.
89      * {@hide}
90      */
91     protected FilterQueryProvider mFilterQueryProvider;
92 
93     /**
94      * If set the adapter will call requery() on the cursor whenever a content change
95      * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
96      *
97      * @deprecated This option is discouraged, as it results in Cursor queries
98      * being performed on the application's UI thread and thus can cause poor
99      * responsiveness or even Application Not Responding errors.  As an alternative,
100      * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
101      */
102     @Deprecated
103     public static final int FLAG_AUTO_REQUERY = 0x01;
104 
105     /**
106      * If set the adapter will register a content observer on the cursor and will call
107      * {@link #onContentChanged()} when a notification comes in.  Be careful when
108      * using this flag: you will need to unset the current Cursor from the adapter
109      * to avoid leaks due to its registered observers.  This flag is not needed
110      * when using a CursorAdapter with a
111      * {@link android.content.CursorLoader}.
112      */
113     public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
114 
115     /**
116      * Constructor that always enables auto-requery.
117      *
118      * @deprecated This option is discouraged, as it results in Cursor queries
119      * being performed on the application's UI thread and thus can cause poor
120      * responsiveness or even Application Not Responding errors.  As an alternative,
121      * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
122      *
123      * @param c The cursor from which to get the data.
124      * @param context The context
125      */
126     @Deprecated
CursorAdapter(Context context, Cursor c)127     public CursorAdapter(Context context, Cursor c) {
128         init(context, c, FLAG_AUTO_REQUERY);
129     }
130 
131     /**
132      * Constructor that allows control over auto-requery.  It is recommended
133      * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
134      * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
135      * will always be set.
136      *
137      * @param c The cursor from which to get the data.
138      * @param context The context
139      * @param autoRequery If true the adapter will call requery() on the
140      *                    cursor whenever it changes so the most recent
141      *                    data is always displayed.  Using true here is discouraged.
142      */
CursorAdapter(Context context, Cursor c, boolean autoRequery)143     public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
144         init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
145     }
146 
147     /**
148      * Recommended constructor.
149      *
150      * @param c The cursor from which to get the data.
151      * @param context The context
152      * @param flags Flags used to determine the behavior of the adapter; may
153      * be any combination of {@link #FLAG_AUTO_REQUERY} and
154      * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
155      */
CursorAdapter(Context context, Cursor c, int flags)156     public CursorAdapter(Context context, Cursor c, int flags) {
157         init(context, c, flags);
158     }
159 
160     /**
161      * @deprecated Don't use this, use the normal constructor.  This will
162      * be removed in the future.
163      */
164     @Deprecated
init(Context context, Cursor c, boolean autoRequery)165     protected void init(Context context, Cursor c, boolean autoRequery) {
166         init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
167     }
168 
init(Context context, Cursor c, int flags)169     void init(Context context, Cursor c, int flags) {
170         if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
171             flags |= FLAG_REGISTER_CONTENT_OBSERVER;
172             mAutoRequery = true;
173         } else {
174             mAutoRequery = false;
175         }
176         boolean cursorPresent = c != null;
177         mCursor = c;
178         mDataValid = cursorPresent;
179         mContext = context;
180         mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
181         if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
182             mChangeObserver = new ChangeObserver();
183             mDataSetObserver = new MyDataSetObserver();
184         } else {
185             mChangeObserver = null;
186             mDataSetObserver = null;
187         }
188 
189         if (cursorPresent) {
190             if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
191             if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
192         }
193     }
194 
195     /**
196      * Sets the {@link Resources.Theme} against which drop-down views are
197      * inflated.
198      * <p>
199      * By default, drop-down views are inflated against the theme of the
200      * {@link Context} passed to the adapter's constructor.
201      *
202      * @param theme the theme against which to inflate drop-down views or
203      *              {@code null} to use the theme from the adapter's context
204      * @see #newDropDownView(Context, Cursor, ViewGroup)
205      */
206     @Override
setDropDownViewTheme(Resources.Theme theme)207     public void setDropDownViewTheme(Resources.Theme theme) {
208         if (theme == null) {
209             mDropDownContext = null;
210         } else if (theme == mContext.getTheme()) {
211             mDropDownContext = mContext;
212         } else {
213             mDropDownContext = new ContextThemeWrapper(mContext, theme);
214         }
215     }
216 
217     @Override
getDropDownViewTheme()218     public Resources.Theme getDropDownViewTheme() {
219         return mDropDownContext == null ? null : mDropDownContext.getTheme();
220     }
221 
222     /**
223      * Returns the cursor.
224      * @return the cursor.
225      */
getCursor()226     public Cursor getCursor() {
227         return mCursor;
228     }
229 
230     /**
231      * @see android.widget.ListAdapter#getCount()
232      */
getCount()233     public int getCount() {
234         if (mDataValid && mCursor != null) {
235             return mCursor.getCount();
236         } else {
237             return 0;
238         }
239     }
240 
241     /**
242      * @see android.widget.ListAdapter#getItem(int)
243      */
getItem(int position)244     public Object getItem(int position) {
245         if (mDataValid && mCursor != null) {
246             mCursor.moveToPosition(position);
247             return mCursor;
248         } else {
249             return null;
250         }
251     }
252 
253     /**
254      * @see android.widget.ListAdapter#getItemId(int)
255      */
getItemId(int position)256     public long getItemId(int position) {
257         if (mDataValid && mCursor != null) {
258             if (mCursor.moveToPosition(position)) {
259                 return mCursor.getLong(mRowIDColumn);
260             } else {
261                 return 0;
262             }
263         } else {
264             return 0;
265         }
266     }
267 
268     @Override
hasStableIds()269     public boolean hasStableIds() {
270         return true;
271     }
272 
273     /**
274      * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
275      */
getView(int position, View convertView, ViewGroup parent)276     public View getView(int position, View convertView, ViewGroup parent) {
277         if (!mDataValid) {
278             throw new IllegalStateException("this should only be called when the cursor is valid");
279         }
280         if (!mCursor.moveToPosition(position)) {
281             throw new IllegalStateException("couldn't move cursor to position " + position);
282         }
283         View v;
284         if (convertView == null) {
285             v = newView(mContext, mCursor, parent);
286         } else {
287             v = convertView;
288         }
289         bindView(v, mContext, mCursor);
290         return v;
291     }
292 
293     @Override
getDropDownView(int position, View convertView, ViewGroup parent)294     public View getDropDownView(int position, View convertView, ViewGroup parent) {
295         if (mDataValid) {
296             final Context context = mDropDownContext == null ? mContext : mDropDownContext;
297             mCursor.moveToPosition(position);
298             final View v;
299             if (convertView == null) {
300                 v = newDropDownView(context, mCursor, parent);
301             } else {
302                 v = convertView;
303             }
304             bindView(v, context, mCursor);
305             return v;
306         } else {
307             return null;
308         }
309     }
310 
311     /**
312      * Makes a new view to hold the data pointed to by cursor.
313      * @param context Interface to application's global information
314      * @param cursor The cursor from which to get the data. The cursor is already
315      * moved to the correct position.
316      * @param parent The parent to which the new view is attached to
317      * @return the newly created view.
318      */
newView(Context context, Cursor cursor, ViewGroup parent)319     public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
320 
321     /**
322      * Makes a new drop down view to hold the data pointed to by cursor.
323      * @param context Interface to application's global information
324      * @param cursor The cursor from which to get the data. The cursor is already
325      * moved to the correct position.
326      * @param parent The parent to which the new view is attached to
327      * @return the newly created view.
328      */
newDropDownView(Context context, Cursor cursor, ViewGroup parent)329     public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
330         return newView(context, cursor, parent);
331     }
332 
333     /**
334      * Bind an existing view to the data pointed to by cursor
335      * @param view Existing view, returned earlier by newView
336      * @param context Interface to application's global information
337      * @param cursor The cursor from which to get the data. The cursor is already
338      * moved to the correct position.
339      */
bindView(View view, Context context, Cursor cursor)340     public abstract void bindView(View view, Context context, Cursor cursor);
341 
342     /**
343      * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
344      * closed.
345      *
346      * @param cursor The new cursor to be used
347      */
changeCursor(Cursor cursor)348     public void changeCursor(Cursor cursor) {
349         Cursor old = swapCursor(cursor);
350         if (old != null) {
351             old.close();
352         }
353     }
354 
355     /**
356      * Swap in a new Cursor, returning the old Cursor.  Unlike
357      * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
358      * closed.
359      *
360      * @param newCursor The new cursor to be used.
361      * @return Returns the previously set Cursor, or null if there wasa not one.
362      * If the given new Cursor is the same instance is the previously set
363      * Cursor, null is also returned.
364      */
swapCursor(Cursor newCursor)365     public Cursor swapCursor(Cursor newCursor) {
366         if (newCursor == mCursor) {
367             return null;
368         }
369         Cursor oldCursor = mCursor;
370         if (oldCursor != null) {
371             if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
372             if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
373         }
374         mCursor = newCursor;
375         if (newCursor != null) {
376             if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
377             if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
378             mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
379             mDataValid = true;
380             // notify the observers about the new cursor
381             notifyDataSetChanged();
382         } else {
383             mRowIDColumn = -1;
384             mDataValid = false;
385             // notify the observers about the lack of a data set
386             notifyDataSetInvalidated();
387         }
388         return oldCursor;
389     }
390 
391     /**
392      * <p>Converts the cursor into a CharSequence. Subclasses should override this
393      * method to convert their results. The default implementation returns an
394      * empty String for null values or the default String representation of
395      * the value.</p>
396      *
397      * @param cursor the cursor to convert to a CharSequence
398      * @return a CharSequence representing the value
399      */
convertToString(Cursor cursor)400     public CharSequence convertToString(Cursor cursor) {
401         return cursor == null ? "" : cursor.toString();
402     }
403 
404     /**
405      * Runs a query with the specified constraint. This query is requested
406      * by the filter attached to this adapter.
407      *
408      * The query is provided by a
409      * {@link android.widget.FilterQueryProvider}.
410      * If no provider is specified, the current cursor is not filtered and returned.
411      *
412      * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
413      * and the previous cursor is closed.
414      *
415      * This method is always executed on a background thread, not on the
416      * application's main thread (or UI thread.)
417      *
418      * Contract: when constraint is null or empty, the original results,
419      * prior to any filtering, must be returned.
420      *
421      * @param constraint the constraint with which the query must be filtered
422      *
423      * @return a Cursor representing the results of the new query
424      *
425      * @see #getFilter()
426      * @see #getFilterQueryProvider()
427      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
428      */
429     @WorkerThread
runQueryOnBackgroundThread(CharSequence constraint)430     public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
431         if (mFilterQueryProvider != null) {
432             return mFilterQueryProvider.runQuery(constraint);
433         }
434 
435         return mCursor;
436     }
437 
getFilter()438     public Filter getFilter() {
439         if (mCursorFilter == null) {
440             mCursorFilter = new CursorFilter(this);
441         }
442         return mCursorFilter;
443     }
444 
445     /**
446      * Returns the query filter provider used for filtering. When the
447      * provider is null, no filtering occurs.
448      *
449      * @return the current filter query provider or null if it does not exist
450      *
451      * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
452      * @see #runQueryOnBackgroundThread(CharSequence)
453      */
getFilterQueryProvider()454     public FilterQueryProvider getFilterQueryProvider() {
455         return mFilterQueryProvider;
456     }
457 
458     /**
459      * Sets the query filter provider used to filter the current Cursor.
460      * The provider's
461      * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
462      * method is invoked when filtering is requested by a client of
463      * this adapter.
464      *
465      * @param filterQueryProvider the filter query provider or null to remove it
466      *
467      * @see #getFilterQueryProvider()
468      * @see #runQueryOnBackgroundThread(CharSequence)
469      */
setFilterQueryProvider(FilterQueryProvider filterQueryProvider)470     public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
471         mFilterQueryProvider = filterQueryProvider;
472     }
473 
474     /**
475      * Called when the {@link ContentObserver} on the cursor receives a change notification.
476      * The default implementation provides the auto-requery logic, but may be overridden by
477      * sub classes.
478      *
479      * @see ContentObserver#onChange(boolean)
480      */
onContentChanged()481     protected void onContentChanged() {
482         if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
483             if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
484             mDataValid = mCursor.requery();
485         }
486     }
487 
488     private class ChangeObserver extends ContentObserver {
ChangeObserver()489         public ChangeObserver() {
490             super(new Handler());
491         }
492 
493         @Override
deliverSelfNotifications()494         public boolean deliverSelfNotifications() {
495             return true;
496         }
497 
498         @Override
onChange(boolean selfChange)499         public void onChange(boolean selfChange) {
500             onContentChanged();
501         }
502     }
503 
504     private class MyDataSetObserver extends DataSetObserver {
505         @Override
onChanged()506         public void onChanged() {
507             mDataValid = true;
508             notifyDataSetChanged();
509         }
510 
511         @Override
onInvalidated()512         public void onInvalidated() {
513             mDataValid = false;
514             notifyDataSetInvalidated();
515         }
516     }
517 
518 }
519