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 17 package android.widget; 18 19 import android.annotation.WorkerThread; 20 import android.compat.annotation.UnsupportedAppUsage; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.database.ContentObserver; 24 import android.database.Cursor; 25 import android.database.DataSetObserver; 26 import android.os.Handler; 27 import android.util.Log; 28 import android.view.ContextThemeWrapper; 29 import android.view.View; 30 import 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 */ 41 public 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 CursorAdapter(Context context, Cursor c)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 */ CursorAdapter(Context context, Cursor c, boolean autoRequery)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 */ CursorAdapter(Context context, Cursor c, int flags)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 init(Context context, Cursor c, boolean autoRequery)172 protected void init(Context context, Cursor c, boolean autoRequery) { 173 init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); 174 } 175 init(Context context, Cursor c, int flags)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 setDropDownViewTheme(Resources.Theme theme)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 getDropDownViewTheme()225 public Resources.Theme getDropDownViewTheme() { 226 return mDropDownContext == null ? null : mDropDownContext.getTheme(); 227 } 228 229 /** 230 * Returns the cursor. 231 * @return the cursor. 232 */ getCursor()233 public Cursor getCursor() { 234 return mCursor; 235 } 236 237 /** 238 * @see android.widget.ListAdapter#getCount() 239 */ getCount()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 */ getItem(int position)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 */ getItemId(int position)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 hasStableIds()276 public boolean hasStableIds() { 277 return true; 278 } 279 280 /** 281 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 282 */ getView(int position, View convertView, ViewGroup parent)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 getDropDownView(int position, View convertView, ViewGroup parent)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 */ newView(Context context, Cursor cursor, ViewGroup parent)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 */ newDropDownView(Context context, Cursor cursor, ViewGroup parent)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 */ bindView(View view, Context context, Cursor cursor)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 */ changeCursor(Cursor cursor)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 */ swapCursor(Cursor newCursor)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 */ convertToString(Cursor cursor)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 runQueryOnBackgroundThread(CharSequence constraint)437 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 438 if (mFilterQueryProvider != null) { 439 return mFilterQueryProvider.runQuery(constraint); 440 } 441 442 return mCursor; 443 } 444 getFilter()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 */ getFilterQueryProvider()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 */ setFilterQueryProvider(FilterQueryProvider filterQueryProvider)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 */ onContentChanged()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 { ChangeObserver()496 public ChangeObserver() { 497 super(new Handler()); 498 } 499 500 @Override deliverSelfNotifications()501 public boolean deliverSelfNotifications() { 502 return true; 503 } 504 505 @Override onChange(boolean selfChange)506 public void onChange(boolean selfChange) { 507 onContentChanged(); 508 } 509 } 510 511 private class MyDataSetObserver extends DataSetObserver { 512 @Override onChanged()513 public void onChanged() { 514 mDataValid = true; 515 notifyDataSetChanged(); 516 } 517 518 @Override onInvalidated()519 public void onInvalidated() { 520 mDataValid = false; 521 notifyDataSetInvalidated(); 522 } 523 } 524 525 } 526