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