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