AbstractCursor.java revision 8b0dd7da360d70920a37802eb455ba41500d3b45
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.database; 18 19import android.content.ContentResolver; 20import android.net.Uri; 21import android.os.Bundle; 22import android.util.Config; 23import android.util.Log; 24 25import java.lang.ref.WeakReference; 26import java.util.HashMap; 27import java.util.Map; 28 29 30/** 31 * This is an abstract cursor class that handles a lot of the common code 32 * that all cursors need to deal with and is provided for convenience reasons. 33 */ 34public abstract class AbstractCursor implements CrossProcessCursor { 35 private static final String TAG = "Cursor"; 36 37 DataSetObservable mDataSetObservable = new DataSetObservable(); 38 ContentObservable mContentObservable = new ContentObservable(); 39 40 /* -------------------------------------------------------- */ 41 /* These need to be implemented by subclasses */ 42 abstract public int getCount(); 43 44 abstract public String[] getColumnNames(); 45 46 abstract public String getString(int column); 47 abstract public short getShort(int column); 48 abstract public int getInt(int column); 49 abstract public long getLong(int column); 50 abstract public float getFloat(int column); 51 abstract public double getDouble(int column); 52 abstract public boolean isNull(int column); 53 54 public int getType(int column) { 55 throw new UnsupportedOperationException(); 56 } 57 58 // TODO implement getBlob in all cursor types 59 public byte[] getBlob(int column) { 60 throw new UnsupportedOperationException("getBlob is not supported"); 61 } 62 /* -------------------------------------------------------- */ 63 /* Methods that may optionally be implemented by subclasses */ 64 65 /** 66 * returns a pre-filled window, return NULL if no such window 67 */ 68 public CursorWindow getWindow() { 69 return null; 70 } 71 72 public int getColumnCount() { 73 return getColumnNames().length; 74 } 75 76 public void deactivate() { 77 deactivateInternal(); 78 } 79 80 /** 81 * @hide 82 */ 83 public void deactivateInternal() { 84 if (mSelfObserver != null) { 85 mContentResolver.unregisterContentObserver(mSelfObserver); 86 mSelfObserverRegistered = false; 87 } 88 mDataSetObservable.notifyInvalidated(); 89 } 90 91 public boolean requery() { 92 if (mSelfObserver != null && mSelfObserverRegistered == false) { 93 mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); 94 mSelfObserverRegistered = true; 95 } 96 mDataSetObservable.notifyChanged(); 97 return true; 98 } 99 100 public boolean isClosed() { 101 return mClosed; 102 } 103 104 public void close() { 105 mClosed = true; 106 mContentObservable.unregisterAll(); 107 deactivateInternal(); 108 } 109 110 /** 111 * This function is called every time the cursor is successfully scrolled 112 * to a new position, giving the subclass a chance to update any state it 113 * may have. If it returns false the move function will also do so and the 114 * cursor will scroll to the beforeFirst position. 115 * 116 * @param oldPosition the position that we're moving from 117 * @param newPosition the position that we're moving to 118 * @return true if the move is successful, false otherwise 119 */ 120 public boolean onMove(int oldPosition, int newPosition) { 121 return true; 122 } 123 124 125 public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { 126 // Default implementation, uses getString 127 String result = getString(columnIndex); 128 if (result != null) { 129 char[] data = buffer.data; 130 if (data == null || data.length < result.length()) { 131 buffer.data = result.toCharArray(); 132 } else { 133 result.getChars(0, result.length(), data, 0); 134 } 135 buffer.sizeCopied = result.length(); 136 } 137 } 138 139 /* -------------------------------------------------------- */ 140 /* Implementation */ 141 public AbstractCursor() { 142 mPos = -1; 143 mRowIdColumnIndex = -1; 144 mCurrentRowID = null; 145 mUpdatedRows = new HashMap<Long, Map<String, Object>>(); 146 } 147 148 public final int getPosition() { 149 return mPos; 150 } 151 152 public final boolean moveToPosition(int position) { 153 // Make sure position isn't past the end of the cursor 154 final int count = getCount(); 155 if (position >= count) { 156 mPos = count; 157 return false; 158 } 159 160 // Make sure position isn't before the beginning of the cursor 161 if (position < 0) { 162 mPos = -1; 163 return false; 164 } 165 166 // Check for no-op moves, and skip the rest of the work for them 167 if (position == mPos) { 168 return true; 169 } 170 171 boolean result = onMove(mPos, position); 172 if (result == false) { 173 mPos = -1; 174 } else { 175 mPos = position; 176 if (mRowIdColumnIndex != -1) { 177 mCurrentRowID = Long.valueOf(getLong(mRowIdColumnIndex)); 178 } 179 } 180 181 return result; 182 } 183 184 /** 185 * Copy data from cursor to CursorWindow 186 * @param position start position of data 187 * @param window 188 */ 189 public void fillWindow(int position, CursorWindow window) { 190 if (position < 0 || position > getCount()) { 191 return; 192 } 193 window.acquireReference(); 194 try { 195 int oldpos = mPos; 196 mPos = position - 1; 197 window.clear(); 198 window.setStartPosition(position); 199 int columnNum = getColumnCount(); 200 window.setNumColumns(columnNum); 201 while (moveToNext() && window.allocRow()) { 202 for (int i = 0; i < columnNum; i++) { 203 String field = getString(i); 204 if (field != null) { 205 if (!window.putString(field, mPos, i)) { 206 window.freeLastRow(); 207 break; 208 } 209 } else { 210 if (!window.putNull(mPos, i)) { 211 window.freeLastRow(); 212 break; 213 } 214 } 215 } 216 } 217 218 mPos = oldpos; 219 } catch (IllegalStateException e){ 220 // simply ignore it 221 } finally { 222 window.releaseReference(); 223 } 224 } 225 226 public final boolean move(int offset) { 227 return moveToPosition(mPos + offset); 228 } 229 230 public final boolean moveToFirst() { 231 return moveToPosition(0); 232 } 233 234 public final boolean moveToLast() { 235 return moveToPosition(getCount() - 1); 236 } 237 238 public final boolean moveToNext() { 239 return moveToPosition(mPos + 1); 240 } 241 242 public final boolean moveToPrevious() { 243 return moveToPosition(mPos - 1); 244 } 245 246 public final boolean isFirst() { 247 return mPos == 0 && getCount() != 0; 248 } 249 250 public final boolean isLast() { 251 int cnt = getCount(); 252 return mPos == (cnt - 1) && cnt != 0; 253 } 254 255 public final boolean isBeforeFirst() { 256 if (getCount() == 0) { 257 return true; 258 } 259 return mPos == -1; 260 } 261 262 public final boolean isAfterLast() { 263 if (getCount() == 0) { 264 return true; 265 } 266 return mPos == getCount(); 267 } 268 269 public int getColumnIndex(String columnName) { 270 // Hack according to bug 903852 271 final int periodIndex = columnName.lastIndexOf('.'); 272 if (periodIndex != -1) { 273 Exception e = new Exception(); 274 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 275 columnName = columnName.substring(periodIndex + 1); 276 } 277 278 String columnNames[] = getColumnNames(); 279 int length = columnNames.length; 280 for (int i = 0; i < length; i++) { 281 if (columnNames[i].equalsIgnoreCase(columnName)) { 282 return i; 283 } 284 } 285 286 if (Config.LOGV) { 287 if (getCount() > 0) { 288 Log.w("AbstractCursor", "Unknown column " + columnName); 289 } 290 } 291 return -1; 292 } 293 294 public int getColumnIndexOrThrow(String columnName) { 295 final int index = getColumnIndex(columnName); 296 if (index < 0) { 297 throw new IllegalArgumentException("column '" + columnName + "' does not exist"); 298 } 299 return index; 300 } 301 302 public String getColumnName(int columnIndex) { 303 return getColumnNames()[columnIndex]; 304 } 305 306 public void registerContentObserver(ContentObserver observer) { 307 mContentObservable.registerObserver(observer); 308 } 309 310 public void unregisterContentObserver(ContentObserver observer) { 311 // cursor will unregister all observers when it close 312 if (!mClosed) { 313 mContentObservable.unregisterObserver(observer); 314 } 315 } 316 317 /** 318 * This is hidden until the data set change model has been re-evaluated. 319 * @hide 320 */ 321 protected void notifyDataSetChange() { 322 mDataSetObservable.notifyChanged(); 323 } 324 325 /** 326 * This is hidden until the data set change model has been re-evaluated. 327 * @hide 328 */ 329 protected DataSetObservable getDataSetObservable() { 330 return mDataSetObservable; 331 332 } 333 public void registerDataSetObserver(DataSetObserver observer) { 334 mDataSetObservable.registerObserver(observer); 335 336 } 337 338 public void unregisterDataSetObserver(DataSetObserver observer) { 339 mDataSetObservable.unregisterObserver(observer); 340 } 341 342 /** 343 * Subclasses must call this method when they finish committing updates to notify all 344 * observers. 345 * 346 * @param selfChange 347 */ 348 protected void onChange(boolean selfChange) { 349 synchronized (mSelfObserverLock) { 350 mContentObservable.dispatchChange(selfChange); 351 if (mNotifyUri != null && selfChange) { 352 mContentResolver.notifyChange(mNotifyUri, mSelfObserver); 353 } 354 } 355 } 356 357 /** 358 * Specifies a content URI to watch for changes. 359 * 360 * @param cr The content resolver from the caller's context. 361 * @param notifyUri The URI to watch for changes. This can be a 362 * specific row URI, or a base URI for a whole class of content. 363 */ 364 public void setNotificationUri(ContentResolver cr, Uri notifyUri) { 365 synchronized (mSelfObserverLock) { 366 mNotifyUri = notifyUri; 367 mContentResolver = cr; 368 if (mSelfObserver != null) { 369 mContentResolver.unregisterContentObserver(mSelfObserver); 370 } 371 mSelfObserver = new SelfContentObserver(this); 372 mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver); 373 mSelfObserverRegistered = true; 374 } 375 } 376 377 public boolean getWantsAllOnMoveCalls() { 378 return false; 379 } 380 381 public Bundle getExtras() { 382 return Bundle.EMPTY; 383 } 384 385 public Bundle respond(Bundle extras) { 386 return Bundle.EMPTY; 387 } 388 389 /** 390 * This function returns true if the field has been updated and is 391 * used in conjunction with {@link #getUpdatedField} to allow subclasses to 392 * support reading uncommitted updates. NOTE: This function and 393 * {@link #getUpdatedField} should be called together inside of a 394 * block synchronized on mUpdatedRows. 395 * 396 * @param columnIndex the column index of the field to check 397 * @return true if the field has been updated, false otherwise 398 */ 399 protected boolean isFieldUpdated(int columnIndex) { 400 if (mRowIdColumnIndex != -1 && mUpdatedRows.size() > 0) { 401 Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID); 402 if (updates != null && updates.containsKey(getColumnNames()[columnIndex])) { 403 return true; 404 } 405 } 406 return false; 407 } 408 409 /** 410 * This function returns the uncommitted updated value for the field 411 * at columnIndex. NOTE: This function and {@link #isFieldUpdated} should 412 * be called together inside of a block synchronized on mUpdatedRows. 413 * 414 * @param columnIndex the column index of the field to retrieve 415 * @return the updated value 416 */ 417 protected Object getUpdatedField(int columnIndex) { 418 Map<String, Object> updates = mUpdatedRows.get(mCurrentRowID); 419 return updates.get(getColumnNames()[columnIndex]); 420 } 421 422 /** 423 * This function throws CursorIndexOutOfBoundsException if 424 * the cursor position is out of bounds. Subclass implementations of 425 * the get functions should call this before attempting 426 * to retrieve data. 427 * 428 * @throws CursorIndexOutOfBoundsException 429 */ 430 protected void checkPosition() { 431 if (-1 == mPos || getCount() == mPos) { 432 throw new CursorIndexOutOfBoundsException(mPos, getCount()); 433 } 434 } 435 436 @Override 437 protected void finalize() { 438 if (mSelfObserver != null && mSelfObserverRegistered == true) { 439 mContentResolver.unregisterContentObserver(mSelfObserver); 440 } 441 } 442 443 /** 444 * Cursors use this class to track changes others make to their URI. 445 */ 446 protected static class SelfContentObserver extends ContentObserver { 447 WeakReference<AbstractCursor> mCursor; 448 449 public SelfContentObserver(AbstractCursor cursor) { 450 super(null); 451 mCursor = new WeakReference<AbstractCursor>(cursor); 452 } 453 454 @Override 455 public boolean deliverSelfNotifications() { 456 return false; 457 } 458 459 @Override 460 public void onChange(boolean selfChange) { 461 AbstractCursor cursor = mCursor.get(); 462 if (cursor != null) { 463 cursor.onChange(false); 464 } 465 } 466 } 467 468 /** 469 * This HashMap contains a mapping from Long rowIDs to another Map 470 * that maps from String column names to new values. A NULL value means to 471 * remove an existing value, and all numeric values are in their class 472 * forms, i.e. Integer, Long, Float, etc. 473 */ 474 protected HashMap<Long, Map<String, Object>> mUpdatedRows; 475 476 /** 477 * This must be set to the index of the row ID column by any 478 * subclass that wishes to support updates. 479 */ 480 protected int mRowIdColumnIndex; 481 482 protected int mPos; 483 protected Long mCurrentRowID; 484 protected ContentResolver mContentResolver; 485 protected boolean mClosed = false; 486 private Uri mNotifyUri; 487 private ContentObserver mSelfObserver; 488 final private Object mSelfObserverLock = new Object(); 489 private boolean mSelfObserverRegistered; 490} 491