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 com.android.internal.util.Preconditions;
26 
27 import java.util.HashMap;
28 import 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  */
37 public 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
SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query)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      */
SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query)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      */
getDatabase()119     public SQLiteDatabase getDatabase() {
120         return mQuery.getDatabase();
121     }
122 
123     @Override
onMove(int oldPosition, int newPosition)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
getCount()135     public int getCount() {
136         if (mCount == NO_COUNT) {
137             fillWindow(0);
138         }
139         return mCount;
140     }
141 
fillWindow(int requiredPos)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
getColumnIndex(String columnName)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
getColumnNames()199     public String[] getColumnNames() {
200         return mColumns;
201     }
202 
203     @Override
deactivate()204     public void deactivate() {
205         super.deactivate();
206         mDriver.cursorDeactivated();
207     }
208 
209     @Override
close()210     public void close() {
211         super.close();
212         synchronized (this) {
213             mQuery.close();
214             mDriver.cursorClosed();
215         }
216     }
217 
218     @Override
requery()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
setWindow(CursorWindow window)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      */
setSelectionArguments(String[] selectionArgs)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      */
setFillWindowForwardOnly(boolean fillWindowForwardOnly)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
finalize()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