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