SQLiteCursor.java revision b18f27dbf43ee9028a11cafbca23d3fa318e278b
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 volatile 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 there is still more data to be retrieved from the resultset
142                    if (count != 0) {
143                        if (count == NO_COUNT){
144                            mCount += mMaxRead;
145                            if (Log.isLoggable(TAG, Log.DEBUG)) {
146                                Log.d(TAG, "received -1 from native_fill_window. read " +
147                                        mCount + " rows so far");
148                            }
149                            sendMessage();
150                        } else {
151                            mCount += count;
152                            if (Log.isLoggable(TAG, Log.DEBUG)) {
153                                Log.d(TAG, "received all data from native_fill_window. read " +
154                                        mCount + " rows.");
155                            }
156                            sendMessage();
157                            break;
158                        }
159                    } else {
160                        break;
161                    }
162                } catch (Exception e) {
163                    // end the tread when the cursor is close
164                    break;
165                } finally {
166                    mLock.unlock();
167                }
168            }
169        }
170    }
171
172    /**
173     * @hide
174     */
175    protected class MainThreadNotificationHandler extends Handler {
176        public void handleMessage(Message msg) {
177            notifyDataSetChange();
178        }
179    }
180
181    /**
182     * @hide
183     */
184    protected MainThreadNotificationHandler mNotificationHandler;
185
186    public void registerDataSetObserver(DataSetObserver observer) {
187        super.registerDataSetObserver(observer);
188        if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) &&
189                mNotificationHandler == null) {
190            queryThreadLock();
191            try {
192                mNotificationHandler = new MainThreadNotificationHandler();
193                if (mPendingData) {
194                    notifyDataSetChange();
195                    mPendingData = false;
196                }
197            } finally {
198                queryThreadUnlock();
199            }
200        }
201
202    }
203
204    /**
205     * Execute a query and provide access to its result set through a Cursor
206     * interface. For a query such as: {@code SELECT name, birth, phone FROM
207     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
208     * phone) would be in the projection argument and everything from
209     * {@code FROM} onward would be in the params argument. This constructor
210     * has package scope.
211     *
212     * @param db a reference to a Database object that is already constructed
213     *     and opened. This param is not used any longer
214     * @param editTable the name of the table used for this query
215     * @param query the rest of the query terms
216     *     cursor is finalized
217     * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
218     */
219    @Deprecated
220    public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
221            String editTable, SQLiteQuery query) {
222        this(driver, editTable, query);
223    }
224
225    /**
226     * Execute a query and provide access to its result set through a Cursor
227     * interface. For a query such as: {@code SELECT name, birth, phone FROM
228     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
229     * phone) would be in the projection argument and everything from
230     * {@code FROM} onward would be in the params argument. This constructor
231     * has package scope.
232     *
233     * @param editTable the name of the table used for this query
234     * @param query the {@link SQLiteQuery} object associated with this cursor object.
235     */
236    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
237        // The AbstractCursor constructor needs to do some setup.
238        super();
239        if (query == null) {
240            throw new IllegalArgumentException("query object cannot be null");
241        }
242        if (query.mDatabase == null) {
243            throw new IllegalArgumentException("query.mDatabase cannot be null");
244        }
245        mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
246        mDriver = driver;
247        mEditTable = editTable;
248        mColumnNameMap = null;
249        mQuery = query;
250
251        try {
252            query.mDatabase.lock();
253
254            // Setup the list of columns
255            int columnCount = mQuery.columnCountLocked();
256            mColumns = new String[columnCount];
257
258            // Read in all column names
259            for (int i = 0; i < columnCount; i++) {
260                String columnName = mQuery.columnNameLocked(i);
261                mColumns[i] = columnName;
262                if (Config.LOGV) {
263                    Log.v("DatabaseWindow", "mColumns[" + i + "] is "
264                            + mColumns[i]);
265                }
266
267                // Make note of the row ID column index for quick access to it
268                if ("_id".equals(columnName)) {
269                    mRowIdColumnIndex = i;
270                }
271            }
272        } finally {
273            query.mDatabase.unlock();
274        }
275    }
276
277    /**
278     * @return the SQLiteDatabase that this cursor is associated with.
279     */
280    public SQLiteDatabase getDatabase() {
281        synchronized (this) {
282            return mQuery.mDatabase;
283        }
284    }
285
286    @Override
287    public boolean onMove(int oldPosition, int newPosition) {
288        // Make sure the row at newPosition is present in the window
289        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
290                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
291            fillWindow(newPosition);
292        }
293
294        return true;
295    }
296
297    @Override
298    public int getCount() {
299        if (mCount == NO_COUNT) {
300            fillWindow(0);
301        }
302        return mCount;
303    }
304
305    private void fillWindow (int startPos) {
306        if (mWindow == null) {
307            // If there isn't a window set already it will only be accessed locally
308            mWindow = new CursorWindow(true /* the window is local only */);
309        } else {
310            mCursorState++;
311                queryThreadLock();
312                try {
313                    mWindow.clear();
314                } finally {
315                    queryThreadUnlock();
316                }
317        }
318        mWindow.setStartPosition(startPos);
319        int count = getQuery().fillWindow(mWindow, mInitialRead, 0);
320        // return -1 means there is still more data to be retrieved from the resultset
321        if (count == NO_COUNT){
322            mCount = startPos + mInitialRead;
323            if (Log.isLoggable(TAG, Log.DEBUG)) {
324                Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far");
325            }
326            Thread t = new Thread(new QueryThread(mCursorState), "query thread");
327            t.start();
328        } else if (startPos == 0) { // native_fill_window returns count(*) only for startPos = 0
329            if (Log.isLoggable(TAG, Log.DEBUG)) {
330                Log.d(TAG, "received count(*) from native_fill_window: " + count);
331            }
332            mCount = count;
333        } else if (mCount <= 0) {
334            throw new IllegalStateException("count should never be non-zero negative number");
335        }
336    }
337
338    private synchronized SQLiteQuery getQuery() {
339        return mQuery;
340    }
341
342    @Override
343    public int getColumnIndex(String columnName) {
344        // Create mColumnNameMap on demand
345        if (mColumnNameMap == null) {
346            String[] columns = mColumns;
347            int columnCount = columns.length;
348            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
349            for (int i = 0; i < columnCount; i++) {
350                map.put(columns[i], i);
351            }
352            mColumnNameMap = map;
353        }
354
355        // Hack according to bug 903852
356        final int periodIndex = columnName.lastIndexOf('.');
357        if (periodIndex != -1) {
358            Exception e = new Exception();
359            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
360            columnName = columnName.substring(periodIndex + 1);
361        }
362
363        Integer i = mColumnNameMap.get(columnName);
364        if (i != null) {
365            return i.intValue();
366        } else {
367            return -1;
368        }
369    }
370
371    @Override
372    public String[] getColumnNames() {
373        return mColumns;
374    }
375
376    private void deactivateCommon() {
377        if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
378        mCursorState = 0;
379        if (mWindow != null) {
380            mWindow.close();
381            mWindow = null;
382        }
383        if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
384    }
385
386    @Override
387    public void deactivate() {
388        super.deactivate();
389        deactivateCommon();
390        mDriver.cursorDeactivated();
391    }
392
393    @Override
394    public void close() {
395        super.close();
396        synchronized (this) {
397            deactivateCommon();
398            mQuery.close();
399            mDriver.cursorClosed();
400        }
401    }
402
403    /**
404     * Show a warning against the use of requery() if called on the main thread.
405     * This warning is shown per database per process.
406     */
407    private void warnIfUiThread() {
408        if (Looper.getMainLooper() == Looper.myLooper()) {
409            String databasePath = getQuery().mDatabase.getPath();
410            // We show the warning once per database in order not to spam logcat.
411            if (!sAlreadyWarned.containsKey(databasePath)) {
412                sAlreadyWarned.put(databasePath, true);
413                String packageName = ActivityThread.currentPackageName();
414                Throwable t = null;
415                // BEGIN STOPSHIP remove the following line
416                t = new RequeryOnUiThreadException(packageName);
417                // END STOPSHIP
418                Log.w(TAG, "should not attempt requery on main (UI) thread: app = " +
419                        packageName == null ? "'unknown'" : packageName, t);
420            }
421        }
422    }
423
424    @Override
425    public boolean requery() {
426        if (isClosed()) {
427            return false;
428        }
429        warnIfUiThread();
430        long timeStart = 0;
431        if (Config.LOGV) {
432            timeStart = System.currentTimeMillis();
433        }
434
435        synchronized (this) {
436            if (mWindow != null) {
437                mWindow.clear();
438            }
439            mPos = -1;
440            SQLiteDatabase db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
441            if (!db.equals(mQuery.mDatabase)) {
442                // since we need to use a different database connection handle,
443                // re-compile the query
444                db.lock();
445                try {
446                    // close the old mQuery object and open a new one
447                    mQuery.close();
448                    mQuery = new SQLiteQuery(db, mQuery);
449                } finally {
450                    db.unlock();
451                }
452            }
453            // This one will recreate the temp table, and get its count
454            mDriver.cursorRequeried(this);
455            mCount = NO_COUNT;
456            mCursorState++;
457            queryThreadLock();
458            try {
459                mQuery.requery();
460            } finally {
461                queryThreadUnlock();
462            }
463        }
464
465        if (Config.LOGV) {
466            Log.v("DatabaseWindow", "closing window in requery()");
467            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
468        }
469
470        boolean result = super.requery();
471        if (Config.LOGV) {
472            long timeEnd = System.currentTimeMillis();
473            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
474        }
475        return result;
476    }
477
478    @Override
479    public void setWindow(CursorWindow window) {
480        if (mWindow != null) {
481            mCursorState++;
482            queryThreadLock();
483            try {
484                mWindow.close();
485            } finally {
486                queryThreadUnlock();
487            }
488            mCount = NO_COUNT;
489        }
490        mWindow = window;
491    }
492
493    /**
494     * Changes the selection arguments. The new values take effect after a call to requery().
495     */
496    public void setSelectionArguments(String[] selectionArgs) {
497        mDriver.setBindArguments(selectionArgs);
498    }
499
500    /**
501     * Release the native resources, if they haven't been released yet.
502     */
503    @Override
504    protected void finalize() {
505        try {
506            // if the cursor hasn't been closed yet, close it first
507            if (mWindow != null) {
508                int len = mQuery.mSql.length();
509                Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " +
510                        "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
511                        ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len),
512                        mStackTrace);
513                close();
514                SQLiteDebug.notifyActiveCursorFinalized();
515            } else {
516                if (Config.LOGV) {
517                    Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
518                            ", table = " + mEditTable + ", query = " + mQuery.mSql);
519                }
520            }
521        } finally {
522            super.finalize();
523        }
524    }
525
526    /**
527     * this is only for testing purposes.
528     */
529    /* package */ int getMCount() {
530        return mCount;
531    }
532}
533