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.content.Context;
20import android.database.ContentObserver;
21import android.database.Cursor;
22import android.database.DataSetObserver;
23import android.os.Handler;
24import android.util.Config;
25import android.util.Log;
26import android.view.View;
27import android.view.ViewGroup;
28
29/**
30 * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
31 * {@link android.widget.ListView ListView} widget. The Cursor must include
32 * a column named "_id" or this class will not work.
33 */
34public abstract class CursorAdapter extends BaseAdapter implements Filterable,
35        CursorFilter.CursorFilterClient {
36    /**
37     * This field should be made private, so it is hidden from the SDK.
38     * {@hide}
39     */
40    protected boolean mDataValid;
41    /**
42     * This field should be made private, so it is hidden from the SDK.
43     * {@hide}
44     */
45    protected boolean mAutoRequery;
46    /**
47     * This field should be made private, so it is hidden from the SDK.
48     * {@hide}
49     */
50    protected Cursor mCursor;
51    /**
52     * This field should be made private, so it is hidden from the SDK.
53     * {@hide}
54     */
55    protected Context mContext;
56    /**
57     * This field should be made private, so it is hidden from the SDK.
58     * {@hide}
59     */
60    protected int mRowIDColumn;
61    /**
62     * This field should be made private, so it is hidden from the SDK.
63     * {@hide}
64     */
65    protected ChangeObserver mChangeObserver;
66    /**
67     * This field should be made private, so it is hidden from the SDK.
68     * {@hide}
69     */
70    protected DataSetObserver mDataSetObserver = new MyDataSetObserver();
71    /**
72     * This field should be made private, so it is hidden from the SDK.
73     * {@hide}
74     */
75    protected CursorFilter mCursorFilter;
76    /**
77     * This field should be made private, so it is hidden from the SDK.
78     * {@hide}
79     */
80    protected FilterQueryProvider mFilterQueryProvider;
81
82    /**
83     * Constructor. The adapter will call requery() on the cursor whenever
84     * it changes so that the most recent data is always displayed.
85     *
86     * @param c The cursor from which to get the data.
87     * @param context The context
88     */
89    public CursorAdapter(Context context, Cursor c) {
90        init(context, c, true);
91    }
92
93    /**
94     * Constructor
95     * @param c The cursor from which to get the data.
96     * @param context The context
97     * @param autoRequery If true the adapter will call requery() on the
98     *                    cursor whenever it changes so the most recent
99     *                    data is always displayed.
100     */
101    public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
102        init(context, c, autoRequery);
103    }
104
105    protected void init(Context context, Cursor c, boolean autoRequery) {
106        boolean cursorPresent = c != null;
107        mAutoRequery = autoRequery;
108        mCursor = c;
109        mDataValid = cursorPresent;
110        mContext = context;
111        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
112        mChangeObserver = new ChangeObserver();
113        if (cursorPresent) {
114            c.registerContentObserver(mChangeObserver);
115            c.registerDataSetObserver(mDataSetObserver);
116        }
117    }
118
119    /**
120     * Returns the cursor.
121     * @return the cursor.
122     */
123    public Cursor getCursor() {
124        return mCursor;
125    }
126
127    /**
128     * @see android.widget.ListAdapter#getCount()
129     */
130    public int getCount() {
131        if (mDataValid && mCursor != null) {
132            return mCursor.getCount();
133        } else {
134            return 0;
135        }
136    }
137
138    /**
139     * @see android.widget.ListAdapter#getItem(int)
140     */
141    public Object getItem(int position) {
142        if (mDataValid && mCursor != null) {
143            mCursor.moveToPosition(position);
144            return mCursor;
145        } else {
146            return null;
147        }
148    }
149
150    /**
151     * @see android.widget.ListAdapter#getItemId(int)
152     */
153    public long getItemId(int position) {
154        if (mDataValid && mCursor != null) {
155            if (mCursor.moveToPosition(position)) {
156                return mCursor.getLong(mRowIDColumn);
157            } else {
158                return 0;
159            }
160        } else {
161            return 0;
162        }
163    }
164
165    @Override
166    public boolean hasStableIds() {
167        return true;
168    }
169
170    /**
171     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
172     */
173    public View getView(int position, View convertView, ViewGroup parent) {
174        if (!mDataValid) {
175            throw new IllegalStateException("this should only be called when the cursor is valid");
176        }
177        if (!mCursor.moveToPosition(position)) {
178            throw new IllegalStateException("couldn't move cursor to position " + position);
179        }
180        View v;
181        if (convertView == null) {
182            v = newView(mContext, mCursor, parent);
183        } else {
184            v = convertView;
185        }
186        bindView(v, mContext, mCursor);
187        return v;
188    }
189
190    @Override
191    public View getDropDownView(int position, View convertView, ViewGroup parent) {
192        if (mDataValid) {
193            mCursor.moveToPosition(position);
194            View v;
195            if (convertView == null) {
196                v = newDropDownView(mContext, mCursor, parent);
197            } else {
198                v = convertView;
199            }
200            bindView(v, mContext, mCursor);
201            return v;
202        } else {
203            return null;
204        }
205    }
206
207    /**
208     * Makes a new view to hold the data pointed to by cursor.
209     * @param context Interface to application's global information
210     * @param cursor The cursor from which to get the data. The cursor is already
211     * moved to the correct position.
212     * @param parent The parent to which the new view is attached to
213     * @return the newly created view.
214     */
215    public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
216
217    /**
218     * Makes a new drop down view to hold the data pointed to by cursor.
219     * @param context Interface to application's global information
220     * @param cursor The cursor from which to get the data. The cursor is already
221     * moved to the correct position.
222     * @param parent The parent to which the new view is attached to
223     * @return the newly created view.
224     */
225    public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
226        return newView(context, cursor, parent);
227    }
228
229    /**
230     * Bind an existing view to the data pointed to by cursor
231     * @param view Existing view, returned earlier by newView
232     * @param context Interface to application's global information
233     * @param cursor The cursor from which to get the data. The cursor is already
234     * moved to the correct position.
235     */
236    public abstract void bindView(View view, Context context, Cursor cursor);
237
238    /**
239     * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
240     * closed.
241     *
242     * @param cursor the new cursor to be used
243     */
244    public void changeCursor(Cursor cursor) {
245        if (cursor == mCursor) {
246            return;
247        }
248        if (mCursor != null) {
249            mCursor.unregisterContentObserver(mChangeObserver);
250            mCursor.unregisterDataSetObserver(mDataSetObserver);
251            mCursor.close();
252        }
253        mCursor = cursor;
254        if (cursor != null) {
255            cursor.registerContentObserver(mChangeObserver);
256            cursor.registerDataSetObserver(mDataSetObserver);
257            mRowIDColumn = cursor.getColumnIndexOrThrow("_id");
258            mDataValid = true;
259            // notify the observers about the new cursor
260            notifyDataSetChanged();
261        } else {
262            mRowIDColumn = -1;
263            mDataValid = false;
264            // notify the observers about the lack of a data set
265            notifyDataSetInvalidated();
266        }
267    }
268
269    /**
270     * <p>Converts the cursor into a CharSequence. Subclasses should override this
271     * method to convert their results. The default implementation returns an
272     * empty String for null values or the default String representation of
273     * the value.</p>
274     *
275     * @param cursor the cursor to convert to a CharSequence
276     * @return a CharSequence representing the value
277     */
278    public CharSequence convertToString(Cursor cursor) {
279        return cursor == null ? "" : cursor.toString();
280    }
281
282    /**
283     * Runs a query with the specified constraint. This query is requested
284     * by the filter attached to this adapter.
285     *
286     * The query is provided by a
287     * {@link android.widget.FilterQueryProvider}.
288     * If no provider is specified, the current cursor is not filtered and returned.
289     *
290     * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
291     * and the previous cursor is closed.
292     *
293     * This method is always executed on a background thread, not on the
294     * application's main thread (or UI thread.)
295     *
296     * Contract: when constraint is null or empty, the original results,
297     * prior to any filtering, must be returned.
298     *
299     * @param constraint the constraint with which the query must be filtered
300     *
301     * @return a Cursor representing the results of the new query
302     *
303     * @see #getFilter()
304     * @see #getFilterQueryProvider()
305     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
306     */
307    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
308        if (mFilterQueryProvider != null) {
309            return mFilterQueryProvider.runQuery(constraint);
310        }
311
312        return mCursor;
313    }
314
315    public Filter getFilter() {
316        if (mCursorFilter == null) {
317            mCursorFilter = new CursorFilter(this);
318        }
319        return mCursorFilter;
320    }
321
322    /**
323     * Returns the query filter provider used for filtering. When the
324     * provider is null, no filtering occurs.
325     *
326     * @return the current filter query provider or null if it does not exist
327     *
328     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
329     * @see #runQueryOnBackgroundThread(CharSequence)
330     */
331    public FilterQueryProvider getFilterQueryProvider() {
332        return mFilterQueryProvider;
333    }
334
335    /**
336     * Sets the query filter provider used to filter the current Cursor.
337     * The provider's
338     * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
339     * method is invoked when filtering is requested by a client of
340     * this adapter.
341     *
342     * @param filterQueryProvider the filter query provider or null to remove it
343     *
344     * @see #getFilterQueryProvider()
345     * @see #runQueryOnBackgroundThread(CharSequence)
346     */
347    public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
348        mFilterQueryProvider = filterQueryProvider;
349    }
350
351    /**
352     * Called when the {@link ContentObserver} on the cursor receives a change notification.
353     * The default implementation provides the auto-requery logic, but may be overridden by
354     * sub classes.
355     *
356     * @see ContentObserver#onChange(boolean)
357     */
358    protected void onContentChanged() {
359        if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
360            if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
361            mDataValid = mCursor.requery();
362        }
363    }
364
365    private class ChangeObserver extends ContentObserver {
366        public ChangeObserver() {
367            super(new Handler());
368        }
369
370        @Override
371        public boolean deliverSelfNotifications() {
372            return true;
373        }
374
375        @Override
376        public void onChange(boolean selfChange) {
377            onContentChanged();
378        }
379    }
380
381    private class MyDataSetObserver extends DataSetObserver {
382        @Override
383        public void onChanged() {
384            mDataValid = true;
385            notifyDataSetChanged();
386        }
387
388        @Override
389        public void onInvalidated() {
390            mDataValid = false;
391            notifyDataSetInvalidated();
392        }
393    }
394
395}
396