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