SQLiteCursor.java revision 7ce745248d4de0e6543a559c93423df899832100
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        // The AbstractCursor constructor needs to do some setup.
93        super();
94        if (query == null) {
95            throw new IllegalArgumentException("query object cannot be null");
96        }
97        if (query.mDatabase == null) {
98            throw new IllegalArgumentException("query.mDatabase cannot be null");
99        }
100        mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
101        mDriver = driver;
102        mEditTable = editTable;
103        mColumnNameMap = null;
104        mQuery = query;
105
106        query.mDatabase.lock(query.mSql);
107        try {
108            // Setup the list of columns
109            int columnCount = mQuery.columnCountLocked();
110            mColumns = new String[columnCount];
111
112            // Read in all column names
113            for (int i = 0; i < columnCount; i++) {
114                String columnName = mQuery.columnNameLocked(i);
115                mColumns[i] = columnName;
116                if (false) {
117                    Log.v("DatabaseWindow", "mColumns[" + i + "] is "
118                            + mColumns[i]);
119                }
120
121                // Make note of the row ID column index for quick access to it
122                if ("_id".equals(columnName)) {
123                    mRowIdColumnIndex = i;
124                }
125            }
126        } finally {
127            query.mDatabase.unlock();
128        }
129    }
130
131    /**
132     * @return the SQLiteDatabase that this cursor is associated with.
133     */
134    public SQLiteDatabase getDatabase() {
135        synchronized (this) {
136            return mQuery.mDatabase;
137        }
138    }
139
140    @Override
141    public boolean onMove(int oldPosition, int newPosition) {
142        // Make sure the row at newPosition is present in the window
143        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
144                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
145            fillWindow(newPosition);
146        }
147
148        return true;
149    }
150
151    @Override
152    public int getCount() {
153        if (mCount == NO_COUNT) {
154            fillWindow(0);
155        }
156        return mCount;
157    }
158
159    private void fillWindow(int startPos) {
160        if (mWindow == null) {
161            // If there isn't a window set already it will only be accessed locally
162            mWindow = new CursorWindow(true /* the window is local only */);
163        } else {
164            mWindow.clear();
165        }
166        mWindow.setStartPosition(startPos);
167        int count = getQuery().fillWindow(mWindow);
168        if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0
169            if (Log.isLoggable(TAG, Log.DEBUG)) {
170                Log.d(TAG, "received count(*) from native_fill_window: " + count);
171            }
172            mCount = count;
173        } else if (mCount <= 0) {
174            throw new IllegalStateException("Row count should never be zero or negative "
175                    + "when the start position is non-zero");
176        }
177    }
178
179    private synchronized SQLiteQuery getQuery() {
180        return mQuery;
181    }
182
183    @Override
184    public int getColumnIndex(String columnName) {
185        // Create mColumnNameMap on demand
186        if (mColumnNameMap == null) {
187            String[] columns = mColumns;
188            int columnCount = columns.length;
189            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
190            for (int i = 0; i < columnCount; i++) {
191                map.put(columns[i], i);
192            }
193            mColumnNameMap = map;
194        }
195
196        // Hack according to bug 903852
197        final int periodIndex = columnName.lastIndexOf('.');
198        if (periodIndex != -1) {
199            Exception e = new Exception();
200            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
201            columnName = columnName.substring(periodIndex + 1);
202        }
203
204        Integer i = mColumnNameMap.get(columnName);
205        if (i != null) {
206            return i.intValue();
207        } else {
208            return -1;
209        }
210    }
211
212    @Override
213    public String[] getColumnNames() {
214        return mColumns;
215    }
216
217    private void deactivateCommon() {
218        if (false) Log.v(TAG, "<<< Releasing cursor " + this);
219        closeWindow();
220        if (false) Log.v("DatabaseWindow", "closing window in release()");
221    }
222
223    @Override
224    public void deactivate() {
225        super.deactivate();
226        deactivateCommon();
227        mDriver.cursorDeactivated();
228    }
229
230    @Override
231    public void close() {
232        super.close();
233        synchronized (this) {
234            deactivateCommon();
235            mQuery.close();
236            mDriver.cursorClosed();
237        }
238    }
239
240    @Override
241    public boolean requery() {
242        if (isClosed()) {
243            return false;
244        }
245        long timeStart = 0;
246        if (false) {
247            timeStart = System.currentTimeMillis();
248        }
249
250        synchronized (this) {
251            if (mWindow != null) {
252                mWindow.clear();
253            }
254            mPos = -1;
255            SQLiteDatabase db = null;
256            try {
257                db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql);
258            } catch (IllegalStateException e) {
259                // for backwards compatibility, just return false
260                Log.w(TAG, "requery() failed " + e.getMessage(), e);
261                return false;
262            }
263            if (!db.equals(mQuery.mDatabase)) {
264                // since we need to use a different database connection handle,
265                // re-compile the query
266                try {
267                    db.lock(mQuery.mSql);
268                } catch (IllegalStateException e) {
269                    // for backwards compatibility, just return false
270                    Log.w(TAG, "requery() failed " + e.getMessage(), e);
271                    return false;
272                }
273                try {
274                    // close the old mQuery object and open a new one
275                    mQuery.close();
276                    mQuery = new SQLiteQuery(db, mQuery);
277                } catch (IllegalStateException e) {
278                    // for backwards compatibility, just return false
279                    Log.w(TAG, "requery() failed " + e.getMessage(), e);
280                    return false;
281                } finally {
282                    db.unlock();
283                }
284            }
285            // This one will recreate the temp table, and get its count
286            mDriver.cursorRequeried(this);
287            mCount = NO_COUNT;
288            try {
289                mQuery.requery();
290            } catch (IllegalStateException e) {
291                // for backwards compatibility, just return false
292                Log.w(TAG, "requery() failed " + e.getMessage(), e);
293                return false;
294            }
295        }
296
297        if (false) {
298            Log.v("DatabaseWindow", "closing window in requery()");
299            Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery);
300        }
301
302        boolean result = false;
303        try {
304            result = super.requery();
305        } catch (IllegalStateException e) {
306            // for backwards compatibility, just return false
307            Log.w(TAG, "requery() failed " + e.getMessage(), e);
308        }
309        if (false) {
310            long timeEnd = System.currentTimeMillis();
311            Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString());
312        }
313        return result;
314    }
315
316    @Override
317    public void setWindow(CursorWindow window) {
318        super.setWindow(window);
319        mCount = NO_COUNT;
320    }
321
322    /**
323     * Changes the selection arguments. The new values take effect after a call to requery().
324     */
325    public void setSelectionArguments(String[] selectionArgs) {
326        mDriver.setBindArguments(selectionArgs);
327    }
328
329    /**
330     * Release the native resources, if they haven't been released yet.
331     */
332    @Override
333    protected void finalize() {
334        try {
335            // if the cursor hasn't been closed yet, close it first
336            if (mWindow != null) {
337                if (StrictMode.vmSqliteObjectLeaksEnabled()) {
338                    int len = mQuery.mSql.length();
339                    StrictMode.onSqliteObjectLeaked(
340                        "Finalizing a Cursor that has not been deactivated or closed. " +
341                        "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable +
342                        ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len),
343                        mStackTrace);
344                }
345                close();
346                SQLiteDebug.notifyActiveCursorFinalized();
347            } else {
348                if (false) {
349                    Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() +
350                            ", table = " + mEditTable + ", query = " + mQuery.mSql);
351                }
352            }
353        } finally {
354            super.finalize();
355        }
356    }
357}
358