SQLiteCursor.java revision 7ce745248d4de0e6543a559c93423df899832100
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 17package android.database.sqlite; 18 19import android.database.AbstractWindowedCursor; 20import android.database.CursorWindow; 21import android.os.StrictMode; 22import android.util.Log; 23 24import java.util.HashMap; 25import java.util.Map; 26 27/** 28 * A Cursor implementation that exposes results from a query on a 29 * {@link SQLiteDatabase}. 30 * 31 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple 32 * threads should perform its own synchronization when using the SQLiteCursor. 33 */ 34public class SQLiteCursor extends AbstractWindowedCursor { 35 static final String TAG = "SQLiteCursor"; 36 static final int NO_COUNT = -1; 37 38 /** The name of the table to edit */ 39 private final String mEditTable; 40 41 /** The names of the columns in the rows */ 42 private final String[] mColumns; 43 44 /** The query object for the cursor */ 45 private SQLiteQuery mQuery; 46 47 /** The compiled query this cursor came from */ 48 private final SQLiteCursorDriver mDriver; 49 50 /** The number of rows in the cursor */ 51 private volatile int mCount = NO_COUNT; 52 53 /** A mapping of column names to column indices, to speed up lookups */ 54 private Map<String, Integer> mColumnNameMap; 55 56 /** Used to find out where a cursor was allocated in case it never got released. */ 57 private final Throwable mStackTrace; 58 59 /** 60 * Execute a query and provide access to its result set through a Cursor 61 * interface. For a query such as: {@code SELECT name, birth, phone FROM 62 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 63 * phone) would be in the projection argument and everything from 64 * {@code FROM} onward would be in the params argument. This constructor 65 * has package scope. 66 * 67 * @param db a reference to a Database object that is already constructed 68 * and opened. This param is not used any longer 69 * @param editTable the name of the table used for this query 70 * @param query the rest of the query terms 71 * cursor is finalized 72 * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead 73 */ 74 @Deprecated 75 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 76 String editTable, SQLiteQuery query) { 77 this(driver, editTable, query); 78 } 79 80 /** 81 * Execute a query and provide access to its result set through a Cursor 82 * interface. For a query such as: {@code SELECT name, birth, phone FROM 83 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 84 * phone) would be in the projection argument and everything from 85 * {@code FROM} onward would be in the params argument. This constructor 86 * has package scope. 87 * 88 * @param editTable the name of the table used for this query 89 * @param query the {@link SQLiteQuery} object associated with this cursor object. 90 */ 91 public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { 92 // The AbstractCursor constructor needs to do some setup. 93 super(); 94 if (query == null) { 95 throw new IllegalArgumentException("query object cannot be null"); 96 } 97 if (query.mDatabase == null) { 98 throw new IllegalArgumentException("query.mDatabase cannot be null"); 99 } 100 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 101 mDriver = driver; 102 mEditTable = editTable; 103 mColumnNameMap = null; 104 mQuery = query; 105 106 query.mDatabase.lock(query.mSql); 107 try { 108 // Setup the list of columns 109 int columnCount = mQuery.columnCountLocked(); 110 mColumns = new String[columnCount]; 111 112 // Read in all column names 113 for (int i = 0; i < columnCount; i++) { 114 String columnName = mQuery.columnNameLocked(i); 115 mColumns[i] = columnName; 116 if (false) { 117 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 118 + mColumns[i]); 119 } 120 121 // Make note of the row ID column index for quick access to it 122 if ("_id".equals(columnName)) { 123 mRowIdColumnIndex = i; 124 } 125 } 126 } finally { 127 query.mDatabase.unlock(); 128 } 129 } 130 131 /** 132 * @return the SQLiteDatabase that this cursor is associated with. 133 */ 134 public SQLiteDatabase getDatabase() { 135 synchronized (this) { 136 return mQuery.mDatabase; 137 } 138 } 139 140 @Override 141 public boolean onMove(int oldPosition, int newPosition) { 142 // Make sure the row at newPosition is present in the window 143 if (mWindow == null || newPosition < mWindow.getStartPosition() || 144 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 145 fillWindow(newPosition); 146 } 147 148 return true; 149 } 150 151 @Override 152 public int getCount() { 153 if (mCount == NO_COUNT) { 154 fillWindow(0); 155 } 156 return mCount; 157 } 158 159 private void fillWindow(int startPos) { 160 if (mWindow == null) { 161 // If there isn't a window set already it will only be accessed locally 162 mWindow = new CursorWindow(true /* the window is local only */); 163 } else { 164 mWindow.clear(); 165 } 166 mWindow.setStartPosition(startPos); 167 int count = getQuery().fillWindow(mWindow); 168 if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 169 if (Log.isLoggable(TAG, Log.DEBUG)) { 170 Log.d(TAG, "received count(*) from native_fill_window: " + count); 171 } 172 mCount = count; 173 } else if (mCount <= 0) { 174 throw new IllegalStateException("Row count should never be zero or negative " 175 + "when the start position is non-zero"); 176 } 177 } 178 179 private synchronized SQLiteQuery getQuery() { 180 return mQuery; 181 } 182 183 @Override 184 public int getColumnIndex(String columnName) { 185 // Create mColumnNameMap on demand 186 if (mColumnNameMap == null) { 187 String[] columns = mColumns; 188 int columnCount = columns.length; 189 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 190 for (int i = 0; i < columnCount; i++) { 191 map.put(columns[i], i); 192 } 193 mColumnNameMap = map; 194 } 195 196 // Hack according to bug 903852 197 final int periodIndex = columnName.lastIndexOf('.'); 198 if (periodIndex != -1) { 199 Exception e = new Exception(); 200 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 201 columnName = columnName.substring(periodIndex + 1); 202 } 203 204 Integer i = mColumnNameMap.get(columnName); 205 if (i != null) { 206 return i.intValue(); 207 } else { 208 return -1; 209 } 210 } 211 212 @Override 213 public String[] getColumnNames() { 214 return mColumns; 215 } 216 217 private void deactivateCommon() { 218 if (false) Log.v(TAG, "<<< Releasing cursor " + this); 219 closeWindow(); 220 if (false) Log.v("DatabaseWindow", "closing window in release()"); 221 } 222 223 @Override 224 public void deactivate() { 225 super.deactivate(); 226 deactivateCommon(); 227 mDriver.cursorDeactivated(); 228 } 229 230 @Override 231 public void close() { 232 super.close(); 233 synchronized (this) { 234 deactivateCommon(); 235 mQuery.close(); 236 mDriver.cursorClosed(); 237 } 238 } 239 240 @Override 241 public boolean requery() { 242 if (isClosed()) { 243 return false; 244 } 245 long timeStart = 0; 246 if (false) { 247 timeStart = System.currentTimeMillis(); 248 } 249 250 synchronized (this) { 251 if (mWindow != null) { 252 mWindow.clear(); 253 } 254 mPos = -1; 255 SQLiteDatabase db = null; 256 try { 257 db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); 258 } catch (IllegalStateException e) { 259 // for backwards compatibility, just return false 260 Log.w(TAG, "requery() failed " + e.getMessage(), e); 261 return false; 262 } 263 if (!db.equals(mQuery.mDatabase)) { 264 // since we need to use a different database connection handle, 265 // re-compile the query 266 try { 267 db.lock(mQuery.mSql); 268 } catch (IllegalStateException e) { 269 // for backwards compatibility, just return false 270 Log.w(TAG, "requery() failed " + e.getMessage(), e); 271 return false; 272 } 273 try { 274 // close the old mQuery object and open a new one 275 mQuery.close(); 276 mQuery = new SQLiteQuery(db, mQuery); 277 } catch (IllegalStateException e) { 278 // for backwards compatibility, just return false 279 Log.w(TAG, "requery() failed " + e.getMessage(), e); 280 return false; 281 } finally { 282 db.unlock(); 283 } 284 } 285 // This one will recreate the temp table, and get its count 286 mDriver.cursorRequeried(this); 287 mCount = NO_COUNT; 288 try { 289 mQuery.requery(); 290 } catch (IllegalStateException e) { 291 // for backwards compatibility, just return false 292 Log.w(TAG, "requery() failed " + e.getMessage(), e); 293 return false; 294 } 295 } 296 297 if (false) { 298 Log.v("DatabaseWindow", "closing window in requery()"); 299 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 300 } 301 302 boolean result = false; 303 try { 304 result = super.requery(); 305 } catch (IllegalStateException e) { 306 // for backwards compatibility, just return false 307 Log.w(TAG, "requery() failed " + e.getMessage(), e); 308 } 309 if (false) { 310 long timeEnd = System.currentTimeMillis(); 311 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 312 } 313 return result; 314 } 315 316 @Override 317 public void setWindow(CursorWindow window) { 318 super.setWindow(window); 319 mCount = NO_COUNT; 320 } 321 322 /** 323 * Changes the selection arguments. The new values take effect after a call to requery(). 324 */ 325 public void setSelectionArguments(String[] selectionArgs) { 326 mDriver.setBindArguments(selectionArgs); 327 } 328 329 /** 330 * Release the native resources, if they haven't been released yet. 331 */ 332 @Override 333 protected void finalize() { 334 try { 335 // if the cursor hasn't been closed yet, close it first 336 if (mWindow != null) { 337 if (StrictMode.vmSqliteObjectLeaksEnabled()) { 338 int len = mQuery.mSql.length(); 339 StrictMode.onSqliteObjectLeaked( 340 "Finalizing a Cursor that has not been deactivated or closed. " + 341 "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + 342 ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len), 343 mStackTrace); 344 } 345 close(); 346 SQLiteDebug.notifyActiveCursorFinalized(); 347 } else { 348 if (false) { 349 Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + 350 ", table = " + mEditTable + ", query = " + mQuery.mSql); 351 } 352 } 353 } finally { 354 super.finalize(); 355 } 356 } 357} 358