SQLiteCursor.java revision 7978a414bbbc737bfb342db8840c29376e33a34d
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.os.StrictMode;
22import android.util.Log;
23
24import java.util.HashMap;
25import java.util.Map;
26
27/**
28 * A Cursor implementation that exposes results from a query on a
29 * {@link SQLiteDatabase}.
30 *
31 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple
32 * threads should perform its own synchronization when using the SQLiteCursor.
33 */
34public class SQLiteCursor extends AbstractWindowedCursor {
35    static final String TAG = "SQLiteCursor";
36    static final int NO_COUNT = -1;
37
38    /** The name of the table to edit */
39    private final String mEditTable;
40
41    /** The names of the columns in the rows */
42    private final String[] mColumns;
43
44    /** The query object for the cursor */
45    private SQLiteQuery mQuery;
46
47    /** The compiled query this cursor came from */
48    private final SQLiteCursorDriver mDriver;
49
50    /** The number of rows in the cursor */
51    private volatile int mCount = NO_COUNT;
52
53    /** A mapping of column names to column indices, to speed up lookups */
54    private Map<String, Integer> mColumnNameMap;
55
56    /** Used to find out where a cursor was allocated in case it never got released. */
57    private final Throwable mStackTrace;
58
59    /**
60     * Execute a query and provide access to its result set through a Cursor
61     * interface. For a query such as: {@code SELECT name, birth, phone FROM
62     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
63     * phone) would be in the projection argument and everything from
64     * {@code FROM} onward would be in the params argument. This constructor
65     * has package scope.
66     *
67     * @param db a reference to a Database object that is already constructed
68     *     and opened. This param is not used any longer
69     * @param editTable the name of the table used for this query
70     * @param query the rest of the query terms
71     *     cursor is finalized
72     * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
73     */
74    @Deprecated
75    public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
76            String editTable, SQLiteQuery query) {
77        this(driver, editTable, query);
78    }
79
80    /**
81     * Execute a query and provide access to its result set through a Cursor
82     * interface. For a query such as: {@code SELECT name, birth, phone FROM
83     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
84     * phone) would be in the projection argument and everything from
85     * {@code FROM} onward would be in the params argument. This constructor
86     * has package scope.
87     *
88     * @param editTable the name of the table used for this query
89     * @param query the {@link SQLiteQuery} object associated with this cursor object.
90     */
91    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
92        if (query == null) {
93            throw new IllegalArgumentException("query object cannot be null");
94        }
95        if (query.mDatabase == null) {
96            throw new IllegalArgumentException("query.mDatabase cannot be null");
97        }
98        if (StrictMode.vmSqliteObjectLeaksEnabled()) {
99            mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
100        } else {
101            mStackTrace = null;
102        }
103        mDriver = driver;
104        mEditTable = editTable;
105        mColumnNameMap = null;
106        mQuery = query;
107
108        query.mDatabase.lock(query.mSql);
109        try {
110            // Setup the list of columns
111            int columnCount = mQuery.columnCountLocked();
112            mColumns = new String[columnCount];
113
114            // Read in all column names
115            for (int i = 0; i < columnCount; i++) {
116                String columnName = mQuery.columnNameLocked(i);
117                mColumns[i] = columnName;
118                if (false) {
119                    Log.v("DatabaseWindow", "mColumns[" + i + "] is "
120                            + mColumns[i]);
121                }
122
123                // Make note of the row ID column index for quick access to it
124                if ("_id".equals(columnName)) {
125                    mRowIdColumnIndex = i;
126                }
127            }
128        } finally {
129            query.mDatabase.unlock();
130        }
131    }
132
133    /**
134     * @return the SQLiteDatabase that this cursor is associated with.
135     */
136    public SQLiteDatabase getDatabase() {
137        synchronized (this) {
138            return mQuery.mDatabase;
139        }
140    }
141
142    @Override
143    public boolean onMove(int oldPosition, int newPosition) {
144        // Make sure the row at newPosition is present in the window
145        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
146                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
147            fillWindow(newPosition);
148        }
149
150        return true;
151    }
152
153    @Override
154    public int getCount() {
155        if (mCount == NO_COUNT) {
156            fillWindow(0);
157        }
158        return mCount;
159    }
160
161    private void fillWindow(int startPos) {
162        clearOrCreateWindow(getDatabase().getPath());
163        mWindow.setStartPosition(startPos);
164        int count = getQuery().fillWindow(mWindow);
165        if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0
166            if (Log.isLoggable(TAG, Log.DEBUG)) {
167                Log.d(TAG, "received count(*) from native_fill_window: " + count);
168            }
169            mCount = count;
170        } else if (mCount <= 0) {
171            throw new IllegalStateException("Row count should never be zero or negative "
172                    + "when the start position is non-zero");
173        }
174    }
175
176    private synchronized SQLiteQuery getQuery() {
177        return mQuery;
178    }
179
180    @Override
181    public int getColumnIndex(String columnName) {
182        // Create mColumnNameMap on demand
183        if (mColumnNameMap == null) {
184            String[] columns = mColumns;
185            int columnCount = columns.length;
186            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
187            for (int i = 0; i < columnCount; i++) {
188                map.put(columns[i], i);
189            }
190            mColumnNameMap = map;
191        }
192
193        // Hack according to bug 903852
194        final int periodIndex = columnName.lastIndexOf('.');
195        if (periodIndex != -1) {
196            Exception e = new Exception();
197            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
198            columnName = columnName.substring(periodIndex + 1);
199        }
200
201        Integer i = mColumnNameMap.get(columnName);
202        if (i != null) {
203            return i.intValue();
204        } else {
205            return -1;
206        }
207    }
208
209    @Override
210    public String[] getColumnNames() {
211        return mColumns;
212    }
213
214    @Override
215    public void deactivate() {
216        super.deactivate();
217        mDriver.cursorDeactivated();
218    }
219
220    @Override
221    public void close() {
222        super.close();
223        synchronized (this) {
224            mQuery.close();
225            mDriver.cursorClosed();
226        }
227    }
228
229    @Override
230    public boolean requery() {
231        if (isClosed()) {
232            return false;
233        }
234        long timeStart = 0;
235        if (false) {
236            timeStart = System.currentTimeMillis();
237        }
238
239        synchronized (this) {
240            if (mWindow != null) {
241                mWindow.clear();
242            }
243            mPos = -1;
244            SQLiteDatabase db = null;
245            try {
246                db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
247            } catch (IllegalStateException e) {
248                // for backwards compatibility, just return false
249                Log.w(TAG, "requery() failed " + e.getMessage(), e);
250                return false;
251            }
252            if (!db.equals(mQuery.mDatabase)) {
253                // since we need to use a different database connection handle,
254                // re-compile the query
255                try {
256                    db.lock(mQuery.mSql);
257                } catch (IllegalStateException e) {
258                    // for backwards compatibility, just return false
259                    Log.w(TAG, "requery() failed " + e.getMessage(), e);
260                    return false;
261                }
262                try {
263                    // close the old mQuery object and open a new one
264                    mQuery.close();
265                    mQuery = new SQLiteQuery(db, mQuery);
266                } catch (IllegalStateException e) {
267                    // for backwards compatibility, just return false
268                    Log.w(TAG, "requery() failed " + e.getMessage(), e);
269                    return false;
270                } finally {
271                    db.unlock();
272                }
273            }
274            // This one will recreate the temp table, and get its count
275            mDriver.cursorRequeried(this);
276            mCount = NO_COUNT;
277            try {
278                mQuery.requery();
279            } catch (IllegalStateException e) {
280                // for backwards compatibility, just return false
281                Log.w(TAG, "requery() failed " + e.getMessage(), e);
282                return false;
283            }
284        }
285
286        if (false) {
287            Log.v("DatabaseWindow", "closing window in requery()");
288            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
289        }
290
291        boolean result = false;
292        try {
293            result = super.requery();
294        } catch (IllegalStateException e) {
295            // for backwards compatibility, just return false
296            Log.w(TAG, "requery() failed " + e.getMessage(), e);
297        }
298        if (false) {
299            long timeEnd = System.currentTimeMillis();
300            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
301        }
302        return result;
303    }
304
305    @Override
306    public void setWindow(CursorWindow window) {
307        super.setWindow(window);
308        mCount = NO_COUNT;
309    }
310
311    /**
312     * Changes the selection arguments. The new values take effect after a call to requery().
313     */
314    public void setSelectionArguments(String[] selectionArgs) {
315        mDriver.setBindArguments(selectionArgs);
316    }
317
318    /**
319     * Release the native resources, if they haven't been released yet.
320     */
321    @Override
322    protected void finalize() {
323        try {
324            // if the cursor hasn't been closed yet, close it first
325            if (mWindow != null) {
326                if (mStackTrace != null) {
327                    int len = mQuery.mSql.length();
328                    StrictMode.onSqliteObjectLeaked(
329                        "Finalizing a Cursor that has not been deactivated or closed. " +
330                        "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
331                        ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len),
332                        mStackTrace);
333                }
334                close();
335                SQLiteDebug.notifyActiveCursorFinalized();
336            } else {
337                if (false) {
338                    Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
339                            ", table = " + mEditTable + ", query = " + mQuery.mSql);
340                }
341            }
342        } finally {
343            super.finalize();
344        }
345    }
346}
347