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