SQLiteCursor.java revision 65a8883f0e605bb8a73a692987b47ce5da632e72
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 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 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 { 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 try { 244 query.mDatabase.lock(); 245 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 mCount = getQuery().fillWindow(mWindow, mInitialRead, 0); 312 // return -1 means not finished 313 if (mCount == NO_COUNT){ 314 mCount = startPos + mInitialRead; 315 Thread t = new Thread(new QueryThread(mCursorState), "query thread"); 316 t.start(); 317 } 318 } 319 320 private synchronized SQLiteQuery getQuery() { 321 return mQuery; 322 } 323 324 @Override 325 public int getColumnIndex(String columnName) { 326 // Create mColumnNameMap on demand 327 if (mColumnNameMap == null) { 328 String[] columns = mColumns; 329 int columnCount = columns.length; 330 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 331 for (int i = 0; i < columnCount; i++) { 332 map.put(columns[i], i); 333 } 334 mColumnNameMap = map; 335 } 336 337 // Hack according to bug 903852 338 final int periodIndex = columnName.lastIndexOf('.'); 339 if (periodIndex != -1) { 340 Exception e = new Exception(); 341 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 342 columnName = columnName.substring(periodIndex + 1); 343 } 344 345 Integer i = mColumnNameMap.get(columnName); 346 if (i != null) { 347 return i.intValue(); 348 } else { 349 return -1; 350 } 351 } 352 353 @Override 354 public String[] getColumnNames() { 355 return mColumns; 356 } 357 358 private void deactivateCommon() { 359 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); 360 mCursorState = 0; 361 if (mWindow != null) { 362 mWindow.close(); 363 mWindow = null; 364 } 365 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); 366 } 367 368 @Override 369 public void deactivate() { 370 super.deactivate(); 371 deactivateCommon(); 372 mDriver.cursorDeactivated(); 373 } 374 375 @Override 376 public void close() { 377 super.close(); 378 synchronized (this) { 379 deactivateCommon(); 380 mQuery.close(); 381 mDriver.cursorClosed(); 382 } 383 } 384 385 /** 386 * Show a warning against the use of requery() if called on the main thread. 387 * This warning is shown per database per process. 388 */ 389 private void warnIfUiThread() { 390 if (Looper.getMainLooper() == Looper.myLooper()) { 391 String databasePath = getQuery().mDatabase.getPath(); 392 // We show the warning once per database in order not to spam logcat. 393 if (!sAlreadyWarned.containsKey(databasePath)) { 394 sAlreadyWarned.put(databasePath, true); 395 String packageName = ActivityThread.currentPackageName(); 396 Log.w(TAG, "should not attempt requery on main (UI) thread: app = " + 397 packageName == null ? "'unknown'" : packageName, 398 new RequeryOnUiThreadException(packageName)); 399 } 400 } 401 } 402 403 @Override 404 public boolean requery() { 405 if (isClosed()) { 406 return false; 407 } 408 warnIfUiThread(); 409 long timeStart = 0; 410 if (Config.LOGV) { 411 timeStart = System.currentTimeMillis(); 412 } 413 414 synchronized (this) { 415 if (mWindow != null) { 416 mWindow.clear(); 417 } 418 mPos = -1; 419 SQLiteDatabase db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); 420 if (!db.equals(mQuery.mDatabase)) { 421 // since we need to use a different database connection handle, 422 // re-compile the query 423 db.lock(); 424 try { 425 // close the old mQuery object and open a new one 426 mQuery.close(); 427 mQuery = new SQLiteQuery(db, mQuery); 428 } finally { 429 db.unlock(); 430 } 431 } 432 // This one will recreate the temp table, and get its count 433 mDriver.cursorRequeried(this); 434 mCount = NO_COUNT; 435 mCursorState++; 436 queryThreadLock(); 437 try { 438 mQuery.requery(); 439 } finally { 440 queryThreadUnlock(); 441 } 442 } 443 444 if (Config.LOGV) { 445 Log.v("DatabaseWindow", "closing window in requery()"); 446 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 447 } 448 449 boolean result = super.requery(); 450 if (Config.LOGV) { 451 long timeEnd = System.currentTimeMillis(); 452 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 453 } 454 return result; 455 } 456 457 @Override 458 public void setWindow(CursorWindow window) { 459 if (mWindow != null) { 460 mCursorState++; 461 queryThreadLock(); 462 try { 463 mWindow.close(); 464 } finally { 465 queryThreadUnlock(); 466 } 467 mCount = NO_COUNT; 468 } 469 mWindow = window; 470 } 471 472 /** 473 * Changes the selection arguments. The new values take effect after a call to requery(). 474 */ 475 public void setSelectionArguments(String[] selectionArgs) { 476 mDriver.setBindArguments(selectionArgs); 477 } 478 479 /** 480 * Release the native resources, if they haven't been released yet. 481 */ 482 @Override 483 protected void finalize() { 484 try { 485 // if the cursor hasn't been closed yet, close it first 486 if (mWindow != null) { 487 int len = mQuery.mSql.length(); 488 Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " + 489 "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + 490 ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len), 491 mStackTrace); 492 close(); 493 SQLiteDebug.notifyActiveCursorFinalized(); 494 } else { 495 if (Config.LOGV) { 496 Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + 497 ", table = " + mEditTable + ", query = " + mQuery.mSql); 498 } 499 } 500 } finally { 501 super.finalize(); 502 } 503 } 504} 505