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