SQLiteCursor.java revision e0ad63bf1e038dd84ec2502243236f86104f990d
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.os.Handler; 25import android.os.Looper; 26import android.os.Message; 27import android.os.Process; 28import android.os.StrictMode; 29import android.util.Config; 30import android.util.Log; 31 32import java.util.HashMap; 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 * 40 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple 41 * threads should perform its own synchronization when using the SQLiteCursor. 42 */ 43public class SQLiteCursor extends AbstractWindowedCursor { 44 static final String TAG = "SQLiteCursor"; 45 static final int NO_COUNT = -1; 46 47 /** The name of the table to edit */ 48 private final String mEditTable; 49 50 /** The names of the columns in the rows */ 51 private final String[] mColumns; 52 53 /** The query object for the cursor */ 54 private SQLiteQuery mQuery; 55 56 /** The compiled query this cursor came from */ 57 private final SQLiteCursorDriver mDriver; 58 59 /** The number of rows in the cursor */ 60 private volatile 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 final 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 * Used by {@link #requery()} to remember for which database we've already shown the warning. 80 */ 81 private static final HashMap<String, Boolean> sAlreadyWarned = new HashMap<String, Boolean>(); 82 83 /** 84 * support for a cursor variant that doesn't always read all results 85 * initialRead is the initial number of items that cursor window reads 86 * if query contains more than this number of items, a thread will be 87 * created and handle the left over items so that caller can show 88 * results as soon as possible 89 * @param initialRead initial number of items that cursor read 90 * @param maxRead leftover items read at maxRead items per time 91 * @hide 92 */ 93 public void setLoadStyle(int initialRead, int maxRead) { 94 mMaxRead = maxRead; 95 mInitialRead = initialRead; 96 mLock = new ReentrantLock(true); 97 } 98 99 private void queryThreadLock() { 100 if (mLock != null) { 101 mLock.lock(); 102 } 103 } 104 105 private void queryThreadUnlock() { 106 if (mLock != null) { 107 mLock.unlock(); 108 } 109 } 110 111 112 /** 113 * @hide 114 */ 115 final private class QueryThread implements Runnable { 116 private final int mThreadState; 117 QueryThread(int version) { 118 mThreadState = version; 119 } 120 private void sendMessage() { 121 if (mNotificationHandler != null) { 122 mNotificationHandler.sendEmptyMessage(1); 123 mPendingData = false; 124 } else { 125 mPendingData = true; 126 } 127 128 } 129 public void run() { 130 // use cached mWindow, to avoid get null mWindow 131 CursorWindow cw = mWindow; 132 Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND); 133 // the cursor's state doesn't change 134 while (true) { 135 mLock.lock(); 136 if (mCursorState != mThreadState) { 137 mLock.unlock(); 138 break; 139 } 140 try { 141 int count = getQuery().fillWindow(cw, mMaxRead, mCount); 142 // return -1 means there is still more data to be retrieved from the resultset 143 if (count != 0) { 144 if (count == NO_COUNT){ 145 mCount += mMaxRead; 146 if (Log.isLoggable(TAG, Log.DEBUG)) { 147 Log.d(TAG, "received -1 from native_fill_window. read " + 148 mCount + " rows so far"); 149 } 150 sendMessage(); 151 } else { 152 mCount += count; 153 if (Log.isLoggable(TAG, Log.DEBUG)) { 154 Log.d(TAG, "received all data from native_fill_window. read " + 155 mCount + " rows."); 156 } 157 sendMessage(); 158 break; 159 } 160 } else { 161 break; 162 } 163 } catch (Exception e) { 164 // end the tread when the cursor is close 165 break; 166 } finally { 167 mLock.unlock(); 168 } 169 } 170 } 171 } 172 173 /** 174 * @hide 175 */ 176 protected class MainThreadNotificationHandler extends Handler { 177 public void handleMessage(Message msg) { 178 notifyDataSetChange(); 179 } 180 } 181 182 /** 183 * @hide 184 */ 185 protected MainThreadNotificationHandler mNotificationHandler; 186 187 public void registerDataSetObserver(DataSetObserver observer) { 188 super.registerDataSetObserver(observer); 189 if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && 190 mNotificationHandler == null) { 191 queryThreadLock(); 192 try { 193 mNotificationHandler = new MainThreadNotificationHandler(); 194 if (mPendingData) { 195 notifyDataSetChange(); 196 mPendingData = false; 197 } 198 } finally { 199 queryThreadUnlock(); 200 } 201 } 202 203 } 204 205 /** 206 * Execute a query and provide access to its result set through a Cursor 207 * interface. For a query such as: {@code SELECT name, birth, phone FROM 208 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 209 * phone) would be in the projection argument and everything from 210 * {@code FROM} onward would be in the params argument. This constructor 211 * has package scope. 212 * 213 * @param db a reference to a Database object that is already constructed 214 * and opened. This param is not used any longer 215 * @param editTable the name of the table used for this query 216 * @param query the rest of the query terms 217 * cursor is finalized 218 * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead 219 */ 220 @Deprecated 221 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 222 String editTable, SQLiteQuery query) { 223 this(driver, editTable, query); 224 } 225 226 /** 227 * Execute a query and provide access to its result set through a Cursor 228 * interface. For a query such as: {@code SELECT name, birth, phone FROM 229 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 230 * phone) would be in the projection argument and everything from 231 * {@code FROM} onward would be in the params argument. This constructor 232 * has package scope. 233 * 234 * @param editTable the name of the table used for this query 235 * @param query the {@link SQLiteQuery} object associated with this cursor object. 236 */ 237 public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { 238 // The AbstractCursor constructor needs to do some setup. 239 super(); 240 if (query == null) { 241 throw new IllegalArgumentException("query object cannot be null"); 242 } 243 if (query.mDatabase == null) { 244 throw new IllegalArgumentException("query.mDatabase cannot be null"); 245 } 246 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 247 mDriver = driver; 248 mEditTable = editTable; 249 mColumnNameMap = null; 250 mQuery = query; 251 252 try { 253 query.mDatabase.lock(); 254 255 // Setup the list of columns 256 int columnCount = mQuery.columnCountLocked(); 257 mColumns = new String[columnCount]; 258 259 // Read in all column names 260 for (int i = 0; i < columnCount; i++) { 261 String columnName = mQuery.columnNameLocked(i); 262 mColumns[i] = columnName; 263 if (Config.LOGV) { 264 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 265 + mColumns[i]); 266 } 267 268 // Make note of the row ID column index for quick access to it 269 if ("_id".equals(columnName)) { 270 mRowIdColumnIndex = i; 271 } 272 } 273 } finally { 274 query.mDatabase.unlock(); 275 } 276 } 277 278 /** 279 * @return the SQLiteDatabase that this cursor is associated with. 280 */ 281 public SQLiteDatabase getDatabase() { 282 synchronized (this) { 283 return mQuery.mDatabase; 284 } 285 } 286 287 @Override 288 public boolean onMove(int oldPosition, int newPosition) { 289 // Make sure the row at newPosition is present in the window 290 if (mWindow == null || newPosition < mWindow.getStartPosition() || 291 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 292 fillWindow(newPosition); 293 } 294 295 return true; 296 } 297 298 @Override 299 public int getCount() { 300 if (mCount == NO_COUNT) { 301 fillWindow(0); 302 } 303 return mCount; 304 } 305 306 private void fillWindow (int startPos) { 307 if (mWindow == null) { 308 // If there isn't a window set already it will only be accessed locally 309 mWindow = new CursorWindow(true /* the window is local only */); 310 } else { 311 mCursorState++; 312 queryThreadLock(); 313 try { 314 mWindow.clear(); 315 } finally { 316 queryThreadUnlock(); 317 } 318 } 319 mWindow.setStartPosition(startPos); 320 int count = getQuery().fillWindow(mWindow, mInitialRead, 0); 321 // return -1 means there is still more data to be retrieved from the resultset 322 if (count == NO_COUNT){ 323 mCount = startPos + mInitialRead; 324 if (Log.isLoggable(TAG, Log.DEBUG)) { 325 Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far"); 326 } 327 Thread t = new Thread(new QueryThread(mCursorState), "query thread"); 328 t.start(); 329 } else if (startPos == 0) { // native_fill_window returns count(*) only for startPos = 0 330 if (Log.isLoggable(TAG, Log.DEBUG)) { 331 Log.d(TAG, "received count(*) from native_fill_window: " + count); 332 } 333 mCount = count; 334 } else if (mCount <= 0) { 335 throw new IllegalStateException("count should never be non-zero negative number"); 336 } 337 } 338 339 private synchronized SQLiteQuery getQuery() { 340 return mQuery; 341 } 342 343 @Override 344 public int getColumnIndex(String columnName) { 345 // Create mColumnNameMap on demand 346 if (mColumnNameMap == null) { 347 String[] columns = mColumns; 348 int columnCount = columns.length; 349 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 350 for (int i = 0; i < columnCount; i++) { 351 map.put(columns[i], i); 352 } 353 mColumnNameMap = map; 354 } 355 356 // Hack according to bug 903852 357 final int periodIndex = columnName.lastIndexOf('.'); 358 if (periodIndex != -1) { 359 Exception e = new Exception(); 360 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 361 columnName = columnName.substring(periodIndex + 1); 362 } 363 364 Integer i = mColumnNameMap.get(columnName); 365 if (i != null) { 366 return i.intValue(); 367 } else { 368 return -1; 369 } 370 } 371 372 @Override 373 public String[] getColumnNames() { 374 return mColumns; 375 } 376 377 private void deactivateCommon() { 378 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); 379 mCursorState = 0; 380 if (mWindow != null) { 381 mWindow.close(); 382 mWindow = null; 383 } 384 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); 385 } 386 387 @Override 388 public void deactivate() { 389 super.deactivate(); 390 deactivateCommon(); 391 mDriver.cursorDeactivated(); 392 } 393 394 @Override 395 public void close() { 396 super.close(); 397 synchronized (this) { 398 deactivateCommon(); 399 mQuery.close(); 400 mDriver.cursorClosed(); 401 } 402 } 403 404 /** 405 * Show a warning against the use of requery() if called on the main thread. 406 * This warning is shown per database per process. 407 */ 408 private void warnIfUiThread() { 409 if (Looper.getMainLooper() == Looper.myLooper()) { 410 String databasePath = getQuery().mDatabase.getPath(); 411 // We show the warning once per database in order not to spam logcat. 412 if (!sAlreadyWarned.containsKey(databasePath)) { 413 sAlreadyWarned.put(databasePath, true); 414 String packageName = ActivityThread.currentPackageName(); 415 Throwable t = null; 416 // BEGIN STOPSHIP remove the following line 417 t = new RequeryOnUiThreadException(packageName); 418 // END STOPSHIP 419 Log.w(TAG, "should not attempt requery on main (UI) thread: app = " + 420 packageName == null ? "'unknown'" : packageName, t); 421 } 422 } 423 } 424 425 @Override 426 public boolean requery() { 427 if (isClosed()) { 428 return false; 429 } 430 warnIfUiThread(); 431 long timeStart = 0; 432 if (Config.LOGV) { 433 timeStart = System.currentTimeMillis(); 434 } 435 436 synchronized (this) { 437 if (mWindow != null) { 438 mWindow.clear(); 439 } 440 mPos = -1; 441 SQLiteDatabase db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); 442 if (!db.equals(mQuery.mDatabase)) { 443 // since we need to use a different database connection handle, 444 // re-compile the query 445 db.lock(); 446 try { 447 // close the old mQuery object and open a new one 448 mQuery.close(); 449 mQuery = new SQLiteQuery(db, mQuery); 450 } finally { 451 db.unlock(); 452 } 453 } 454 // This one will recreate the temp table, and get its count 455 mDriver.cursorRequeried(this); 456 mCount = NO_COUNT; 457 mCursorState++; 458 queryThreadLock(); 459 try { 460 mQuery.requery(); 461 } finally { 462 queryThreadUnlock(); 463 } 464 } 465 466 if (Config.LOGV) { 467 Log.v("DatabaseWindow", "closing window in requery()"); 468 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 469 } 470 471 boolean result = super.requery(); 472 if (Config.LOGV) { 473 long timeEnd = System.currentTimeMillis(); 474 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 475 } 476 return result; 477 } 478 479 @Override 480 public void setWindow(CursorWindow window) { 481 if (mWindow != null) { 482 mCursorState++; 483 queryThreadLock(); 484 try { 485 mWindow.close(); 486 } finally { 487 queryThreadUnlock(); 488 } 489 mCount = NO_COUNT; 490 } 491 mWindow = window; 492 } 493 494 /** 495 * Changes the selection arguments. The new values take effect after a call to requery(). 496 */ 497 public void setSelectionArguments(String[] selectionArgs) { 498 mDriver.setBindArguments(selectionArgs); 499 } 500 501 /** 502 * Release the native resources, if they haven't been released yet. 503 */ 504 @Override 505 protected void finalize() { 506 try { 507 // if the cursor hasn't been closed yet, close it first 508 if (mWindow != null) { 509 if (StrictMode.vmSqliteObjectLeaksEnabled()) { 510 int len = mQuery.mSql.length(); 511 StrictMode.onSqliteObjectLeaked( 512 "Finalizing a Cursor that has not been deactivated or closed. " + 513 "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + 514 ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len), 515 mStackTrace); 516 } 517 close(); 518 SQLiteDebug.notifyActiveCursorFinalized(); 519 } else { 520 if (Config.LOGV) { 521 Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + 522 ", table = " + mEditTable + ", query = " + mQuery.mSql); 523 } 524 } 525 } finally { 526 super.finalize(); 527 } 528 } 529 530 /** 531 * this is only for testing purposes. 532 */ 533 /* package */ int getMCount() { 534 return mCount; 535 } 536} 537