SQLiteCursor.java revision 38767b34ba056a4e384bcebc9d497da77b3a0260
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.sqlite; 18 19import android.database.AbstractWindowedCursor; 20import android.database.CursorWindow; 21import android.database.DataSetObserver; 22import android.database.SQLException; 23 24import android.os.Handler; 25import android.os.Message; 26import android.os.Process; 27import android.text.TextUtils; 28import android.util.Config; 29import android.util.Log; 30 31import java.util.HashMap; 32import java.util.Iterator; 33import java.util.Map; 34import java.util.concurrent.locks.ReentrantLock; 35 36/** 37 * A Cursor implementation that exposes results from a query on a 38 * {@link SQLiteDatabase}. 39 * 40 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple 41 * threads should perform its own synchronization when using the SQLiteCursor. 42 */ 43public class SQLiteCursor extends AbstractWindowedCursor { 44 static final String TAG = "Cursor"; 45 static final int NO_COUNT = -1; 46 47 /** The name of the table to edit */ 48 private String mEditTable; 49 50 /** The names of the columns in the rows */ 51 private String[] mColumns; 52 53 /** The query object for the cursor */ 54 private SQLiteQuery mQuery; 55 56 /** The database the cursor was created from */ 57 private SQLiteDatabase mDatabase; 58 59 /** The compiled query this cursor came from */ 60 private SQLiteCursorDriver mDriver; 61 62 /** The number of rows in the cursor */ 63 private int mCount = NO_COUNT; 64 65 /** A mapping of column names to column indices, to speed up lookups */ 66 private Map<String, Integer> mColumnNameMap; 67 68 /** Used to find out where a cursor was allocated in case it never got released. */ 69 private Throwable mStackTrace; 70 71 /** 72 * mMaxRead is the max items that each cursor window reads 73 * default to a very high value 74 */ 75 private int mMaxRead = Integer.MAX_VALUE; 76 private int mInitialRead = Integer.MAX_VALUE; 77 private int mCursorState = 0; 78 private ReentrantLock mLock = null; 79 private boolean mPendingData = false; 80 81 /** 82 * support for a cursor variant that doesn't always read all results 83 * initialRead is the initial number of items that cursor window reads 84 * if query contains more than this number of items, a thread will be 85 * created and handle the left over items so that caller can show 86 * results as soon as possible 87 * @param initialRead initial number of items that cursor read 88 * @param maxRead leftover items read at maxRead items per time 89 * @hide 90 */ 91 public void setLoadStyle(int initialRead, int maxRead) { 92 mMaxRead = maxRead; 93 mInitialRead = initialRead; 94 mLock = new ReentrantLock(true); 95 } 96 97 private void queryThreadLock() { 98 if (mLock != null) { 99 mLock.lock(); 100 } 101 } 102 103 private void queryThreadUnlock() { 104 if (mLock != null) { 105 mLock.unlock(); 106 } 107 } 108 109 110 /** 111 * @hide 112 */ 113 final private class QueryThread implements Runnable { 114 private final int mThreadState; 115 QueryThread(int version) { 116 mThreadState = version; 117 } 118 private void sendMessage() { 119 if (mNotificationHandler != null) { 120 mNotificationHandler.sendEmptyMessage(1); 121 mPendingData = false; 122 } else { 123 mPendingData = true; 124 } 125 126 } 127 public void run() { 128 // use cached mWindow, to avoid get null mWindow 129 CursorWindow cw = mWindow; 130 Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND); 131 // the cursor's state doesn't change 132 while (true) { 133 mLock.lock(); 134 try { 135 if (mCursorState != mThreadState) { 136 break; 137 } 138 139 int count = mQuery.fillWindow(cw, mMaxRead, mCount); 140 // return -1 means not finished 141 if (count != 0) { 142 if (count == NO_COUNT){ 143 mCount += mMaxRead; 144 sendMessage(); 145 } else { 146 mCount = count; 147 sendMessage(); 148 break; 149 } 150 } else { 151 break; 152 } 153 } catch (Exception e) { 154 // end the tread when the cursor is close 155 break; 156 } finally { 157 mLock.unlock(); 158 } 159 } 160 } 161 } 162 163 /** 164 * @hide 165 */ 166 protected class MainThreadNotificationHandler extends Handler { 167 public void handleMessage(Message msg) { 168 notifyDataSetChange(); 169 } 170 } 171 172 /** 173 * @hide 174 */ 175 protected MainThreadNotificationHandler mNotificationHandler; 176 177 public void registerDataSetObserver(DataSetObserver observer) { 178 super.registerDataSetObserver(observer); 179 if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && 180 mNotificationHandler == null) { 181 queryThreadLock(); 182 try { 183 mNotificationHandler = new MainThreadNotificationHandler(); 184 if (mPendingData) { 185 notifyDataSetChange(); 186 mPendingData = false; 187 } 188 } finally { 189 queryThreadUnlock(); 190 } 191 } 192 193 } 194 195 /** 196 * Execute a query and provide access to its result set through a Cursor 197 * interface. For a query such as: {@code SELECT name, birth, phone FROM 198 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 199 * phone) would be in the projection argument and everything from 200 * {@code FROM} onward would be in the params argument. This constructor 201 * has package scope. 202 * 203 * @param db a reference to a Database object that is already constructed 204 * and opened 205 * @param editTable the name of the table used for this query 206 * @param query the rest of the query terms 207 * cursor is finalized 208 */ 209 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 210 String editTable, SQLiteQuery query) { 211 // The AbstractCursor constructor needs to do some setup. 212 super(); 213 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 214 mDatabase = db; 215 mDriver = driver; 216 mEditTable = editTable; 217 mColumnNameMap = null; 218 mQuery = query; 219 220 db.lock(); 221 try { 222 // Setup the list of columns 223 int columnCount = mQuery.columnCountLocked(); 224 mColumns = new String[columnCount]; 225 226 // Read in all column names 227 for (int i = 0; i < columnCount; i++) { 228 String columnName = mQuery.columnNameLocked(i); 229 mColumns[i] = columnName; 230 if (Config.LOGV) { 231 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 232 + mColumns[i]); 233 } 234 235 // Make note of the row ID column index for quick access to it 236 if ("_id".equals(columnName)) { 237 mRowIdColumnIndex = i; 238 } 239 } 240 } finally { 241 db.unlock(); 242 } 243 } 244 245 /** 246 * @return the SQLiteDatabase that this cursor is associated with. 247 */ 248 public SQLiteDatabase getDatabase() { 249 return mDatabase; 250 } 251 252 @Override 253 public boolean onMove(int oldPosition, int newPosition) { 254 // Make sure the row at newPosition is present in the window 255 if (mWindow == null || newPosition < mWindow.getStartPosition() || 256 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 257 fillWindow(newPosition); 258 } 259 260 return true; 261 } 262 263 @Override 264 public int getCount() { 265 if (mCount == NO_COUNT) { 266 fillWindow(0); 267 } 268 return mCount; 269 } 270 271 private void fillWindow (int startPos) { 272 if (mWindow == null) { 273 // If there isn't a window set already it will only be accessed locally 274 mWindow = new CursorWindow(true /* the window is local only */); 275 } else { 276 mCursorState++; 277 queryThreadLock(); 278 try { 279 mWindow.clear(); 280 } finally { 281 queryThreadUnlock(); 282 } 283 } 284 mWindow.setStartPosition(startPos); 285 mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); 286 // return -1 means not finished 287 if (mCount == NO_COUNT){ 288 mCount = startPos + mInitialRead; 289 Thread t = new Thread(new QueryThread(mCursorState), "query thread"); 290 t.start(); 291 } 292 } 293 294 @Override 295 public int getColumnIndex(String columnName) { 296 // Create mColumnNameMap on demand 297 if (mColumnNameMap == null) { 298 String[] columns = mColumns; 299 int columnCount = columns.length; 300 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 301 for (int i = 0; i < columnCount; i++) { 302 map.put(columns[i], i); 303 } 304 mColumnNameMap = map; 305 } 306 307 // Hack according to bug 903852 308 final int periodIndex = columnName.lastIndexOf('.'); 309 if (periodIndex != -1) { 310 Exception e = new Exception(); 311 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 312 columnName = columnName.substring(periodIndex + 1); 313 } 314 315 Integer i = mColumnNameMap.get(columnName); 316 if (i != null) { 317 return i.intValue(); 318 } else { 319 return -1; 320 } 321 } 322 323 /** 324 * @hide 325 * @deprecated 326 */ 327 @Override 328 public boolean deleteRow() { 329 checkPosition(); 330 331 // Only allow deletes if there is an ID column, and the ID has been read from it 332 if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { 333 Log.e(TAG, 334 "Could not delete row because either the row ID column is not available or it" + 335 "has not been read."); 336 return false; 337 } 338 339 boolean success; 340 341 /* 342 * Ensure we don't change the state of the database when another 343 * thread is holding the database lock. requery() and moveTo() are also 344 * synchronized here to make sure they get the state of the database 345 * immediately following the DELETE. 346 */ 347 mDatabase.lock(); 348 try { 349 try { 350 mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", 351 new String[] {mCurrentRowID.toString()}); 352 success = true; 353 } catch (SQLException e) { 354 success = false; 355 } 356 357 int pos = mPos; 358 requery(); 359 360 /* 361 * Ensure proper cursor state. Note that mCurrentRowID changes 362 * in this call. 363 */ 364 moveToPosition(pos); 365 } finally { 366 mDatabase.unlock(); 367 } 368 369 if (success) { 370 onChange(true); 371 return true; 372 } else { 373 return false; 374 } 375 } 376 377 @Override 378 public String[] getColumnNames() { 379 return mColumns; 380 } 381 382 /** 383 * @hide 384 * @deprecated 385 */ 386 @Override 387 public boolean supportsUpdates() { 388 return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); 389 } 390 391 /** 392 * @hide 393 * @deprecated 394 */ 395 @Override 396 public boolean commitUpdates(Map<? extends Long, 397 ? extends Map<String, Object>> additionalValues) { 398 if (!supportsUpdates()) { 399 Log.e(TAG, "commitUpdates not supported on this cursor, did you " 400 + "include the _id column?"); 401 return false; 402 } 403 404 /* 405 * Prevent other threads from changing the updated rows while they're 406 * being processed here. 407 */ 408 synchronized (mUpdatedRows) { 409 if (additionalValues != null) { 410 mUpdatedRows.putAll(additionalValues); 411 } 412 413 if (mUpdatedRows.size() == 0) { 414 return true; 415 } 416 417 /* 418 * Prevent other threads from changing the database state while 419 * we process the updated rows, and prevents us from changing the 420 * database behind the back of another thread. 421 */ 422 mDatabase.beginTransaction(); 423 try { 424 StringBuilder sql = new StringBuilder(128); 425 426 // For each row that has been updated 427 for (Map.Entry<Long, Map<String, Object>> rowEntry : 428 mUpdatedRows.entrySet()) { 429 Map<String, Object> values = rowEntry.getValue(); 430 Long rowIdObj = rowEntry.getKey(); 431 432 if (rowIdObj == null || values == null) { 433 throw new IllegalStateException("null rowId or values found! rowId = " 434 + rowIdObj + ", values = " + values); 435 } 436 437 if (values.size() == 0) { 438 continue; 439 } 440 441 long rowId = rowIdObj.longValue(); 442 443 Iterator<Map.Entry<String, Object>> valuesIter = 444 values.entrySet().iterator(); 445 446 sql.setLength(0); 447 sql.append("UPDATE " + mEditTable + " SET "); 448 449 // For each column value that has been updated 450 Object[] bindings = new Object[values.size()]; 451 int i = 0; 452 while (valuesIter.hasNext()) { 453 Map.Entry<String, Object> entry = valuesIter.next(); 454 sql.append(entry.getKey()); 455 sql.append("=?"); 456 bindings[i] = entry.getValue(); 457 if (valuesIter.hasNext()) { 458 sql.append(", "); 459 } 460 i++; 461 } 462 463 sql.append(" WHERE " + mColumns[mRowIdColumnIndex] 464 + '=' + rowId); 465 sql.append(';'); 466 mDatabase.execSQL(sql.toString(), bindings); 467 mDatabase.rowUpdated(mEditTable, rowId); 468 } 469 mDatabase.setTransactionSuccessful(); 470 } finally { 471 mDatabase.endTransaction(); 472 } 473 474 mUpdatedRows.clear(); 475 } 476 477 // Let any change observers know about the update 478 onChange(true); 479 480 return true; 481 } 482 483 private void deactivateCommon() { 484 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); 485 mCursorState = 0; 486 if (mWindow != null) { 487 mWindow.close(); 488 mWindow = null; 489 } 490 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); 491 } 492 493 @Override 494 public void deactivate() { 495 super.deactivate(); 496 deactivateCommon(); 497 mDriver.cursorDeactivated(); 498 } 499 500 @Override 501 public void close() { 502 super.close(); 503 deactivateCommon(); 504 mQuery.close(); 505 mDriver.cursorClosed(); 506 } 507 508 @Override 509 public boolean requery() { 510 if (isClosed()) { 511 return false; 512 } 513 long timeStart = 0; 514 if (Config.LOGV) { 515 timeStart = System.currentTimeMillis(); 516 } 517 /* 518 * Synchronize on the database lock to ensure that mCount matches the 519 * results of mQuery.requery(). 520 */ 521 mDatabase.lock(); 522 try { 523 if (mWindow != null) { 524 mWindow.clear(); 525 } 526 mPos = -1; 527 // This one will recreate the temp table, and get its count 528 mDriver.cursorRequeried(this); 529 mCount = NO_COUNT; 530 mCursorState++; 531 queryThreadLock(); 532 try { 533 mQuery.requery(); 534 } finally { 535 queryThreadUnlock(); 536 } 537 } finally { 538 mDatabase.unlock(); 539 } 540 541 if (Config.LOGV) { 542 Log.v("DatabaseWindow", "closing window in requery()"); 543 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 544 } 545 546 boolean result = super.requery(); 547 if (Config.LOGV) { 548 long timeEnd = System.currentTimeMillis(); 549 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 550 } 551 return result; 552 } 553 554 @Override 555 public void setWindow(CursorWindow window) { 556 if (mWindow != null) { 557 mCursorState++; 558 queryThreadLock(); 559 try { 560 mWindow.close(); 561 } finally { 562 queryThreadUnlock(); 563 } 564 mCount = NO_COUNT; 565 } 566 mWindow = window; 567 } 568 569 /** 570 * Changes the selection arguments. The new values take effect after a call to requery(). 571 */ 572 public void setSelectionArguments(String[] selectionArgs) { 573 mDriver.setBindArguments(selectionArgs); 574 } 575 576 /** 577 * Release the native resources, if they haven't been released yet. 578 */ 579 @Override 580 protected void finalize() { 581 try { 582 // if the cursor hasn't been closed yet, close it first 583 if (mWindow != null) { 584 int len = mQuery.mSql.length(); 585 Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " + 586 "database = " + mDatabase.getPath() + ", table = " + mEditTable + 587 ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len), 588 mStackTrace); 589 close(); 590 SQLiteDebug.notifyActiveCursorFinalized(); 591 } else { 592 if (Config.LOGV) { 593 Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + 594 ", table = " + mEditTable + ", query = " + mQuery.mSql); 595 } 596 } 597 } finally { 598 super.finalize(); 599 } 600 } 601} 602