SQLiteCursor.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
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.database.SQLException; 22import android.text.TextUtils; 23import android.util.Config; 24import android.util.Log; 25 26import java.util.HashMap; 27import java.util.Iterator; 28import java.util.Map; 29 30/** 31 * A Cursor implementation that exposes results from a query on a 32 * {@link SQLiteDatabase}. 33 */ 34public class SQLiteCursor extends AbstractWindowedCursor { 35 static final String TAG = "Cursor"; 36 static final int NO_COUNT = -1; 37 38 /** The name of the table to edit */ 39 private String mEditTable; 40 41 /** The names of the columns in the rows */ 42 private String[] mColumns; 43 44 /** The query object for the cursor */ 45 private SQLiteQuery mQuery; 46 47 /** The database the cursor was created from */ 48 private SQLiteDatabase mDatabase; 49 50 /** The compiled query this cursor came from */ 51 private SQLiteCursorDriver mDriver; 52 53 /** The number of rows in the cursor */ 54 private int mCount = NO_COUNT; 55 56 /** A mapping of column names to column indices, to speed up lookups */ 57 private Map<String, Integer> mColumnNameMap; 58 59 /** Used to find out where a cursor was allocated in case it never got 60 * released. */ 61 private StackTraceElement[] mStackTraceElements; 62 63 /** 64 * Execute a query and provide access to its result set through a Cursor 65 * interface. For a query such as: {@code SELECT name, birth, phone FROM 66 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 67 * phone) would be in the projection argument and everything from 68 * {@code FROM} onward would be in the params argument. This constructor 69 * has package scope. 70 * 71 * @param db a reference to a Database object that is already constructed 72 * and opened 73 * @param editTable the name of the table used for this query 74 * @param query the rest of the query terms 75 * cursor is finalized 76 */ 77 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 78 String editTable, SQLiteQuery query) { 79 // The AbstractCursor constructor needs to do some setup. 80 super(); 81 82 if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { 83 mStackTraceElements = new Exception().getStackTrace(); 84 } 85 86 mDatabase = db; 87 mDriver = driver; 88 mEditTable = editTable; 89 mColumnNameMap = null; 90 mQuery = query; 91 92 try { 93 db.lock(); 94 95 // Setup the list of columns 96 int columnCount = mQuery.columnCountLocked(); 97 mColumns = new String[columnCount]; 98 99 // Read in all column names 100 for (int i = 0; i < columnCount; i++) { 101 String columnName = mQuery.columnNameLocked(i); 102 mColumns[i] = columnName; 103 if (Config.LOGV) { 104 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 105 + mColumns[i]); 106 } 107 108 // Make note of the row ID column index for quick access to it 109 if ("_id".equals(columnName)) { 110 mRowIdColumnIndex = i; 111 } 112 } 113 } finally { 114 db.unlock(); 115 } 116 } 117 118 /** 119 * @return the SQLiteDatabase that this cursor is associated with. 120 */ 121 public SQLiteDatabase getDatabase() { 122 return mDatabase; 123 } 124 125 @Override 126 public boolean onMove(int oldPosition, int newPosition) { 127 // Make sure the row at newPosition is present in the window 128 if (mWindow == null || newPosition < mWindow.getStartPosition() || 129 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 130 fillWindow(newPosition); 131 } 132 133 return true; 134 } 135 136 @Override 137 public int getCount() { 138 if (mCount == NO_COUNT) { 139 fillWindow(0); 140 } 141 return mCount; 142 } 143 144 private void fillWindow (int startPos) { 145 if (mWindow == null) { 146 // If there isn't a window set already it will only be accessed locally 147 mWindow = new CursorWindow(true /* the window is local only */); 148 } else { 149 mWindow.clear(); 150 } 151 152 // mWindow must be cleared 153 mCount = mQuery.fillWindow(mWindow, startPos); 154 } 155 156 @Override 157 public int getColumnIndex(String columnName) { 158 // Create mColumnNameMap on demand 159 if (mColumnNameMap == null) { 160 String[] columns = mColumns; 161 int columnCount = columns.length; 162 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 163 for (int i = 0; i < columnCount; i++) { 164 map.put(columns[i], i); 165 } 166 mColumnNameMap = map; 167 } 168 169 // Hack according to bug 903852 170 final int periodIndex = columnName.lastIndexOf('.'); 171 if (periodIndex != -1) { 172 Exception e = new Exception(); 173 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 174 columnName = columnName.substring(periodIndex + 1); 175 } 176 177 Integer i = mColumnNameMap.get(columnName); 178 if (i != null) { 179 return i.intValue(); 180 } else { 181 return -1; 182 } 183 } 184 185 /** 186 * @hide 187 * @deprecated 188 */ 189 @Override 190 public boolean deleteRow() { 191 checkPosition(); 192 193 // Only allow deletes if there is an ID column, and the ID has been read from it 194 if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { 195 Log.e(TAG, 196 "Could not delete row because either the row ID column is not available or it" + 197 "has not been read."); 198 return false; 199 } 200 201 boolean success; 202 203 /* 204 * Ensure we don't change the state of the database when another 205 * thread is holding the database lock. requery() and moveTo() are also 206 * synchronized here to make sure they get the state of the database 207 * immediately following the DELETE. 208 */ 209 mDatabase.lock(); 210 try { 211 try { 212 mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", 213 new String[] {mCurrentRowID.toString()}); 214 success = true; 215 } catch (SQLException e) { 216 success = false; 217 } 218 219 int pos = mPos; 220 requery(); 221 222 /* 223 * Ensure proper cursor state. Note that mCurrentRowID changes 224 * in this call. 225 */ 226 moveToPosition(pos); 227 } finally { 228 mDatabase.unlock(); 229 } 230 231 if (success) { 232 onChange(true); 233 return true; 234 } else { 235 return false; 236 } 237 } 238 239 @Override 240 public String[] getColumnNames() { 241 return mColumns; 242 } 243 244 /** 245 * @hide 246 * @deprecated 247 */ 248 @Override 249 public boolean supportsUpdates() { 250 return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); 251 } 252 253 /** 254 * @hide 255 * @deprecated 256 */ 257 @Override 258 public boolean commitUpdates(Map<? extends Long, 259 ? extends Map<String, Object>> additionalValues) { 260 if (!supportsUpdates()) { 261 Log.e(TAG, "commitUpdates not supported on this cursor, did you " 262 + "include the _id column?"); 263 return false; 264 } 265 266 /* 267 * Prevent other threads from changing the updated rows while they're 268 * being processed here. 269 */ 270 synchronized (mUpdatedRows) { 271 if (additionalValues != null) { 272 mUpdatedRows.putAll(additionalValues); 273 } 274 275 if (mUpdatedRows.size() == 0) { 276 return true; 277 } 278 279 /* 280 * Prevent other threads from changing the database state while 281 * we process the updated rows, and prevents us from changing the 282 * database behind the back of another thread. 283 */ 284 mDatabase.beginTransaction(); 285 try { 286 StringBuilder sql = new StringBuilder(128); 287 288 // For each row that has been updated 289 for (Map.Entry<Long, Map<String, Object>> rowEntry : 290 mUpdatedRows.entrySet()) { 291 Map<String, Object> values = rowEntry.getValue(); 292 Long rowIdObj = rowEntry.getKey(); 293 294 if (rowIdObj == null || values == null) { 295 throw new IllegalStateException("null rowId or values found! rowId = " 296 + rowIdObj + ", values = " + values); 297 } 298 299 if (values.size() == 0) { 300 continue; 301 } 302 303 long rowId = rowIdObj.longValue(); 304 305 Iterator<Map.Entry<String, Object>> valuesIter = 306 values.entrySet().iterator(); 307 308 sql.setLength(0); 309 sql.append("UPDATE " + mEditTable + " SET "); 310 311 // For each column value that has been updated 312 Object[] bindings = new Object[values.size()]; 313 int i = 0; 314 while (valuesIter.hasNext()) { 315 Map.Entry<String, Object> entry = valuesIter.next(); 316 sql.append(entry.getKey()); 317 sql.append("=?"); 318 bindings[i] = entry.getValue(); 319 if (valuesIter.hasNext()) { 320 sql.append(", "); 321 } 322 i++; 323 } 324 325 sql.append(" WHERE " + mColumns[mRowIdColumnIndex] 326 + '=' + rowId); 327 sql.append(';'); 328 mDatabase.execSQL(sql.toString(), bindings); 329 mDatabase.rowUpdated(mEditTable, rowId); 330 } 331 mDatabase.setTransactionSuccessful(); 332 } finally { 333 mDatabase.endTransaction(); 334 } 335 336 mUpdatedRows.clear(); 337 } 338 339 // Let any change observers know about the update 340 onChange(true); 341 342 return true; 343 } 344 345 private void deactivateCommon() { 346 if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); 347 if (mWindow != null) { 348 mWindow.close(); 349 mWindow = null; 350 } 351 if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); 352 } 353 354 @Override 355 public void deactivate() { 356 super.deactivate(); 357 deactivateCommon(); 358 mDriver.cursorDeactivated(); 359 } 360 361 @Override 362 public void close() { 363 super.close(); 364 deactivateCommon(); 365 mQuery.close(); 366 mDriver.cursorClosed(); 367 } 368 369 @Override 370 public boolean requery() { 371 long timeStart = 0; 372 if (Config.LOGV) { 373 timeStart = System.currentTimeMillis(); 374 } 375 /* 376 * Synchronize on the database lock to ensure that mCount matches the 377 * results of mQuery.requery(). 378 */ 379 mDatabase.lock(); 380 try { 381 if (mWindow != null) { 382 mWindow.clear(); 383 } 384 mPos = -1; 385 // This one will recreate the temp table, and get its count 386 mDriver.cursorRequeried(this); 387 mCount = NO_COUNT; 388 // Requery the program that runs over the temp table 389 mQuery.requery(); 390 } finally { 391 mDatabase.unlock(); 392 } 393 394 if (Config.LOGV) { 395 Log.v("DatabaseWindow", "closing window in requery()"); 396 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 397 } 398 399 boolean result = super.requery(); 400 if (Config.LOGV) { 401 long timeEnd = System.currentTimeMillis(); 402 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 403 } 404 return result; 405 } 406 407 @Override 408 public void setWindow(CursorWindow window) { 409 if (mWindow != null) { 410 mWindow.close(); 411 mCount = NO_COUNT; 412 } 413 mWindow = window; 414 } 415 416 /** 417 * Changes the selection arguments. The new values take effect after a call to requery(). 418 */ 419 public void setSelectionArguments(String[] selectionArgs) { 420 mDriver.setBindArguments(selectionArgs); 421 } 422 423 /** 424 * Release the native resources, if they haven't been released yet. 425 */ 426 @Override 427 protected void finalize() { 428 try { 429 if (mWindow != null) { 430 close(); 431 String message = "Finalizing cursor " + this + " on " + mEditTable 432 + " that has not been deactivated or closed"; 433 if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { 434 Log.d(TAG, message + "\nThis cursor was created in:"); 435 for (StackTraceElement ste : mStackTraceElements) { 436 Log.d(TAG, " " + ste); 437 } 438 } 439 SQLiteDebug.notifyActiveCursorFinalized(); 440 throw new IllegalStateException(message); 441 } else { 442 if (Config.LOGV) { 443 Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable); 444 } 445 } 446 } finally { 447 super.finalize(); 448 } 449 } 450} 451