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