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