blob: 9fb98db54e72550dc59797264aa142d0324a4bd0 [file] [log] [blame]
Justin Klaassen10d07c82017-09-15 17:58:39 -04001/*
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}