SQLiteCursor.java revision 65a8883f0e605bb8a73a692987b47ce5da632e72
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 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 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. 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        try {
244            query.mDatabase.lock();
245
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        mCount = getQuery().fillWindow(mWindow, mInitialRead, 0);
312        // return -1 means not finished
313        if (mCount == NO_COUNT){
314            mCount = startPos + mInitialRead;
315            Thread t = new Thread(new QueryThread(mCursorState), "query thread");
316            t.start();
317        }
318    }
319
320    private synchronized SQLiteQuery getQuery() {
321        return mQuery;
322    }
323
324    @Override
325    public int getColumnIndex(String columnName) {
326        // Create mColumnNameMap on demand
327        if (mColumnNameMap == null) {
328            String[] columns = mColumns;
329            int columnCount = columns.length;
330            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
331            for (int i = 0; i < columnCount; i++) {
332                map.put(columns[i], i);
333            }
334            mColumnNameMap = map;
335        }
336
337        // Hack according to bug 903852
338        final int periodIndex = columnName.lastIndexOf('.');
339        if (periodIndex != -1) {
340            Exception e = new Exception();
341            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
342            columnName = columnName.substring(periodIndex + 1);
343        }
344
345        Integer i = mColumnNameMap.get(columnName);
346        if (i != null) {
347            return i.intValue();
348        } else {
349            return -1;
350        }
351    }
352
353    @Override
354    public String[] getColumnNames() {
355        return mColumns;
356    }
357
358    private void deactivateCommon() {
359        if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
360        mCursorState = 0;
361        if (mWindow != null) {
362            mWindow.close();
363            mWindow = null;
364        }
365        if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
366    }
367
368    @Override
369    public void deactivate() {
370        super.deactivate();
371        deactivateCommon();
372        mDriver.cursorDeactivated();
373    }
374
375    @Override
376    public void close() {
377        super.close();
378        synchronized (this) {
379            deactivateCommon();
380            mQuery.close();
381            mDriver.cursorClosed();
382        }
383    }
384
385    /**
386     * Show a warning against the use of requery() if called on the main thread.
387     * This warning is shown per database per process.
388     */
389    private void warnIfUiThread() {
390        if (Looper.getMainLooper() == Looper.myLooper()) {
391            String databasePath = getQuery().mDatabase.getPath();
392            // We show the warning once per database in order not to spam logcat.
393            if (!sAlreadyWarned.containsKey(databasePath)) {
394                sAlreadyWarned.put(databasePath, true);
395                String packageName = ActivityThread.currentPackageName();
396                Log.w(TAG, "should not attempt requery on main (UI) thread: app = " +
397                        packageName == null ? "'unknown'" : packageName,
398                        new RequeryOnUiThreadException(packageName));
399            }
400        }
401    }
402
403    @Override
404    public boolean requery() {
405        if (isClosed()) {
406            return false;
407        }
408        warnIfUiThread();
409        long timeStart = 0;
410        if (Config.LOGV) {
411            timeStart = System.currentTimeMillis();
412        }
413
414        synchronized (this) {
415            if (mWindow != null) {
416                mWindow.clear();
417            }
418            mPos = -1;
419            SQLiteDatabase db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
420            if (!db.equals(mQuery.mDatabase)) {
421                // since we need to use a different database connection handle,
422                // re-compile the query
423                db.lock();
424                try {
425                    // close the old mQuery object and open a new one
426                    mQuery.close();
427                    mQuery = new SQLiteQuery(db, mQuery);
428                } finally {
429                    db.unlock();
430                }
431            }
432            // This one will recreate the temp table, and get its count
433            mDriver.cursorRequeried(this);
434            mCount = NO_COUNT;
435            mCursorState++;
436            queryThreadLock();
437            try {
438                mQuery.requery();
439            } finally {
440                queryThreadUnlock();
441            }
442        }
443
444        if (Config.LOGV) {
445            Log.v("DatabaseWindow", "closing window in requery()");
446            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
447        }
448
449        boolean result = super.requery();
450        if (Config.LOGV) {
451            long timeEnd = System.currentTimeMillis();
452            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
453        }
454        return result;
455    }
456
457    @Override
458    public void setWindow(CursorWindow window) {
459        if (mWindow != null) {
460            mCursorState++;
461            queryThreadLock();
462            try {
463                mWindow.close();
464            } finally {
465                queryThreadUnlock();
466            }
467            mCount = NO_COUNT;
468        }
469        mWindow = window;
470    }
471
472    /**
473     * Changes the selection arguments. The new values take effect after a call to requery().
474     */
475    public void setSelectionArguments(String[] selectionArgs) {
476        mDriver.setBindArguments(selectionArgs);
477    }
478
479    /**
480     * Release the native resources, if they haven't been released yet.
481     */
482    @Override
483    protected void finalize() {
484        try {
485            // if the cursor hasn't been closed yet, close it first
486            if (mWindow != null) {
487                int len = mQuery.mSql.length();
488                Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " +
489                        "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
490                        ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len),
491                        mStackTrace);
492                close();
493                SQLiteDebug.notifyActiveCursorFinalized();
494            } else {
495                if (Config.LOGV) {
496                    Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
497                            ", table = " + mEditTable + ", query = " + mQuery.mSql);
498                }
499            }
500        } finally {
501            super.finalize();
502        }
503    }
504}
505