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