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 17 package android.database.sqlite; 18 19 import android.database.AbstractWindowedCursor; 20 import android.database.CursorWindow; 21 import android.database.DataSetObserver; 22 import android.database.SQLException; 23 24 import android.os.Handler; 25 import android.os.Message; 26 import android.os.Process; 27 import android.os.StrictMode; 28 import android.text.TextUtils; 29 import android.util.Config; 30 import android.util.Log; 31 32 import java.util.HashMap; 33 import java.util.Iterator; 34 import java.util.Map; 35 import java.util.concurrent.locks.ReentrantLock; 36 37 /** 38 * A Cursor implementation that exposes results from a query on a 39 * {@link SQLiteDatabase}. 40 * 41 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple 42 * threads should perform its own synchronization when using the SQLiteCursor. 43 */ 44 public class SQLiteCursor extends AbstractWindowedCursor { 45 static final String TAG = "Cursor"; 46 static final int NO_COUNT = -1; 47 48 /** The name of the table to edit */ 49 private String mEditTable; 50 51 /** The names of the columns in the rows */ 52 private String[] mColumns; 53 54 /** The query object for the cursor */ 55 private SQLiteQuery mQuery; 56 57 /** The database the cursor was created from */ 58 private SQLiteDatabase mDatabase; 59 60 /** The compiled query this cursor came from */ 61 private SQLiteCursorDriver mDriver; 62 63 /** The number of rows in the cursor */ 64 private int mCount = NO_COUNT; 65 66 /** A mapping of column names to column indices, to speed up lookups */ 67 private Map<String, Integer> mColumnNameMap; 68 69 /** Used to find out where a cursor was allocated in case it never got released. */ 70 private Throwable mStackTrace; 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 */ setLoadStyle(int initialRead, int maxRead)92 public void setLoadStyle(int initialRead, int maxRead) { 93 mMaxRead = maxRead; 94 mInitialRead = initialRead; 95 mLock = new ReentrantLock(true); 96 } 97 queryThreadLock()98 private void queryThreadLock() { 99 if (mLock != null) { 100 mLock.lock(); 101 } 102 } 103 queryThreadUnlock()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; QueryThread(int version)116 QueryThread(int version) { 117 mThreadState = version; 118 } sendMessage()119 private void sendMessage() { 120 if (mNotificationHandler != null) { 121 mNotificationHandler.sendEmptyMessage(1); 122 mPendingData = false; 123 } else { 124 mPendingData = true; 125 } 126 127 } run()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 { handleMessage(Message msg)168 public void handleMessage(Message msg) { 169 notifyDataSetChange(); 170 } 171 } 172 173 /** 174 * @hide 175 */ 176 protected MainThreadNotificationHandler mNotificationHandler; 177 registerDataSetObserver(DataSetObserver observer)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 */ SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query)210 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 211 String editTable, SQLiteQuery query) { 212 // The AbstractCursor constructor needs to do some setup. 213 super(); 214 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 215 mDatabase = db; 216 mDriver = driver; 217 mEditTable = editTable; 218 mColumnNameMap = null; 219 mQuery = query; 220 221 try { 222 db.lock(); 223 224 // Setup the list of columns 225 int columnCount = mQuery.columnCountLocked(); 226 mColumns = new String[columnCount]; 227 228 // Read in all column names 229 for (int i = 0; i < columnCount; i++) { 230 String columnName = mQuery.columnNameLocked(i); 231 mColumns[i] = columnName; 232 if (Config.LOGV) { 233 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 234 + mColumns[i]); 235 } 236 237 // Make note of the row ID column index for quick access to it 238 if ("_id".equals(columnName)) { 239 mRowIdColumnIndex = i; 240 } 241 } 242 } finally { 243 db.unlock(); 244 } 245 } 246 247 /** 248 * @return the SQLiteDatabase that this cursor is associated with. 249 */ getDatabase()250 public SQLiteDatabase getDatabase() { 251 return mDatabase; 252 } 253 254 @Override onMove(int oldPosition, int newPosition)255 public boolean onMove(int oldPosition, int newPosition) { 256 // Make sure the row at newPosition is present in the window 257 if (mWindow == null || newPosition < mWindow.getStartPosition() || 258 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 259 fillWindow(newPosition); 260 } 261 262 return true; 263 } 264 265 @Override getCount()266 public int getCount() { 267 if (mCount == NO_COUNT) { 268 fillWindow(0); 269 } 270 return mCount; 271 } 272 fillWindow(int startPos)273 private void fillWindow (int startPos) { 274 if (mWindow == null) { 275 // If there isn't a window set already it will only be accessed locally 276 mWindow = new CursorWindow(true /* the window is local only */); 277 } else { 278 mCursorState++; 279 queryThreadLock(); 280 try { 281 mWindow.clear(); 282 } finally { 283 queryThreadUnlock(); 284 } 285 } 286 mWindow.setStartPosition(startPos); 287 mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); 288 // return -1 means not finished 289 if (mCount == NO_COUNT){ 290 mCount = startPos + mInitialRead; 291 Thread t = new Thread(new QueryThread(mCursorState), "query thread"); 292 t.start(); 293 } 294 } 295 296 @Override getColumnIndex(String columnName)297 public int getColumnIndex(String columnName) { 298 // Create mColumnNameMap on demand 299 if (mColumnNameMap == null) { 300 String[] columns = mColumns; 301 int columnCount = columns.length; 302 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 303 for (int i = 0; i < columnCount; i++) { 304 map.put(columns[i], i); 305 } 306 mColumnNameMap = map; 307 } 308 309 // Hack according to bug 903852 310 final int periodIndex = columnName.lastIndexOf('.'); 311 if (periodIndex != -1) { 312 Exception e = new Exception(); 313 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 314 columnName = columnName.substring(periodIndex + 1); 315 } 316 317 Integer i = mColumnNameMap.get(columnName); 318 if (i != null) { 319 return i.intValue(); 320 } else { 321 return -1; 322 } 323 } 324 325 /** 326 * @hide 327 * @deprecated 328 */ 329 @Override deleteRow()330 public boolean deleteRow() { 331 checkPosition(); 332 333 // Only allow deletes if there is an ID column, and the ID has been read from it 334 if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { 335 Log.e(TAG, 336 "Could not delete row because either the row ID column is not available or it" + 337 "has not been read."); 338 return false; 339 } 340 341 boolean success; 342 343 /* 344 * Ensure we don't change the state of the database when another 345 * thread is holding the database lock. requery() and moveTo() are also 346 * synchronized here to make sure they get the state of the database 347 * immediately following the DELETE. 348 */ 349 mDatabase.lock(); 350 try { 351 try { 352 mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", 353 new String[] {mCurrentRowID.toString()}); 354 success = true; 355 } catch (SQLException e) { 356 success = false; 357 } 358 359 int pos = mPos; 360 requery(); 361 362 /* 363 * Ensure proper cursor state. Note that mCurrentRowID changes 364 * in this call. 365 */ 366 moveToPosition(pos); 367 } finally { 368 mDatabase.unlock(); 369 } 370 371 if (success) { 372 onChange(true); 373 return true; 374 } else { 375 return false; 376 } 377 } 378 379 @Override getColumnNames()380 public String[] getColumnNames() { 381 return mColumns; 382 } 383 384 /** 385 * @hide 386 * @deprecated 387 */ 388 @Override supportsUpdates()389 public boolean supportsUpdates() { 390 return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); 391 } 392 393 /** 394 * @hide 395 * @deprecated 396 */ 397 @Override commitUpdates(Map<? extends Long, ? extends Map<String, Object>> additionalValues)398 public boolean commitUpdates(Map<? extends Long, 399 ? extends Map<String, Object>> additionalValues) { 400 if (!supportsUpdates()) { 401 Log.e(TAG, "commitUpdates not supported on this cursor, did you " 402 + "include the _id column?"); 403 return false; 404 } 405 406 /* 407 * Prevent other threads from changing the updated rows while they're 408 * being processed here. 409 */ 410 synchronized (mUpdatedRows) { 411 if (additionalValues != null) { 412 mUpdatedRows.putAll(additionalValues); 413 } 414 415 if (mUpdatedRows.size() == 0) { 416 return true; 417 } 418 419 /* 420 * Prevent other threads from changing the database state while 421 * we process the updated rows, and prevents us from changing the 422 * database behind the back of another thread. 423 */ 424 mDatabase.beginTransaction(); 425 try { 426 StringBuilder sql = new StringBuilder(128); 427 428 // For each row that has been updated 429 for (Map.Entry<Long, Map<String, Object>> rowEntry : 430 mUpdatedRows.entrySet()) { 431 Map<String, Object> values = rowEntry.getValue(); 432 Long rowIdObj = rowEntry.getKey(); 433 434 if (rowIdObj == null || values == null) { 435 throw new IllegalStateException("null rowId or values found! rowId = " 436 + rowIdObj + ", values = " + values); 437 } 438 439 if (values.size() == 0) { 440 continue; 441 } 442 443 long rowId = rowIdObj.longValue(); 444 445 Iterator<Map.Entry<String, Object>> valuesIter = 446 values.entrySet().iterator(); 447 448 sql.setLength(0); 449 sql.append("UPDATE " + mEditTable + " SET "); 450 451 // For each column value that has been updated 452 Object[] bindings = new Object[values.size()]; 453 int i = 0; 454 while (valuesIter.hasNext()) { 455 Map.Entry<String, Object> entry = valuesIter.next(); 456 sql.append(entry.getKey()); 457 sql.append("=?"); 458 bindings[i] = entry.getValue(); 459 if (valuesIter.hasNext()) { 460 sql.append(", "); 461 } 462 i++; 463 } 464 465 sql.append(" WHERE " + mColumns[mRowIdColumnIndex] 466 + '=' + rowId); 467 sql.append(';'); 468 mDatabase.execSQL(sql.toString(), bindings); 469 mDatabase.rowUpdated(mEditTable, rowId); 470 } 471 mDatabase.setTransactionSuccessful(); 472 } finally { 473 mDatabase.endTransaction(); 474 } 475 476 mUpdatedRows.clear(); 477 } 478 479 // Let any change observers know about the update 480 onChange(true); 481 482 return true; 483 } 484 deactivateCommon()485 private void deactivateCommon() { 486 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); 487 mCursorState = 0; 488 if (mWindow != null) { 489 mWindow.close(); 490 mWindow = null; 491 } 492 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); 493 } 494 495 @Override deactivate()496 public void deactivate() { 497 super.deactivate(); 498 deactivateCommon(); 499 mDriver.cursorDeactivated(); 500 } 501 502 @Override close()503 public void close() { 504 super.close(); 505 deactivateCommon(); 506 mQuery.close(); 507 mDriver.cursorClosed(); 508 } 509 510 @Override requery()511 public boolean requery() { 512 if (isClosed()) { 513 return false; 514 } 515 long timeStart = 0; 516 if (Config.LOGV) { 517 timeStart = System.currentTimeMillis(); 518 } 519 /* 520 * Synchronize on the database lock to ensure that mCount matches the 521 * results of mQuery.requery(). 522 */ 523 mDatabase.lock(); 524 try { 525 if (mWindow != null) { 526 mWindow.clear(); 527 } 528 mPos = -1; 529 // This one will recreate the temp table, and get its count 530 mDriver.cursorRequeried(this); 531 mCount = NO_COUNT; 532 mCursorState++; 533 queryThreadLock(); 534 try { 535 mQuery.requery(); 536 } finally { 537 queryThreadUnlock(); 538 } 539 } finally { 540 mDatabase.unlock(); 541 } 542 543 if (Config.LOGV) { 544 Log.v("DatabaseWindow", "closing window in requery()"); 545 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 546 } 547 548 boolean result = super.requery(); 549 if (Config.LOGV) { 550 long timeEnd = System.currentTimeMillis(); 551 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 552 } 553 return result; 554 } 555 556 @Override setWindow(CursorWindow window)557 public void setWindow(CursorWindow window) { 558 if (mWindow != null) { 559 mCursorState++; 560 queryThreadLock(); 561 try { 562 mWindow.close(); 563 } finally { 564 queryThreadUnlock(); 565 } 566 mCount = NO_COUNT; 567 } 568 mWindow = window; 569 } 570 571 /** 572 * Changes the selection arguments. The new values take effect after a call to requery(). 573 */ setSelectionArguments(String[] selectionArgs)574 public void setSelectionArguments(String[] selectionArgs) { 575 mDriver.setBindArguments(selectionArgs); 576 } 577 578 /** 579 * Release the native resources, if they haven't been released yet. 580 */ 581 @Override finalize()582 protected void finalize() { 583 try { 584 // if the cursor hasn't been closed yet, close it first 585 if (mWindow != null) { 586 if (StrictMode.vmSqliteObjectLeaksEnabled()) { 587 int len = mQuery.mSql.length(); 588 StrictMode.onSqliteObjectLeaked( 589 "Finalizing a Cursor that has not been deactivated or closed. " + 590 "database = " + mDatabase.getPath() + ", table = " + mEditTable + 591 ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len), 592 mStackTrace); 593 } 594 close(); 595 SQLiteDebug.notifyActiveCursorFinalized(); 596 } else { 597 if (Config.LOGV) { 598 Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() + 599 ", table = " + mEditTable + ", query = " + mQuery.mSql); 600 } 601 } 602 } finally { 603 super.finalize(); 604 } 605 } 606 } 607