blob: 600a34c33eb97714f90deb6cc29fd6b4da8698c7 [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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.compat.annotation.UnsupportedAppUsage;
21import android.content.Context;
22import android.content.res.Resources;
23import android.database.ContentObserver;
24import android.database.Cursor;
25import android.database.DataSetObserver;
26import android.os.Handler;
27import android.util.Log;
28import android.view.ContextThemeWrapper;
29import android.view.View;
30import 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 */
41public 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
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 */
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 */
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
172 protected void init(Context context, Cursor c, boolean autoRequery) {
173 init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
174 }
175
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
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
225 public Resources.Theme getDropDownViewTheme() {
226 return mDropDownContext == null ? null : mDropDownContext.getTheme();
227 }
228
229 /**
230 * Returns the cursor.
231 * @return the cursor.
232 */
233 public Cursor getCursor() {
234 return mCursor;
235 }
236
237 /**
238 * @see android.widget.ListAdapter#getCount()
239 */
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 */
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 */
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
276 public boolean hasStableIds() {
277 return true;
278 }
279
280 /**
281 * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
282 */
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
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 */
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 */
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 */
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 */
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 */
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 */
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
437 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
438 if (mFilterQueryProvider != null) {
439 return mFilterQueryProvider.runQuery(constraint);
440 }
441
442 return mCursor;
443 }
444
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 */
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 */
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 */
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 {
496 public ChangeObserver() {
497 super(new Handler());
498 }
499
500 @Override
501 public boolean deliverSelfNotifications() {
502 return true;
503 }
504
505 @Override
506 public void onChange(boolean selfChange) {
507 onContentChanged();
508 }
509 }
510
511 private class MyDataSetObserver extends DataSetObserver {
512 @Override
513 public void onChanged() {
514 mDataValid = true;
515 notifyDataSetChanged();
516 }
517
518 @Override
519 public void onInvalidated() {
520 mDataValid = false;
521 notifyDataSetInvalidated();
522 }
523 }
524
525}