1/* 2 * Copyright (C) 2011 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.support.v4.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.Log; 25import android.view.View; 26import android.view.ViewGroup; 27import android.widget.BaseAdapter; 28import android.widget.Filter; 29import android.widget.FilterQueryProvider; 30import android.widget.Filterable; 31 32/** 33 * Static library support version of the framework's {@link android.widget.CursorAdapter}. 34 * Used to write apps that run on platforms prior to Android 3.0. When running 35 * on Android 3.0 or above, this implementation is still used; it does not try 36 * to switch to the framework's implementation. See the framework SDK 37 * documentation for a class overview. 38 */ 39public abstract class CursorAdapter extends BaseAdapter implements Filterable, 40 CursorFilter.CursorFilterClient { 41 /** 42 * This field should be made private, so it is hidden from the SDK. 43 * {@hide} 44 */ 45 protected boolean mDataValid; 46 /** 47 * This field should be made private, so it is hidden from the SDK. 48 * {@hide} 49 */ 50 protected boolean mAutoRequery; 51 /** 52 * This field should be made private, so it is hidden from the SDK. 53 * {@hide} 54 */ 55 protected Cursor mCursor; 56 /** 57 * This field should be made private, so it is hidden from the SDK. 58 * {@hide} 59 */ 60 protected Context mContext; 61 /** 62 * This field should be made private, so it is hidden from the SDK. 63 * {@hide} 64 */ 65 protected int mRowIDColumn; 66 /** 67 * This field should be made private, so it is hidden from the SDK. 68 * {@hide} 69 */ 70 protected ChangeObserver mChangeObserver; 71 /** 72 * This field should be made private, so it is hidden from the SDK. 73 * {@hide} 74 */ 75 protected DataSetObserver mDataSetObserver; 76 /** 77 * This field should be made private, so it is hidden from the SDK. 78 * {@hide} 79 */ 80 protected CursorFilter mCursorFilter; 81 /** 82 * This field should be made private, so it is hidden from the SDK. 83 * {@hide} 84 */ 85 protected FilterQueryProvider mFilterQueryProvider; 86 87 /** 88 * If set the adapter will call requery() on the cursor whenever a content change 89 * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}. 90 * 91 * @deprecated This option is discouraged, as it results in Cursor queries 92 * being performed on the application's UI thread and thus can cause poor 93 * responsiveness or even Application Not Responding errors. As an alternative, 94 * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. 95 */ 96 @Deprecated 97 public static final int FLAG_AUTO_REQUERY = 0x01; 98 99 /** 100 * If set the adapter will register a content observer on the cursor and will call 101 * {@link #onContentChanged()} when a notification comes in. Be careful when 102 * using this flag: you will need to unset the current Cursor from the adapter 103 * to avoid leaks due to its registered observers. This flag is not needed 104 * when using a CursorAdapter with a 105 * {@link android.content.CursorLoader}. 106 */ 107 public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; 108 109 /** 110 * Constructor that always enables auto-requery. 111 * 112 * @deprecated This option is discouraged, as it results in Cursor queries 113 * being performed on the application's UI thread and thus can cause poor 114 * responsiveness or even Application Not Responding errors. As an alternative, 115 * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. 116 * 117 * @param c The cursor from which to get the data. 118 * @param context The context 119 */ 120 @Deprecated 121 public CursorAdapter(Context context, Cursor c) { 122 init(context, c, FLAG_AUTO_REQUERY); 123 } 124 125 /** 126 * Constructor that allows control over auto-requery. It is recommended 127 * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}. 128 * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} 129 * will always be set. 130 * 131 * @param c The cursor from which to get the data. 132 * @param context The context 133 * @param autoRequery If true the adapter will call requery() on the 134 * cursor whenever it changes so the most recent 135 * data is always displayed. Using true here is discouraged. 136 */ 137 public CursorAdapter(Context context, Cursor c, boolean autoRequery) { 138 init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); 139 } 140 141 /** 142 * Recommended constructor. 143 * 144 * @param c The cursor from which to get the data. 145 * @param context The context 146 * @param flags Flags used to determine the behavior of the adapter; may 147 * be any combination of {@link #FLAG_AUTO_REQUERY} and 148 * {@link #FLAG_REGISTER_CONTENT_OBSERVER}. 149 */ 150 public CursorAdapter(Context context, Cursor c, int flags) { 151 init(context, c, flags); 152 } 153 154 /** 155 * @deprecated Don't use this, use the normal constructor. This will 156 * be removed in the future. 157 */ 158 @Deprecated 159 protected void init(Context context, Cursor c, boolean autoRequery) { 160 init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); 161 } 162 163 void init(Context context, Cursor c, int flags) { 164 if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { 165 flags |= FLAG_REGISTER_CONTENT_OBSERVER; 166 mAutoRequery = true; 167 } else { 168 mAutoRequery = false; 169 } 170 boolean cursorPresent = c != null; 171 mCursor = c; 172 mDataValid = cursorPresent; 173 mContext = context; 174 mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; 175 if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { 176 mChangeObserver = new ChangeObserver(); 177 mDataSetObserver = new MyDataSetObserver(); 178 } else { 179 mChangeObserver = null; 180 mDataSetObserver = null; 181 } 182 183 if (cursorPresent) { 184 if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); 185 if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); 186 } 187 } 188 189 /** 190 * Returns the cursor. 191 * @return the cursor. 192 */ 193 public Cursor getCursor() { 194 return mCursor; 195 } 196 197 /** 198 * @see android.widget.ListAdapter#getCount() 199 */ 200 public int getCount() { 201 if (mDataValid && mCursor != null) { 202 return mCursor.getCount(); 203 } else { 204 return 0; 205 } 206 } 207 208 /** 209 * @see android.widget.ListAdapter#getItem(int) 210 */ 211 public Object getItem(int position) { 212 if (mDataValid && mCursor != null) { 213 mCursor.moveToPosition(position); 214 return mCursor; 215 } else { 216 return null; 217 } 218 } 219 220 /** 221 * @see android.widget.ListAdapter#getItemId(int) 222 */ 223 public long getItemId(int position) { 224 if (mDataValid && mCursor != null) { 225 if (mCursor.moveToPosition(position)) { 226 return mCursor.getLong(mRowIDColumn); 227 } else { 228 return 0; 229 } 230 } else { 231 return 0; 232 } 233 } 234 235 @Override 236 public boolean hasStableIds() { 237 return true; 238 } 239 240 /** 241 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 242 */ 243 public View getView(int position, View convertView, ViewGroup parent) { 244 if (!mDataValid) { 245 throw new IllegalStateException("this should only be called when the cursor is valid"); 246 } 247 if (!mCursor.moveToPosition(position)) { 248 throw new IllegalStateException("couldn't move cursor to position " + position); 249 } 250 View v; 251 if (convertView == null) { 252 v = newView(mContext, mCursor, parent); 253 } else { 254 v = convertView; 255 } 256 bindView(v, mContext, mCursor); 257 return v; 258 } 259 260 @Override 261 public View getDropDownView(int position, View convertView, ViewGroup parent) { 262 if (mDataValid) { 263 mCursor.moveToPosition(position); 264 View v; 265 if (convertView == null) { 266 v = newDropDownView(mContext, mCursor, parent); 267 } else { 268 v = convertView; 269 } 270 bindView(v, mContext, mCursor); 271 return v; 272 } else { 273 return null; 274 } 275 } 276 277 /** 278 * Makes a new view to hold the data pointed to by cursor. 279 * @param context Interface to application's global information 280 * @param cursor The cursor from which to get the data. The cursor is already 281 * moved to the correct position. 282 * @param parent The parent to which the new view is attached to 283 * @return the newly created view. 284 */ 285 public abstract View newView(Context context, Cursor cursor, ViewGroup parent); 286 287 /** 288 * Makes a new drop down view to hold the data pointed to by cursor. 289 * @param context Interface to application's global information 290 * @param cursor The cursor from which to get the data. The cursor is already 291 * moved to the correct position. 292 * @param parent The parent to which the new view is attached to 293 * @return the newly created view. 294 */ 295 public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { 296 return newView(context, cursor, parent); 297 } 298 299 /** 300 * Bind an existing view to the data pointed to by cursor 301 * @param view Existing view, returned earlier by newView 302 * @param context Interface to application's global information 303 * @param cursor The cursor from which to get the data. The cursor is already 304 * moved to the correct position. 305 */ 306 public abstract void bindView(View view, Context context, Cursor cursor); 307 308 /** 309 * Change the underlying cursor to a new cursor. If there is an existing cursor it will be 310 * closed. 311 * 312 * @param cursor The new cursor to be used 313 */ 314 public void changeCursor(Cursor cursor) { 315 Cursor old = swapCursor(cursor); 316 if (old != null) { 317 old.close(); 318 } 319 } 320 321 /** 322 * Swap in a new Cursor, returning the old Cursor. Unlike 323 * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em> 324 * closed. 325 * 326 * @param newCursor The new cursor to be used. 327 * @return Returns the previously set Cursor, or null if there wasa not one. 328 * If the given new Cursor is the same instance is the previously set 329 * Cursor, null is also returned. 330 */ 331 public Cursor swapCursor(Cursor newCursor) { 332 if (newCursor == mCursor) { 333 return null; 334 } 335 Cursor oldCursor = mCursor; 336 if (oldCursor != null) { 337 if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); 338 if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); 339 } 340 mCursor = newCursor; 341 if (newCursor != null) { 342 if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); 343 if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); 344 mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); 345 mDataValid = true; 346 // notify the observers about the new cursor 347 notifyDataSetChanged(); 348 } else { 349 mRowIDColumn = -1; 350 mDataValid = false; 351 // notify the observers about the lack of a data set 352 notifyDataSetInvalidated(); 353 } 354 return oldCursor; 355 } 356 357 /** 358 * <p>Converts the cursor into a CharSequence. Subclasses should override this 359 * method to convert their results. The default implementation returns an 360 * empty String for null values or the default String representation of 361 * the value.</p> 362 * 363 * @param cursor the cursor to convert to a CharSequence 364 * @return a CharSequence representing the value 365 */ 366 public CharSequence convertToString(Cursor cursor) { 367 return cursor == null ? "" : cursor.toString(); 368 } 369 370 /** 371 * Runs a query with the specified constraint. This query is requested 372 * by the filter attached to this adapter. 373 * 374 * The query is provided by a 375 * {@link android.widget.FilterQueryProvider}. 376 * If no provider is specified, the current cursor is not filtered and returned. 377 * 378 * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} 379 * and the previous cursor is closed. 380 * 381 * This method is always executed on a background thread, not on the 382 * application's main thread (or UI thread.) 383 * 384 * Contract: when constraint is null or empty, the original results, 385 * prior to any filtering, must be returned. 386 * 387 * @param constraint the constraint with which the query must be filtered 388 * 389 * @return a Cursor representing the results of the new query 390 * 391 * @see #getFilter() 392 * @see #getFilterQueryProvider() 393 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 394 */ 395 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 396 if (mFilterQueryProvider != null) { 397 return mFilterQueryProvider.runQuery(constraint); 398 } 399 400 return mCursor; 401 } 402 403 public Filter getFilter() { 404 if (mCursorFilter == null) { 405 mCursorFilter = new CursorFilter(this); 406 } 407 return mCursorFilter; 408 } 409 410 /** 411 * Returns the query filter provider used for filtering. When the 412 * provider is null, no filtering occurs. 413 * 414 * @return the current filter query provider or null if it does not exist 415 * 416 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 417 * @see #runQueryOnBackgroundThread(CharSequence) 418 */ 419 public FilterQueryProvider getFilterQueryProvider() { 420 return mFilterQueryProvider; 421 } 422 423 /** 424 * Sets the query filter provider used to filter the current Cursor. 425 * The provider's 426 * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} 427 * method is invoked when filtering is requested by a client of 428 * this adapter. 429 * 430 * @param filterQueryProvider the filter query provider or null to remove it 431 * 432 * @see #getFilterQueryProvider() 433 * @see #runQueryOnBackgroundThread(CharSequence) 434 */ 435 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { 436 mFilterQueryProvider = filterQueryProvider; 437 } 438 439 /** 440 * Called when the {@link ContentObserver} on the cursor receives a change notification. 441 * The default implementation provides the auto-requery logic, but may be overridden by 442 * sub classes. 443 * 444 * @see ContentObserver#onChange(boolean) 445 */ 446 protected void onContentChanged() { 447 if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { 448 if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); 449 mDataValid = mCursor.requery(); 450 } 451 } 452 453 private class ChangeObserver extends ContentObserver { 454 public ChangeObserver() { 455 super(new Handler()); 456 } 457 458 @Override 459 public boolean deliverSelfNotifications() { 460 return true; 461 } 462 463 @Override 464 public void onChange(boolean selfChange) { 465 onContentChanged(); 466 } 467 } 468 469 private class MyDataSetObserver extends DataSetObserver { 470 @Override 471 public void onChanged() { 472 mDataValid = true; 473 notifyDataSetChanged(); 474 } 475 476 @Override 477 public void onInvalidated() { 478 mDataValid = false; 479 notifyDataSetInvalidated(); 480 } 481 } 482 483} 484