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