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 android.widget;
18
19import android.annotation.WorkerThread;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.ContentObserver;
23import android.database.Cursor;
24import android.database.DataSetObserver;
25import android.os.Handler;
26import android.util.Log;
27import android.view.ContextThemeWrapper;
28import android.view.View;
29import 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 */
40public 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
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     */
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     */
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
165    protected void init(Context context, Cursor c, boolean autoRequery) {
166        init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
167    }
168
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
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
218    public Resources.Theme getDropDownViewTheme() {
219        return mDropDownContext == null ? null : mDropDownContext.getTheme();
220    }
221
222    /**
223     * Returns the cursor.
224     * @return the cursor.
225     */
226    public Cursor getCursor() {
227        return mCursor;
228    }
229
230    /**
231     * @see android.widget.ListAdapter#getCount()
232     */
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     */
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     */
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
269    public boolean hasStableIds() {
270        return true;
271    }
272
273    /**
274     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
275     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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
430    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
431        if (mFilterQueryProvider != null) {
432            return mFilterQueryProvider.runQuery(constraint);
433        }
434
435        return mCursor;
436    }
437
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     */
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     */
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     */
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 {
489        public ChangeObserver() {
490            super(new Handler());
491        }
492
493        @Override
494        public boolean deliverSelfNotifications() {
495            return true;
496        }
497
498        @Override
499        public void onChange(boolean selfChange) {
500            onContentChanged();
501        }
502    }
503
504    private class MyDataSetObserver extends DataSetObserver {
505        @Override
506        public void onChanged() {
507            mDataValid = true;
508            notifyDataSetChanged();
509        }
510
511        @Override
512        public void onInvalidated() {
513            mDataValid = false;
514            notifyDataSetInvalidated();
515        }
516    }
517
518}
519