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.
69     *
70     * @param db a reference to a Database object that is already constructed
71     *     and opened. This param is not used any longer
72     * @param editTable the name of the table used for this query
73     * @param query the rest of the query terms
74     *     cursor is finalized
75     * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead
76     */
77    @Deprecated
78    public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver,
79            String editTable, SQLiteQuery query) {
80        this(driver, editTable, query);
81    }
82
83    /**
84     * Execute a query and provide access to its result set through a Cursor
85     * interface. For a query such as: {@code SELECT name, birth, phone FROM
86     * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth,
87     * phone) would be in the projection argument and everything from
88     * {@code FROM} onward would be in the params argument.
89     *
90     * @param editTable the name of the table used for this query
91     * @param query the {@link SQLiteQuery} object associated with this cursor object.
92     */
93    public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
94        if (query == null) {
95            throw new IllegalArgumentException("query object cannot be null");
96        }
97        if (StrictMode.vmSqliteObjectLeaksEnabled()) {
98            mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
99        } else {
100            mStackTrace = null;
101        }
102        mDriver = driver;
103        mEditTable = editTable;
104        mColumnNameMap = null;
105        mQuery = query;
106
107        mColumns = query.getColumnNames();
108        mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns);
109    }
110
111    /**
112     * Get the database that this cursor is associated with.
113     * @return the SQLiteDatabase that this cursor is associated with.
114     */
115    public SQLiteDatabase getDatabase() {
116        return mQuery.getDatabase();
117    }
118
119    @Override
120    public boolean onMove(int oldPosition, int newPosition) {
121        // Make sure the row at newPosition is present in the window
122        if (mWindow == null || newPosition < mWindow.getStartPosition() ||
123                newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
124            fillWindow(newPosition);
125        }
126
127        return true;
128    }
129
130    @Override
131    public int getCount() {
132        if (mCount == NO_COUNT) {
133            fillWindow(0);
134        }
135        return mCount;
136    }
137
138    private void fillWindow(int requiredPos) {
139        clearOrCreateWindow(getDatabase().getPath());
140
141        try {
142            if (mCount == NO_COUNT) {
143                int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
144                mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
145                mCursorWindowCapacity = mWindow.getNumRows();
146                if (Log.isLoggable(TAG, Log.DEBUG)) {
147                    Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
148                }
149            } else {
150                int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
151                        mCursorWindowCapacity);
152                mQuery.fillWindow(mWindow, startPos, requiredPos, false);
153            }
154        } catch (RuntimeException ex) {
155            // Close the cursor window if the query failed and therefore will
156            // not produce any results.  This helps to avoid accidentally leaking
157            // the cursor window if the client does not correctly handle exceptions
158            // and fails to close the cursor.
159            closeWindow();
160            throw ex;
161        }
162    }
163
164    @Override
165    public int getColumnIndex(String columnName) {
166        // Create mColumnNameMap on demand
167        if (mColumnNameMap == null) {
168            String[] columns = mColumns;
169            int columnCount = columns.length;
170            HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
171            for (int i = 0; i < columnCount; i++) {
172                map.put(columns[i], i);
173            }
174            mColumnNameMap = map;
175        }
176
177        // Hack according to bug 903852
178        final int periodIndex = columnName.lastIndexOf('.');
179        if (periodIndex != -1) {
180            Exception e = new Exception();
181            Log.e(TAG, "requesting column name with table name -- " + columnName, e);
182            columnName = columnName.substring(periodIndex + 1);
183        }
184
185        Integer i = mColumnNameMap.get(columnName);
186        if (i != null) {
187            return i.intValue();
188        } else {
189            return -1;
190        }
191    }
192
193    @Override
194    public String[] getColumnNames() {
195        return mColumns;
196    }
197
198    @Override
199    public void deactivate() {
200        super.deactivate();
201        mDriver.cursorDeactivated();
202    }
203
204    @Override
205    public void close() {
206        super.close();
207        synchronized (this) {
208            mQuery.close();
209            mDriver.cursorClosed();
210        }
211    }
212
213    @Override
214    public boolean requery() {
215        if (isClosed()) {
216            return false;
217        }
218
219        synchronized (this) {
220            if (!mQuery.getDatabase().isOpen()) {
221                return false;
222            }
223
224            if (mWindow != null) {
225                mWindow.clear();
226            }
227            mPos = -1;
228            mCount = NO_COUNT;
229
230            mDriver.cursorRequeried(this);
231        }
232
233        try {
234            return super.requery();
235        } catch (IllegalStateException e) {
236            // for backwards compatibility, just return false
237            Log.w(TAG, "requery() failed " + e.getMessage(), e);
238            return false;
239        }
240    }
241
242    @Override
243    public void setWindow(CursorWindow window) {
244        super.setWindow(window);
245        mCount = NO_COUNT;
246    }
247
248    /**
249     * Changes the selection arguments. The new values take effect after a call to requery().
250     */
251    public void setSelectionArguments(String[] selectionArgs) {
252        mDriver.setBindArguments(selectionArgs);
253    }
254
255    /**
256     * Release the native resources, if they haven't been released yet.
257     */
258    @Override
259    protected void finalize() {
260        try {
261            // if the cursor hasn't been closed yet, close it first
262            if (mWindow != null) {
263                if (mStackTrace != null) {
264                    String sql = mQuery.getSql();
265                    int len = sql.length();
266                    StrictMode.onSqliteObjectLeaked(
267                        "Finalizing a Cursor that has not been deactivated or closed. " +
268                        "database = " + mQuery.getDatabase().getLabel() +
269                        ", table = " + mEditTable +
270                        ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
271                        mStackTrace);
272                }
273                close();
274            }
275        } finally {
276            super.finalize();
277        }
278    }
279}
280