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