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