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