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