blob: d8dcaf7edc9d98755b0774a3f9d8b11bf0397e48 [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;
22import android.database.SQLException;
23
24import android.os.Handler;
25import android.os.Message;
26import android.os.Process;
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -070027import android.os.StrictMode;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080028import android.text.TextUtils;
29import android.util.Config;
30import android.util.Log;
31
32import java.util.HashMap;
33import java.util.Iterator;
34import java.util.Map;
35import java.util.concurrent.locks.ReentrantLock;
36
37/**
38 * A Cursor implementation that exposes results from a query on a
39 * {@link SQLiteDatabase}.
Jeff Hamiltonf3ca9a52010-05-12 15:04:33 -070040 *
41 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
42 * threads should perform its own synchronization when using the SQLiteCursor.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043 */
44public class SQLiteCursor extends AbstractWindowedCursor {
45 static final String TAG = "Cursor";
46 static final int NO_COUNT = -1;
47
48 /** The name of the table to edit */
49 private String mEditTable;
50
51 /** The names of the columns in the rows */
52 private String[] mColumns;
53
54 /** The query object for the cursor */
55 private SQLiteQuery mQuery;
56
57 /** The database the cursor was created from */
58 private SQLiteDatabase mDatabase;
59
60 /** The compiled query this cursor came from */
61 private SQLiteCursorDriver mDriver;
62
63 /** The number of rows in the cursor */
64 private int mCount = NO_COUNT;
65
66 /** A mapping of column names to column indices, to speed up lookups */
67 private Map<String, Integer> mColumnNameMap;
68
Vasu Norid606b4b2010-02-24 12:54:20 -080069 /** Used to find out where a cursor was allocated in case it never got released. */
70 private Throwable mStackTrace;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071
72 /**
73 * mMaxRead is the max items that each cursor window reads
74 * default to a very high value
75 */
76 private int mMaxRead = Integer.MAX_VALUE;
77 private int mInitialRead = Integer.MAX_VALUE;
78 private int mCursorState = 0;
79 private ReentrantLock mLock = null;
80 private boolean mPendingData = false;
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 = mQuery.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
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 */
210 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
211 String editTable, SQLiteQuery query) {
212 // The AbstractCursor constructor needs to do some setup.
213 super();
Vasu Nori08b448e2010-03-03 10:05:16 -0800214 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 mDatabase = db;
216 mDriver = driver;
217 mEditTable = editTable;
218 mColumnNameMap = null;
219 mQuery = query;
220
221 try {
222 db.lock();
223
224 // Setup the list of columns
225 int columnCount = mQuery.columnCountLocked();
226 mColumns = new String[columnCount];
227
228 // Read in all column names
229 for (int i = 0; i < columnCount; i++) {
230 String columnName = mQuery.columnNameLocked(i);
231 mColumns[i] = columnName;
232 if (Config.LOGV) {
233 Log.v("DatabaseWindow", "mColumns[" + i + "] is "
234 + mColumns[i]);
235 }
236
237 // Make note of the row ID column index for quick access to it
238 if ("_id".equals(columnName)) {
239 mRowIdColumnIndex = i;
240 }
241 }
242 } finally {
243 db.unlock();
244 }
245 }
246
247 /**
248 * @return the SQLiteDatabase that this cursor is associated with.
249 */
250 public SQLiteDatabase getDatabase() {
251 return mDatabase;
252 }
253
254 @Override
255 public boolean onMove(int oldPosition, int newPosition) {
256 // Make sure the row at newPosition is present in the window
257 if (mWindow == null || newPosition < mWindow.getStartPosition() ||
258 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
259 fillWindow(newPosition);
260 }
261
262 return true;
263 }
264
265 @Override
266 public int getCount() {
267 if (mCount == NO_COUNT) {
268 fillWindow(0);
269 }
270 return mCount;
271 }
272
273 private void fillWindow (int startPos) {
274 if (mWindow == null) {
275 // If there isn't a window set already it will only be accessed locally
276 mWindow = new CursorWindow(true /* the window is local only */);
277 } else {
278 mCursorState++;
279 queryThreadLock();
280 try {
281 mWindow.clear();
282 } finally {
283 queryThreadUnlock();
284 }
285 }
286 mWindow.setStartPosition(startPos);
287 mCount = mQuery.fillWindow(mWindow, mInitialRead, 0);
288 // return -1 means not finished
289 if (mCount == NO_COUNT){
290 mCount = startPos + mInitialRead;
291 Thread t = new Thread(new QueryThread(mCursorState), "query thread");
292 t.start();
293 }
294 }
295
296 @Override
297 public int getColumnIndex(String columnName) {
298 // Create mColumnNameMap on demand
299 if (mColumnNameMap == null) {
300 String[] columns = mColumns;
301 int columnCount = columns.length;
302 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
303 for (int i = 0; i < columnCount; i++) {
304 map.put(columns[i], i);
305 }
306 mColumnNameMap = map;
307 }
308
309 // Hack according to bug 903852
310 final int periodIndex = columnName.lastIndexOf('.');
311 if (periodIndex != -1) {
312 Exception e = new Exception();
313 Log.e(TAG, "requesting column name with table name -- " + columnName, e);
314 columnName = columnName.substring(periodIndex + 1);
315 }
316
317 Integer i = mColumnNameMap.get(columnName);
318 if (i != null) {
319 return i.intValue();
320 } else {
321 return -1;
322 }
323 }
324
325 /**
326 * @hide
327 * @deprecated
328 */
329 @Override
330 public boolean deleteRow() {
331 checkPosition();
332
333 // Only allow deletes if there is an ID column, and the ID has been read from it
334 if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
335 Log.e(TAG,
336 "Could not delete row because either the row ID column is not available or it" +
337 "has not been read.");
338 return false;
339 }
340
341 boolean success;
342
343 /*
344 * Ensure we don't change the state of the database when another
345 * thread is holding the database lock. requery() and moveTo() are also
346 * synchronized here to make sure they get the state of the database
347 * immediately following the DELETE.
348 */
349 mDatabase.lock();
350 try {
351 try {
352 mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
353 new String[] {mCurrentRowID.toString()});
354 success = true;
355 } catch (SQLException e) {
356 success = false;
357 }
358
359 int pos = mPos;
360 requery();
361
362 /*
363 * Ensure proper cursor state. Note that mCurrentRowID changes
364 * in this call.
365 */
366 moveToPosition(pos);
367 } finally {
368 mDatabase.unlock();
369 }
370
371 if (success) {
372 onChange(true);
373 return true;
374 } else {
375 return false;
376 }
377 }
378
379 @Override
380 public String[] getColumnNames() {
381 return mColumns;
382 }
383
384 /**
385 * @hide
386 * @deprecated
387 */
388 @Override
389 public boolean supportsUpdates() {
390 return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
391 }
392
393 /**
394 * @hide
395 * @deprecated
396 */
397 @Override
398 public boolean commitUpdates(Map<? extends Long,
399 ? extends Map<String, Object>> additionalValues) {
400 if (!supportsUpdates()) {
401 Log.e(TAG, "commitUpdates not supported on this cursor, did you "
402 + "include the _id column?");
403 return false;
404 }
405
406 /*
407 * Prevent other threads from changing the updated rows while they're
408 * being processed here.
409 */
410 synchronized (mUpdatedRows) {
411 if (additionalValues != null) {
412 mUpdatedRows.putAll(additionalValues);
413 }
414
415 if (mUpdatedRows.size() == 0) {
416 return true;
417 }
418
419 /*
420 * Prevent other threads from changing the database state while
421 * we process the updated rows, and prevents us from changing the
422 * database behind the back of another thread.
423 */
424 mDatabase.beginTransaction();
425 try {
426 StringBuilder sql = new StringBuilder(128);
427
428 // For each row that has been updated
429 for (Map.Entry<Long, Map<String, Object>> rowEntry :
430 mUpdatedRows.entrySet()) {
431 Map<String, Object> values = rowEntry.getValue();
432 Long rowIdObj = rowEntry.getKey();
433
434 if (rowIdObj == null || values == null) {
435 throw new IllegalStateException("null rowId or values found! rowId = "
436 + rowIdObj + ", values = " + values);
437 }
438
439 if (values.size() == 0) {
440 continue;
441 }
442
443 long rowId = rowIdObj.longValue();
444
445 Iterator<Map.Entry<String, Object>> valuesIter =
446 values.entrySet().iterator();
447
448 sql.setLength(0);
449 sql.append("UPDATE " + mEditTable + " SET ");
450
451 // For each column value that has been updated
452 Object[] bindings = new Object[values.size()];
453 int i = 0;
454 while (valuesIter.hasNext()) {
455 Map.Entry<String, Object> entry = valuesIter.next();
456 sql.append(entry.getKey());
457 sql.append("=?");
458 bindings[i] = entry.getValue();
459 if (valuesIter.hasNext()) {
460 sql.append(", ");
461 }
462 i++;
463 }
464
465 sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
466 + '=' + rowId);
467 sql.append(';');
468 mDatabase.execSQL(sql.toString(), bindings);
469 mDatabase.rowUpdated(mEditTable, rowId);
470 }
471 mDatabase.setTransactionSuccessful();
472 } finally {
473 mDatabase.endTransaction();
474 }
475
476 mUpdatedRows.clear();
477 }
478
479 // Let any change observers know about the update
480 onChange(true);
481
482 return true;
483 }
484
485 private void deactivateCommon() {
486 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
487 mCursorState = 0;
488 if (mWindow != null) {
489 mWindow.close();
490 mWindow = null;
491 }
492 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
493 }
494
495 @Override
496 public void deactivate() {
497 super.deactivate();
498 deactivateCommon();
499 mDriver.cursorDeactivated();
500 }
501
502 @Override
503 public void close() {
504 super.close();
505 deactivateCommon();
506 mQuery.close();
507 mDriver.cursorClosed();
508 }
509
510 @Override
511 public boolean requery() {
512 if (isClosed()) {
513 return false;
514 }
515 long timeStart = 0;
516 if (Config.LOGV) {
517 timeStart = System.currentTimeMillis();
518 }
519 /*
520 * Synchronize on the database lock to ensure that mCount matches the
521 * results of mQuery.requery().
522 */
523 mDatabase.lock();
524 try {
525 if (mWindow != null) {
526 mWindow.clear();
527 }
528 mPos = -1;
529 // This one will recreate the temp table, and get its count
530 mDriver.cursorRequeried(this);
531 mCount = NO_COUNT;
532 mCursorState++;
533 queryThreadLock();
534 try {
535 mQuery.requery();
536 } finally {
537 queryThreadUnlock();
538 }
539 } finally {
540 mDatabase.unlock();
541 }
542
543 if (Config.LOGV) {
544 Log.v("DatabaseWindow", "closing window in requery()");
545 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
546 }
547
548 boolean result = super.requery();
549 if (Config.LOGV) {
550 long timeEnd = System.currentTimeMillis();
551 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
552 }
553 return result;
554 }
555
556 @Override
557 public void setWindow(CursorWindow window) {
558 if (mWindow != null) {
559 mCursorState++;
560 queryThreadLock();
561 try {
562 mWindow.close();
563 } finally {
564 queryThreadUnlock();
565 }
566 mCount = NO_COUNT;
567 }
568 mWindow = window;
569 }
570
571 /**
572 * Changes the selection arguments. The new values take effect after a call to requery().
573 */
574 public void setSelectionArguments(String[] selectionArgs) {
575 mDriver.setBindArguments(selectionArgs);
576 }
577
578 /**
579 * Release the native resources, if they haven't been released yet.
580 */
581 @Override
582 protected void finalize() {
583 try {
Vasu Nori42960e82010-01-06 23:12:02 -0800584 // if the cursor hasn't been closed yet, close it first
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800585 if (mWindow != null) {
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -0700586 if (StrictMode.vmSqliteObjectLeaksEnabled()) {
587 int len = mQuery.mSql.length();
588 StrictMode.onSqliteObjectLeaked(
589 "Finalizing a Cursor that has not been deactivated or closed. " +
Vasu Norid606b4b2010-02-24 12:54:20 -0800590 "database = " + mDatabase.getPath() + ", table = " + mEditTable +
Vasu Nori2cc1df02010-03-23 10:17:48 -0700591 ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len),
592 mStackTrace);
Brad Fitzpatrick32e60c72010-09-30 16:22:36 -0700593 }
Vasu Nori2cc1df02010-03-23 10:17:48 -0700594 close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800595 SQLiteDebug.notifyActiveCursorFinalized();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800596 } else {
597 if (Config.LOGV) {
Vasu Nori42960e82010-01-06 23:12:02 -0800598 Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() +
599 ", table = " + mEditTable + ", query = " + mQuery.mSql);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800600 }
601 }
602 } finally {
603 super.finalize();
604 }
605 }
606}