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     /** 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         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 
137     @UnsupportedAppUsage
fillWindow(int requiredPos)138     private void fillWindow(int requiredPos) {
139         clearOrCreateWindow(getDatabase().getPath());
140         try {
141             Preconditions.checkArgumentNonnegative(requiredPos,
142                     "requiredPos cannot be negative");
143 
144             if (mCount == NO_COUNT) {
145                 mCount = mQuery.fillWindow(mWindow, requiredPos, requiredPos, true);
146                 mCursorWindowCapacity = mWindow.getNumRows();
147                 if (SQLiteDebug.NoPreloadHolder.DEBUG_SQL_LOG) {
148                     Log.d(TAG, "received count(*) from native_fill_window: " + mCount);
149                 }
150             } else {
151                 int startPos = mFillWindowForwardOnly ? requiredPos : DatabaseUtils
152                         .cursorPickFillWindowStartPosition(requiredPos, mCursorWindowCapacity);
153                 mQuery.fillWindow(mWindow, startPos, requiredPos, false);
154             }
155         } catch (RuntimeException ex) {
156             // Close the cursor window if the query failed and therefore will
157             // not produce any results.  This helps to avoid accidentally leaking
158             // the cursor window if the client does not correctly handle exceptions
159             // and fails to close the cursor.
160             closeWindow();
161             throw ex;
162         }
163     }
164 
165     @Override
getColumnIndex(String columnName)166     public int getColumnIndex(String columnName) {
167         // Create mColumnNameMap on demand
168         if (mColumnNameMap == null) {
169             String[] columns = mColumns;
170             int columnCount = columns.length;
171             HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1);
172             for (int i = 0; i < columnCount; i++) {
173                 map.put(columns[i], i);
174             }
175             mColumnNameMap = map;
176         }
177 
178         // Hack according to bug 903852
179         final int periodIndex = columnName.lastIndexOf('.');
180         if (periodIndex != -1) {
181             Exception e = new Exception();
182             Log.e(TAG, "requesting column name with table name -- " + columnName, e);
183             columnName = columnName.substring(periodIndex + 1);
184         }
185 
186         Integer i = mColumnNameMap.get(columnName);
187         if (i != null) {
188             return i.intValue();
189         } else {
190             return -1;
191         }
192     }
193 
194     @Override
getColumnNames()195     public String[] getColumnNames() {
196         return mColumns;
197     }
198 
199     @Override
deactivate()200     public void deactivate() {
201         super.deactivate();
202         mDriver.cursorDeactivated();
203     }
204 
205     @Override
close()206     public void close() {
207         super.close();
208         synchronized (this) {
209             mQuery.close();
210             mDriver.cursorClosed();
211         }
212     }
213 
214     @Override
requery()215     public boolean requery() {
216         if (isClosed()) {
217             return false;
218         }
219 
220         synchronized (this) {
221             if (!mQuery.getDatabase().isOpen()) {
222                 return false;
223             }
224 
225             if (mWindow != null) {
226                 mWindow.clear();
227             }
228             mPos = -1;
229             mCount = NO_COUNT;
230 
231             mDriver.cursorRequeried(this);
232         }
233 
234         try {
235             return super.requery();
236         } catch (IllegalStateException e) {
237             // for backwards compatibility, just return false
238             Log.w(TAG, "requery() failed " + e.getMessage(), e);
239             return false;
240         }
241     }
242 
243     @Override
setWindow(CursorWindow window)244     public void setWindow(CursorWindow window) {
245         super.setWindow(window);
246         mCount = NO_COUNT;
247     }
248 
249     /**
250      * Changes the selection arguments. The new values take effect after a call to requery().
251      */
setSelectionArguments(String[] selectionArgs)252     public void setSelectionArguments(String[] selectionArgs) {
253         mDriver.setBindArguments(selectionArgs);
254     }
255 
256     /**
257      * Controls fetching of rows relative to requested position.
258      *
259      * <p>Calling this method defines how rows will be loaded, but it doesn't affect rows that
260      * are already in the window. This setting is preserved if a new window is
261      * {@link #setWindow(CursorWindow) set}
262      *
263      * @param fillWindowForwardOnly if true, rows will be fetched starting from requested position
264      * up to the window's capacity. Default value is false.
265      */
setFillWindowForwardOnly(boolean fillWindowForwardOnly)266     public void setFillWindowForwardOnly(boolean fillWindowForwardOnly) {
267         mFillWindowForwardOnly = fillWindowForwardOnly;
268     }
269 
270     /**
271      * Release the native resources, if they haven't been released yet.
272      */
273     @Override
finalize()274     protected void finalize() {
275         try {
276             // if the cursor hasn't been closed yet, close it first
277             if (mWindow != null) {
278                 // Report original sql statement
279                 if (StrictMode.vmSqliteObjectLeaksEnabled()) {
280                     String sql = mQuery.getSql();
281                     int len = sql.length();
282                     StrictMode.onSqliteObjectLeaked(
283                             "Finalizing a Cursor that has not been deactivated or closed. "
284                             + "database = " + mQuery.getDatabase().getLabel()
285                             + ", table = " + mEditTable
286                             + ", query = " + sql.substring(0, (len > 1000) ? 1000 : len),
287                             null);
288                 }
289             }
290         } finally {
291             super.finalize();
292         }
293     }
294 }
295