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