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