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