blob: ea9346d93fbba83a103748dfad241d4f6fa3604a [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.os.Handler;
23import android.os.Message;
24import android.os.Process;
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -070025import android.os.StrictMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.util.Log;
27
28import java.util.HashMap;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import 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}.
Jeff Hamiltonf3ca9a52010-05-12 15:04:33 -070035 *
36 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
37 * threads should perform its own synchronization when using the SQLiteCursor.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038 */
39public class SQLiteCursor extends AbstractWindowedCursor {
Vasu Nori071df262010-09-14 16:02:02 -070040 static final String TAG = "SQLiteCursor";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 static final int NO_COUNT = -1;
42
43 /** The name of the table to edit */
Vasu Nori65a88832010-07-16 15:14:08 -070044 private final String mEditTable;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045
46 /** The names of the columns in the rows */
Vasu Nori65a88832010-07-16 15:14:08 -070047 private final String[] mColumns;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048
49 /** The query object for the cursor */
50 private SQLiteQuery mQuery;
51
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052 /** The compiled query this cursor came from */
Vasu Nori65a88832010-07-16 15:14:08 -070053 private final SQLiteCursorDriver mDriver;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054
55 /** The number of rows in the cursor */
Vasu Norib18f27d2010-08-12 18:16:35 -070056 private volatile int mCount = NO_COUNT;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057
58 /** A mapping of column names to column indices, to speed up lookups */
59 private Map<String, Integer> mColumnNameMap;
60
Vasu Norid606b4b2010-02-24 12:54:20 -080061 /** Used to find out where a cursor was allocated in case it never got released. */
Vasu Nori65a88832010-07-16 15:14:08 -070062 private final Throwable mStackTrace;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080063
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;
Makoto Onuki25897162010-04-29 11:26:20 -070073
74 /**
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080075 * 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();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800127 try {
Daniel Trebbien38767b32010-10-31 13:45:02 -0700128 if (mCursorState != mThreadState) {
129 break;
130 }
131
Vasu Nori65a88832010-07-16 15:14:08 -0700132 int count = getQuery().fillWindow(cw, mMaxRead, mCount);
Vasu Norib18f27d2010-08-12 18:16:35 -0700133 // return -1 means there is still more data to be retrieved from the resultset
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800134 if (count != 0) {
135 if (count == NO_COUNT){
136 mCount += mMaxRead;
Vasu Norib18f27d2010-08-12 18:16:35 -0700137 if (Log.isLoggable(TAG, Log.DEBUG)) {
138 Log.d(TAG, "received -1 from native_fill_window. read " +
139 mCount + " rows so far");
140 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800141 sendMessage();
142 } else {
Vasu Norib18f27d2010-08-12 18:16:35 -0700143 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800148 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
Vasu Nori65a88832010-07-16 15:14:08 -0700205 * and opened. This param is not used any longer
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 * @param editTable the name of the table used for this query
207 * @param query the rest of the query terms
208 * cursor is finalized
Vasu Nori65a88832010-07-16 15:14:08 -0700209 * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800210 */
Vasu Nori65a88832010-07-16 15:14:08 -0700211 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800212 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
213 String editTable, SQLiteQuery query) {
Vasu Nori65a88832010-07-16 15:14:08 -0700214 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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229 // The AbstractCursor constructor needs to do some setup.
230 super();
Vasu Nori65a88832010-07-16 15:14:08 -0700231 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 }
Vasu Nori08b448e2010-03-03 10:05:16 -0800237 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 mDriver = driver;
239 mEditTable = editTable;
240 mColumnNameMap = null;
241 mQuery = query;
242
Vasu Nori16057fa2011-03-18 11:40:37 -0700243 query.mDatabase.lock(query.mSql);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800244 try {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800245 // 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;
Joe Onorato43a17652011-04-06 19:22:23 -0700253 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800254 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 {
Vasu Nori65a88832010-07-16 15:14:08 -0700264 query.mDatabase.unlock();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800265 }
266 }
267
268 /**
269 * @return the SQLiteDatabase that this cursor is associated with.
270 */
271 public SQLiteDatabase getDatabase() {
Vasu Nori65a88832010-07-16 15:14:08 -0700272 synchronized (this) {
273 return mQuery.mDatabase;
274 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275 }
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);
Vasu Norib18f27d2010-08-12 18:16:35 -0700310 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){
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800313 mCount = startPos + mInitialRead;
Vasu Norib18f27d2010-08-12 18:16:35 -0700314 if (Log.isLoggable(TAG, Log.DEBUG)) {
315 Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far");
316 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 Thread t = new Thread(new QueryThread(mCursorState), "query thread");
318 t.start();
Vasu Norib18f27d2010-08-12 18:16:35 -0700319 } 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800327 }
328
Vasu Nori65a88832010-07-16 15:14:08 -0700329 private synchronized SQLiteQuery getQuery() {
330 return mQuery;
331 }
332
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800333 @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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800362 @Override
363 public String[] getColumnNames() {
364 return mColumns;
365 }
366
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800367 private void deactivateCommon() {
Joe Onorato43a17652011-04-06 19:22:23 -0700368 if (false) Log.v(TAG, "<<< Releasing cursor " + this);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800369 mCursorState = 0;
370 if (mWindow != null) {
371 mWindow.close();
372 mWindow = null;
373 }
Joe Onorato43a17652011-04-06 19:22:23 -0700374 if (false) Log.v("DatabaseWindow", "closing window in release()");
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800375 }
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();
Vasu Nori65a88832010-07-16 15:14:08 -0700387 synchronized (this) {
388 deactivateCommon();
389 mQuery.close();
390 mDriver.cursorClosed();
391 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800392 }
393
394 @Override
395 public boolean requery() {
396 if (isClosed()) {
397 return false;
398 }
399 long timeStart = 0;
Joe Onorato43a17652011-04-06 19:22:23 -0700400 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 timeStart = System.currentTimeMillis();
402 }
Vasu Nori65a88832010-07-16 15:14:08 -0700403
404 synchronized (this) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800405 if (mWindow != null) {
406 mWindow.clear();
407 }
408 mPos = -1;
Vasu Norica748972011-01-06 16:35:37 -0800409 SQLiteDatabase db = null;
410 try {
411 db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
412 } catch (IllegalStateException e) {
413 // for backwards compatibility, just return false
Vasu Nori324dbe52011-01-06 17:45:32 -0800414 Log.w(TAG, "requery() failed " + e.getMessage(), e);
Vasu Norica748972011-01-06 16:35:37 -0800415 return false;
416 }
Vasu Nori65a88832010-07-16 15:14:08 -0700417 if (!db.equals(mQuery.mDatabase)) {
418 // since we need to use a different database connection handle,
419 // re-compile the query
Vasu Norica748972011-01-06 16:35:37 -0800420 try {
Vasu Nori16057fa2011-03-18 11:40:37 -0700421 db.lock(mQuery.mSql);
Vasu Norica748972011-01-06 16:35:37 -0800422 } catch (IllegalStateException e) {
423 // for backwards compatibility, just return false
Vasu Nori324dbe52011-01-06 17:45:32 -0800424 Log.w(TAG, "requery() failed " + e.getMessage(), e);
Vasu Norica748972011-01-06 16:35:37 -0800425 return false;
426 }
Vasu Nori65a88832010-07-16 15:14:08 -0700427 try {
428 // close the old mQuery object and open a new one
429 mQuery.close();
430 mQuery = new SQLiteQuery(db, mQuery);
Vasu Norica748972011-01-06 16:35:37 -0800431 } catch (IllegalStateException e) {
432 // for backwards compatibility, just return false
Vasu Nori324dbe52011-01-06 17:45:32 -0800433 Log.w(TAG, "requery() failed " + e.getMessage(), e);
Vasu Norica748972011-01-06 16:35:37 -0800434 return false;
Vasu Nori65a88832010-07-16 15:14:08 -0700435 } finally {
436 db.unlock();
437 }
438 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800439 // 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();
Vasu Norica748972011-01-06 16:35:37 -0800446 } catch (IllegalStateException e) {
447 // for backwards compatibility, just return false
Vasu Nori324dbe52011-01-06 17:45:32 -0800448 Log.w(TAG, "requery() failed " + e.getMessage(), e);
Vasu Norica748972011-01-06 16:35:37 -0800449 return false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800450 } finally {
451 queryThreadUnlock();
452 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800453 }
454
Joe Onorato43a17652011-04-06 19:22:23 -0700455 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800456 Log.v("DatabaseWindow", "closing window in requery()");
457 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
458 }
459
Vasu Norica748972011-01-06 16:35:37 -0800460 boolean result = false;
461 try {
462 result = super.requery();
463 } catch (IllegalStateException e) {
464 // for backwards compatibility, just return false
Vasu Nori324dbe52011-01-06 17:45:32 -0800465 Log.w(TAG, "requery() failed " + e.getMessage(), e);
Vasu Norica748972011-01-06 16:35:37 -0800466 }
Joe Onorato43a17652011-04-06 19:22:23 -0700467 if (false) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800468 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 {
Vasu Nori42960e82010-01-06 23:12:02 -0800502 // if the cursor hasn't been closed yet, close it first
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503 if (mWindow != null) {
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -0700504 if (StrictMode.vmSqliteObjectLeaksEnabled()) {
505 int len = mQuery.mSql.length();
506 StrictMode.onSqliteObjectLeaked(
507 "Finalizing a Cursor that has not been deactivated or closed. " +
Vasu Nori65a88832010-07-16 15:14:08 -0700508 "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
Vasu Norib73cf1c2010-10-25 11:55:56 -0700509 ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len),
Vasu Nori2cc1df02010-03-23 10:17:48 -0700510 mStackTrace);
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -0700511 }
Vasu Nori2cc1df02010-03-23 10:17:48 -0700512 close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800513 SQLiteDebug.notifyActiveCursorFinalized();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800514 } else {
Joe Onorato43a17652011-04-06 19:22:23 -0700515 if (false) {
Vasu Nori65a88832010-07-16 15:14:08 -0700516 Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
Vasu Nori42960e82010-01-06 23:12:02 -0800517 ", table = " + mEditTable + ", query = " + mQuery.mSql);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800518 }
519 }
520 } finally {
521 super.finalize();
522 }
523 }
Vasu Norib18f27d2010-08-12 18:16:35 -0700524
525 /**
526 * this is only for testing purposes.
527 */
528 /* package */ int getMCount() {
529 return mCount;
530 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800531}