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