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 
17 package android.database.sqlite;
18 
19 import android.database.AbstractWindowedCursor;
20 import android.database.CursorWindow;
21 import android.database.DatabaseUtils;
22 import android.os.StrictMode;
23 import android.util.Log;
24 
25 import java.util.HashMap;
26 import 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  */
35 public 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
SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query)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      */
SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query)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     }
109 
110     /**
111      * Get the database that this cursor is associated with.
112      * @return the SQLiteDatabase that this cursor is associated with.
113      */
getDatabase()114     public SQLiteDatabase getDatabase() {
115         return mQuery.getDatabase();
116     }
117 
118     @Override
onMove(int oldPosition, int newPosition)119     public boolean onMove(int oldPosition, int newPosition) {
120         // Make sure the row at newPosition is present in the window
121         if (mWindow == null || newPosition < mWindow.getStartPosition() ||
122                 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) {
123             fillWindow(newPosition);
124         }
125 
126         return true;
127     }
128 
129     @Override
getCount()130     public int getCount() {
131         if (mCount == NO_COUNT) {
132             fillWindow(0);
133         }
134         return mCount;
135     }
136 
fillWindow(int requiredPos)137     private void fillWindow(int requiredPos) {
138         clearOrCreateWindow(getDatabase().getPath());
139 
140         try {
141             if (mCount == NO_COUNT) {
142                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0);
143                 mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true);
144                 mCursorWindowCapacity = mWindow.getNumRows();
145                 if (Log.isLoggable(TAG, Log.DEBUG)) {
146                     Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
147                 }
148             } else {
149                 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos,
150                         mCursorWindowCapacity);
151                 mQuery.fillWindow(mWindow, startPos, requiredPos, false);
152             }
153         } catch (RuntimeException ex) {
154             // Close the cursor window if the query failed and therefore will
155             // not produce any results.  This helps to avoid accidentally leaking
156             // the cursor window if the client does not correctly handle exceptions
157             // and fails to close the cursor.
158             closeWindow();
159             throw ex;
160         }
161     }
162 
163     @Override
getColumnIndex(String columnName)164     public int getColumnIndex(String columnName) {
165         // Create mColumnNameMap on demand
166         if (mColumnNameMap == null) {
167             String[] columns = mColumns;
168             int columnCount = columns.length;
169             HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
170             for (int i = 0; i < columnCount; i++) {
171                 map.put(columns[i], i);
172             }
173             mColumnNameMap = map;
174         }
175 
176         // Hack according to bug 903852
177         final int periodIndex = columnName.lastIndexOf('.');
178         if (periodIndex != -1) {
179             Exception e = new Exception();
180             Log.e(TAG, "requesting column name with table name -- " + columnName, e);
181             columnName = columnName.substring(periodIndex + 1);
182         }
183 
184         Integer i = mColumnNameMap.get(columnName);
185         if (i != null) {
186             return i.intValue();
187         } else {
188             return -1;
189         }
190     }
191 
192     @Override
getColumnNames()193     public String[] getColumnNames() {
194         return mColumns;
195     }
196 
197     @Override
deactivate()198     public void deactivate() {
199         super.deactivate();
200         mDriver.cursorDeactivated();
201     }
202 
203     @Override
close()204     public void close() {
205         super.close();
206         synchronized (this) {
207             mQuery.close();
208             mDriver.cursorClosed();
209         }
210     }
211 
212     @Override
requery()213     public boolean requery() {
214         if (isClosed()) {
215             return false;
216         }
217 
218         synchronized (this) {
219             if (!mQuery.getDatabase().isOpen()) {
220                 return false;
221             }
222 
223             if (mWindow != null) {
224                 mWindow.clear();
225             }
226             mPos = -1;
227             mCount = NO_COUNT;
228 
229             mDriver.cursorRequeried(this);
230         }
231 
232         try {
233             return super.requery();
234         } catch (IllegalStateException e) {
235             // for backwards compatibility, just return false
236             Log.w(TAG, "requery() failed " + e.getMessage(), e);
237             return false;
238         }
239     }
240 
241     @Override
setWindow(CursorWindow window)242     public void setWindow(CursorWindow window) {
243         super.setWindow(window);
244         mCount = NO_COUNT;
245     }
246 
247     /**
248      * Changes the selection arguments. The new values take effect after a call to requery().
249      */
setSelectionArguments(String[] selectionArgs)250     public void setSelectionArguments(String[] selectionArgs) {
251         mDriver.setBindArguments(selectionArgs);
252     }
253 
254     /**
255      * Release the native resources, if they haven't been released yet.
256      */
257     @Override
finalize()258     protected void finalize() {
259         try {
260             // if the cursor hasn't been closed yet, close it first
261             if (mWindow != null) {
262                 if (mStackTrace != null) {
263                     String sql = mQuery.getSql();
264                     int len = sql.length();
265                     StrictMode.onSqliteObjectLeaked(
266                         "Finalizing a Cursor that has not been deactivated or closed. " +
267                         "database = " + mQuery.getDatabase().getLabel() +
268                         ", table = " + mEditTable +
269                         ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
270                         mStackTrace);
271                 }
272                 close();
273             }
274         } finally {
275             super.finalize();
276         }
277     }
278 }
279