SQLiteCursor.java revision 1a3b3d48413d9134738c9b457292fb2b71a5dfe4
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                if (mCursorState != mThreadState) {
135                    mLock.unlock();
136                    break;
137                }
138                try {
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        try {
221            db.lock();
222
223            // Setup the list of columns
224            int columnCount = mQuery.columnCountLocked();
225            mColumns = new String[columnCount];
226
227            // Read in all column names
228            for (int i = 0; i < columnCount; i++) {
229                String columnName = mQuery.columnNameLocked(i);
230                mColumns[i] = columnName;
231                if (Config.LOGV) {
232                    Log.v("DatabaseWindow", "mColumns[" + i + "] is "
233                            + mColumns[i]);
234                }
235
236                // Make note of the row ID column index for quick access to it
237                if ("_id".equals(columnName)) {
238                    mRowIdColumnIndex = i;
239                }
240            }
241        } finally {
242            db.unlock();
243        }
244    }
245
246    /**
247     * @return the SQLiteDatabase that this cursor is associated with.
248     */
249    public SQLiteDatabase getDatabase() {
250        return mDatabase;
251    }
252
253    @Override
254    public boolean onMove(int oldPosition, int newPosition) {
255        // Make sure the row at newPosition is present in the window
256        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
257                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
258            fillWindow(newPosition);
259        }
260
261        return true;
262    }
263
264    @Override
265    public int getCount() {
266        if (mCount == NO_COUNT) {
267            fillWindow(0);
268        }
269        return mCount;
270    }
271
272    private void fillWindow (int startPos) {
273        if (mWindow == null) {
274            // If there isn't a window set already it will only be accessed locally
275            mWindow = new CursorWindow(true /* the window is local only */);
276        } else {
277            mCursorState++;
278                queryThreadLock();
279                try {
280                    mWindow.clear();
281                } finally {
282                    queryThreadUnlock();
283                }
284        }
285        mWindow.setStartPosition(startPos);
286        mCount = mQuery.fillWindow(mWindow, mInitialRead, 0);
287        // return -1 means not finished
288        if (mCount == NO_COUNT){
289            mCount = startPos + mInitialRead;
290            Thread t = new Thread(new QueryThread(mCursorState), "query thread");
291            t.start();
292        }
293    }
294
295    @Override
296    public int getColumnIndex(String columnName) {
297        // Create mColumnNameMap on demand
298        if (mColumnNameMap == null) {
299            String[] columns = mColumns;
300            int columnCount = columns.length;
301            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
302            for (int i = 0; i < columnCount; i++) {
303                map.put(columns[i], i);
304            }
305            mColumnNameMap = map;
306        }
307
308        // Hack according to bug 903852
309        final int periodIndex = columnName.lastIndexOf('.');
310        if (periodIndex != -1) {
311            Exception e = new Exception();
312            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
313            columnName = columnName.substring(periodIndex + 1);
314        }
315
316        Integer i = mColumnNameMap.get(columnName);
317        if (i != null) {
318            return i.intValue();
319        } else {
320            return -1;
321        }
322    }
323
324    /**
325     * @hide
326     * @deprecated
327     */
328    @Override
329    public boolean deleteRow() {
330        checkPosition();
331
332        // Only allow deletes if there is an ID column, and the ID has been read from it
333        if (mRowIdColumnIndex == -1 || mCurrentRowID == null) {
334            Log.e(TAG,
335                    "Could not delete row because either the row ID column is not available or it" +
336                    "has not been read.");
337            return false;
338        }
339
340        boolean success;
341
342        /*
343         * Ensure we don't change the state of the database when another
344         * thread is holding the database lock. requery() and moveTo() are also
345         * synchronized here to make sure they get the state of the database
346         * immediately following the DELETE.
347         */
348        mDatabase.lock();
349        try {
350            try {
351                mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?",
352                        new String[] {mCurrentRowID.toString()});
353                success = true;
354            } catch (SQLException e) {
355                success = false;
356            }
357
358            int pos = mPos;
359            requery();
360
361            /*
362             * Ensure proper cursor state. Note that mCurrentRowID changes
363             * in this call.
364             */
365            moveToPosition(pos);
366        } finally {
367            mDatabase.unlock();
368        }
369
370        if (success) {
371            onChange(true);
372            return true;
373        } else {
374            return false;
375        }
376    }
377
378    @Override
379    public String[] getColumnNames() {
380        return mColumns;
381    }
382
383    /**
384     * @hide
385     * @deprecated
386     */
387    @Override
388    public boolean supportsUpdates() {
389        return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable);
390    }
391
392    /**
393     * @hide
394     * @deprecated
395     */
396    @Override
397    public boolean commitUpdates(Map<? extends Long,
398            ? extends Map<String, Object>> additionalValues) {
399        if (!supportsUpdates()) {
400            Log.e(TAG, "commitUpdates not supported on this cursor, did you "
401                    + "include the _id column?");
402            return false;
403        }
404
405        /*
406         * Prevent other threads from changing the updated rows while they're
407         * being processed here.
408         */
409        synchronized (mUpdatedRows) {
410            if (additionalValues != null) {
411                mUpdatedRows.putAll(additionalValues);
412            }
413
414            if (mUpdatedRows.size() == 0) {
415                return true;
416            }
417
418            /*
419             * Prevent other threads from changing the database state while
420             * we process the updated rows, and prevents us from changing the
421             * database behind the back of another thread.
422             */
423            mDatabase.beginTransaction();
424            try {
425                StringBuilder sql = new StringBuilder(128);
426
427                // For each row that has been updated
428                for (Map.Entry<Long, Map<String, Object>> rowEntry :
429                        mUpdatedRows.entrySet()) {
430                    Map<String, Object> values = rowEntry.getValue();
431                    Long rowIdObj = rowEntry.getKey();
432
433                    if (rowIdObj == null || values == null) {
434                        throw new IllegalStateException("null rowId or values found! rowId = "
435                                + rowIdObj + ", values = " + values);
436                    }
437
438                    if (values.size() == 0) {
439                        continue;
440                    }
441
442                    long rowId = rowIdObj.longValue();
443
444                    Iterator<Map.Entry<String, Object>> valuesIter =
445                            values.entrySet().iterator();
446
447                    sql.setLength(0);
448                    sql.append("UPDATE " + mEditTable + " SET ");
449
450                    // For each column value that has been updated
451                    Object[] bindings = new Object[values.size()];
452                    int i = 0;
453                    while (valuesIter.hasNext()) {
454                        Map.Entry<String, Object> entry = valuesIter.next();
455                        sql.append(entry.getKey());
456                        sql.append("=?");
457                        bindings[i] = entry.getValue();
458                        if (valuesIter.hasNext()) {
459                            sql.append(", ");
460                        }
461                        i++;
462                    }
463
464                    sql.append(" WHERE " + mColumns[mRowIdColumnIndex]
465                            + '=' + rowId);
466                    sql.append(';');
467                    mDatabase.execSQL(sql.toString(), bindings);
468                    mDatabase.rowUpdated(mEditTable, rowId);
469                }
470                mDatabase.setTransactionSuccessful();
471            } finally {
472                mDatabase.endTransaction();
473            }
474
475            mUpdatedRows.clear();
476        }
477
478        // Let any change observers know about the update
479        onChange(true);
480
481        return true;
482    }
483
484    private void deactivateCommon() {
485        if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this);
486        mCursorState = 0;
487        if (mWindow != null) {
488            mWindow.close();
489            mWindow = null;
490        }
491        if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()");
492    }
493
494    @Override
495    public void deactivate() {
496        super.deactivate();
497        deactivateCommon();
498        mDriver.cursorDeactivated();
499    }
500
501    @Override
502    public void close() {
503        super.close();
504        deactivateCommon();
505        mQuery.close();
506        mDriver.cursorClosed();
507    }
508
509    @Override
510    public boolean requery() {
511        if (isClosed()) {
512            return false;
513        }
514        long timeStart = 0;
515        if (Config.LOGV) {
516            timeStart = System.currentTimeMillis();
517        }
518        /*
519         * Synchronize on the database lock to ensure that mCount matches the
520         * results of mQuery.requery().
521         */
522        mDatabase.lock();
523        try {
524            if (mWindow != null) {
525                mWindow.clear();
526            }
527            mPos = -1;
528            // This one will recreate the temp table, and get its count
529            mDriver.cursorRequeried(this);
530            mCount = NO_COUNT;
531            mCursorState++;
532            queryThreadLock();
533            try {
534                mQuery.requery();
535            } finally {
536                queryThreadUnlock();
537            }
538        } finally {
539            mDatabase.unlock();
540        }
541
542        if (Config.LOGV) {
543            Log.v("DatabaseWindow", "closing window in requery()");
544            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
545        }
546
547        boolean result = super.requery();
548        if (Config.LOGV) {
549            long timeEnd = System.currentTimeMillis();
550            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
551        }
552        return result;
553    }
554
555    @Override
556    public void setWindow(CursorWindow window) {
557        if (mWindow != null) {
558            mCursorState++;
559            queryThreadLock();
560            try {
561                mWindow.close();
562            } finally {
563                queryThreadUnlock();
564            }
565            mCount = NO_COUNT;
566        }
567        mWindow = window;
568    }
569
570    /**
571     * Changes the selection arguments. The new values take effect after a call to requery().
572     */
573    public void setSelectionArguments(String[] selectionArgs) {
574        mDriver.setBindArguments(selectionArgs);
575    }
576
577    /**
578     * Release the native resources, if they haven't been released yet.
579     */
580    @Override
581    protected void finalize() {
582        try {
583            // if the cursor hasn't been closed yet, close it first
584            if (mWindow != null) {
585                int len = mQuery.mSql.length();
586                Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " +
587                        "database = " + mDatabase.getPath() + ", table = " + mEditTable +
588                        ", query = " + mQuery.mSql.substring(0, (len > 100) ? 100 : len),
589                        mStackTrace);
590                close();
591                SQLiteDebug.notifyActiveCursorFinalized();
592            } else {
593                if (Config.LOGV) {
594                    Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() +
595                            ", table = " + mEditTable + ", query = " + mQuery.mSql);
596                }
597            }
598        } finally {
599            super.finalize();
600        }
601    }
602}
603