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