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