CursorAdapter.java revision b9ead4a91599ca63e947f74f83b67a58bda64a82
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.content.res.Resources; 21import android.database.ContentObserver; 22import android.database.Cursor; 23import android.database.DataSetObserver; 24import android.os.Handler; 25import android.util.Log; 26import android.view.ContextThemeWrapper; 27import android.view.LayoutInflater; 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, Spinner.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 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 430 if (mFilterQueryProvider != null) { 431 return mFilterQueryProvider.runQuery(constraint); 432 } 433 434 return mCursor; 435 } 436 437 public Filter getFilter() { 438 if (mCursorFilter == null) { 439 mCursorFilter = new CursorFilter(this); 440 } 441 return mCursorFilter; 442 } 443 444 /** 445 * Returns the query filter provider used for filtering. When the 446 * provider is null, no filtering occurs. 447 * 448 * @return the current filter query provider or null if it does not exist 449 * 450 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 451 * @see #runQueryOnBackgroundThread(CharSequence) 452 */ 453 public FilterQueryProvider getFilterQueryProvider() { 454 return mFilterQueryProvider; 455 } 456 457 /** 458 * Sets the query filter provider used to filter the current Cursor. 459 * The provider's 460 * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} 461 * method is invoked when filtering is requested by a client of 462 * this adapter. 463 * 464 * @param filterQueryProvider the filter query provider or null to remove it 465 * 466 * @see #getFilterQueryProvider() 467 * @see #runQueryOnBackgroundThread(CharSequence) 468 */ 469 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { 470 mFilterQueryProvider = filterQueryProvider; 471 } 472 473 /** 474 * Called when the {@link ContentObserver} on the cursor receives a change notification. 475 * The default implementation provides the auto-requery logic, but may be overridden by 476 * sub classes. 477 * 478 * @see ContentObserver#onChange(boolean) 479 */ 480 protected void onContentChanged() { 481 if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { 482 if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); 483 mDataValid = mCursor.requery(); 484 } 485 } 486 487 private class ChangeObserver extends ContentObserver { 488 public ChangeObserver() { 489 super(new Handler()); 490 } 491 492 @Override 493 public boolean deliverSelfNotifications() { 494 return true; 495 } 496 497 @Override 498 public void onChange(boolean selfChange) { 499 onContentChanged(); 500 } 501 } 502 503 private class MyDataSetObserver extends DataSetObserver { 504 @Override 505 public void onChanged() { 506 mDataValid = true; 507 notifyDataSetChanged(); 508 } 509 510 @Override 511 public void onInvalidated() { 512 mDataValid = false; 513 notifyDataSetInvalidated(); 514 } 515 } 516 517} 518