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