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