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