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