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