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.database.ContentObserver; 21import android.database.Cursor; 22import android.database.DataSetObserver; 23import android.os.Handler; 24import android.util.Config; 25import android.util.Log; 26import android.view.View; 27import android.view.ViewGroup; 28 29/** 30 * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a 31 * {@link android.widget.ListView ListView} widget. The Cursor must include 32 * a column named "_id" or this class will not work. 33 */ 34public abstract class CursorAdapter extends BaseAdapter implements Filterable, 35 CursorFilter.CursorFilterClient { 36 /** 37 * This field should be made private, so it is hidden from the SDK. 38 * {@hide} 39 */ 40 protected boolean mDataValid; 41 /** 42 * This field should be made private, so it is hidden from the SDK. 43 * {@hide} 44 */ 45 protected boolean mAutoRequery; 46 /** 47 * This field should be made private, so it is hidden from the SDK. 48 * {@hide} 49 */ 50 protected Cursor mCursor; 51 /** 52 * This field should be made private, so it is hidden from the SDK. 53 * {@hide} 54 */ 55 protected Context mContext; 56 /** 57 * This field should be made private, so it is hidden from the SDK. 58 * {@hide} 59 */ 60 protected int mRowIDColumn; 61 /** 62 * This field should be made private, so it is hidden from the SDK. 63 * {@hide} 64 */ 65 protected ChangeObserver mChangeObserver; 66 /** 67 * This field should be made private, so it is hidden from the SDK. 68 * {@hide} 69 */ 70 protected DataSetObserver mDataSetObserver = new MyDataSetObserver(); 71 /** 72 * This field should be made private, so it is hidden from the SDK. 73 * {@hide} 74 */ 75 protected CursorFilter mCursorFilter; 76 /** 77 * This field should be made private, so it is hidden from the SDK. 78 * {@hide} 79 */ 80 protected FilterQueryProvider mFilterQueryProvider; 81 82 /** 83 * Constructor. The adapter will call requery() on the cursor whenever 84 * it changes so that the most recent data is always displayed. 85 * 86 * @param c The cursor from which to get the data. 87 * @param context The context 88 */ 89 public CursorAdapter(Context context, Cursor c) { 90 init(context, c, true); 91 } 92 93 /** 94 * Constructor 95 * @param c The cursor from which to get the data. 96 * @param context The context 97 * @param autoRequery If true the adapter will call requery() on the 98 * cursor whenever it changes so the most recent 99 * data is always displayed. 100 */ 101 public CursorAdapter(Context context, Cursor c, boolean autoRequery) { 102 init(context, c, autoRequery); 103 } 104 105 protected void init(Context context, Cursor c, boolean autoRequery) { 106 boolean cursorPresent = c != null; 107 mAutoRequery = autoRequery; 108 mCursor = c; 109 mDataValid = cursorPresent; 110 mContext = context; 111 mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; 112 mChangeObserver = new ChangeObserver(); 113 if (cursorPresent) { 114 c.registerContentObserver(mChangeObserver); 115 c.registerDataSetObserver(mDataSetObserver); 116 } 117 } 118 119 /** 120 * Returns the cursor. 121 * @return the cursor. 122 */ 123 public Cursor getCursor() { 124 return mCursor; 125 } 126 127 /** 128 * @see android.widget.ListAdapter#getCount() 129 */ 130 public int getCount() { 131 if (mDataValid && mCursor != null) { 132 return mCursor.getCount(); 133 } else { 134 return 0; 135 } 136 } 137 138 /** 139 * @see android.widget.ListAdapter#getItem(int) 140 */ 141 public Object getItem(int position) { 142 if (mDataValid && mCursor != null) { 143 mCursor.moveToPosition(position); 144 return mCursor; 145 } else { 146 return null; 147 } 148 } 149 150 /** 151 * @see android.widget.ListAdapter#getItemId(int) 152 */ 153 public long getItemId(int position) { 154 if (mDataValid && mCursor != null) { 155 if (mCursor.moveToPosition(position)) { 156 return mCursor.getLong(mRowIDColumn); 157 } else { 158 return 0; 159 } 160 } else { 161 return 0; 162 } 163 } 164 165 @Override 166 public boolean hasStableIds() { 167 return true; 168 } 169 170 /** 171 * @see android.widget.ListAdapter#getView(int, View, ViewGroup) 172 */ 173 public View getView(int position, View convertView, ViewGroup parent) { 174 if (!mDataValid) { 175 throw new IllegalStateException("this should only be called when the cursor is valid"); 176 } 177 if (!mCursor.moveToPosition(position)) { 178 throw new IllegalStateException("couldn't move cursor to position " + position); 179 } 180 View v; 181 if (convertView == null) { 182 v = newView(mContext, mCursor, parent); 183 } else { 184 v = convertView; 185 } 186 bindView(v, mContext, mCursor); 187 return v; 188 } 189 190 @Override 191 public View getDropDownView(int position, View convertView, ViewGroup parent) { 192 if (mDataValid) { 193 mCursor.moveToPosition(position); 194 View v; 195 if (convertView == null) { 196 v = newDropDownView(mContext, mCursor, parent); 197 } else { 198 v = convertView; 199 } 200 bindView(v, mContext, mCursor); 201 return v; 202 } else { 203 return null; 204 } 205 } 206 207 /** 208 * Makes a new view to hold the data pointed to by cursor. 209 * @param context Interface to application's global information 210 * @param cursor The cursor from which to get the data. The cursor is already 211 * moved to the correct position. 212 * @param parent The parent to which the new view is attached to 213 * @return the newly created view. 214 */ 215 public abstract View newView(Context context, Cursor cursor, ViewGroup parent); 216 217 /** 218 * Makes a new drop down view to hold the data pointed to by cursor. 219 * @param context Interface to application's global information 220 * @param cursor The cursor from which to get the data. The cursor is already 221 * moved to the correct position. 222 * @param parent The parent to which the new view is attached to 223 * @return the newly created view. 224 */ 225 public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { 226 return newView(context, cursor, parent); 227 } 228 229 /** 230 * Bind an existing view to the data pointed to by cursor 231 * @param view Existing view, returned earlier by newView 232 * @param context Interface to application's global information 233 * @param cursor The cursor from which to get the data. The cursor is already 234 * moved to the correct position. 235 */ 236 public abstract void bindView(View view, Context context, Cursor cursor); 237 238 /** 239 * Change the underlying cursor to a new cursor. If there is an existing cursor it will be 240 * closed. 241 * 242 * @param cursor the new cursor to be used 243 */ 244 public void changeCursor(Cursor cursor) { 245 if (cursor == mCursor) { 246 return; 247 } 248 if (mCursor != null) { 249 mCursor.unregisterContentObserver(mChangeObserver); 250 mCursor.unregisterDataSetObserver(mDataSetObserver); 251 mCursor.close(); 252 } 253 mCursor = cursor; 254 if (cursor != null) { 255 cursor.registerContentObserver(mChangeObserver); 256 cursor.registerDataSetObserver(mDataSetObserver); 257 mRowIDColumn = cursor.getColumnIndexOrThrow("_id"); 258 mDataValid = true; 259 // notify the observers about the new cursor 260 notifyDataSetChanged(); 261 } else { 262 mRowIDColumn = -1; 263 mDataValid = false; 264 // notify the observers about the lack of a data set 265 notifyDataSetInvalidated(); 266 } 267 } 268 269 /** 270 * <p>Converts the cursor into a CharSequence. Subclasses should override this 271 * method to convert their results. The default implementation returns an 272 * empty String for null values or the default String representation of 273 * the value.</p> 274 * 275 * @param cursor the cursor to convert to a CharSequence 276 * @return a CharSequence representing the value 277 */ 278 public CharSequence convertToString(Cursor cursor) { 279 return cursor == null ? "" : cursor.toString(); 280 } 281 282 /** 283 * Runs a query with the specified constraint. This query is requested 284 * by the filter attached to this adapter. 285 * 286 * The query is provided by a 287 * {@link android.widget.FilterQueryProvider}. 288 * If no provider is specified, the current cursor is not filtered and returned. 289 * 290 * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} 291 * and the previous cursor is closed. 292 * 293 * This method is always executed on a background thread, not on the 294 * application's main thread (or UI thread.) 295 * 296 * Contract: when constraint is null or empty, the original results, 297 * prior to any filtering, must be returned. 298 * 299 * @param constraint the constraint with which the query must be filtered 300 * 301 * @return a Cursor representing the results of the new query 302 * 303 * @see #getFilter() 304 * @see #getFilterQueryProvider() 305 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 306 */ 307 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 308 if (mFilterQueryProvider != null) { 309 return mFilterQueryProvider.runQuery(constraint); 310 } 311 312 return mCursor; 313 } 314 315 public Filter getFilter() { 316 if (mCursorFilter == null) { 317 mCursorFilter = new CursorFilter(this); 318 } 319 return mCursorFilter; 320 } 321 322 /** 323 * Returns the query filter provider used for filtering. When the 324 * provider is null, no filtering occurs. 325 * 326 * @return the current filter query provider or null if it does not exist 327 * 328 * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) 329 * @see #runQueryOnBackgroundThread(CharSequence) 330 */ 331 public FilterQueryProvider getFilterQueryProvider() { 332 return mFilterQueryProvider; 333 } 334 335 /** 336 * Sets the query filter provider used to filter the current Cursor. 337 * The provider's 338 * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} 339 * method is invoked when filtering is requested by a client of 340 * this adapter. 341 * 342 * @param filterQueryProvider the filter query provider or null to remove it 343 * 344 * @see #getFilterQueryProvider() 345 * @see #runQueryOnBackgroundThread(CharSequence) 346 */ 347 public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { 348 mFilterQueryProvider = filterQueryProvider; 349 } 350 351 /** 352 * Called when the {@link ContentObserver} on the cursor receives a change notification. 353 * The default implementation provides the auto-requery logic, but may be overridden by 354 * sub classes. 355 * 356 * @see ContentObserver#onChange(boolean) 357 */ 358 protected void onContentChanged() { 359 if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { 360 if (Config.LOGV) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); 361 mDataValid = mCursor.requery(); 362 } 363 } 364 365 private class ChangeObserver extends ContentObserver { 366 public ChangeObserver() { 367 super(new Handler()); 368 } 369 370 @Override 371 public boolean deliverSelfNotifications() { 372 return true; 373 } 374 375 @Override 376 public void onChange(boolean selfChange) { 377 onContentChanged(); 378 } 379 } 380 381 private class MyDataSetObserver extends DataSetObserver { 382 @Override 383 public void onChanged() { 384 mDataValid = true; 385 notifyDataSetChanged(); 386 } 387 388 @Override 389 public void onInvalidated() { 390 mDataValid = false; 391 notifyDataSetInvalidated(); 392 } 393 } 394 395} 396