SQLiteCursor.java revision 7978a414bbbc737bfb342db8840c29376e33a34d
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 if (query == null) { 93 throw new IllegalArgumentException("query object cannot be null"); 94 } 95 if (query.mDatabase == null) { 96 throw new IllegalArgumentException("query.mDatabase cannot be null"); 97 } 98 if (StrictMode.vmSqliteObjectLeaksEnabled()) { 99 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 100 } else { 101 mStackTrace = null; 102 } 103 mDriver = driver; 104 mEditTable = editTable; 105 mColumnNameMap = null; 106 mQuery = query; 107 108 query.mDatabase.lock(query.mSql); 109 try { 110 // Setup the list of columns 111 int columnCount = mQuery.columnCountLocked(); 112 mColumns = new String[columnCount]; 113 114 // Read in all column names 115 for (int i = 0; i < columnCount; i++) { 116 String columnName = mQuery.columnNameLocked(i); 117 mColumns[i] = columnName; 118 if (false) { 119 Log.v("DatabaseWindow", "mColumns[" + i + "] is " 120 + mColumns[i]); 121 } 122 123 // Make note of the row ID column index for quick access to it 124 if ("_id".equals(columnName)) { 125 mRowIdColumnIndex = i; 126 } 127 } 128 } finally { 129 query.mDatabase.unlock(); 130 } 131 } 132 133 /** 134 * @return the SQLiteDatabase that this cursor is associated with. 135 */ 136 public SQLiteDatabase getDatabase() { 137 synchronized (this) { 138 return mQuery.mDatabase; 139 } 140 } 141 142 @Override 143 public boolean onMove(int oldPosition, int newPosition) { 144 // Make sure the row at newPosition is present in the window 145 if (mWindow == null || newPosition < mWindow.getStartPosition() || 146 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 147 fillWindow(newPosition); 148 } 149 150 return true; 151 } 152 153 @Override 154 public int getCount() { 155 if (mCount == NO_COUNT) { 156 fillWindow(0); 157 } 158 return mCount; 159 } 160 161 private void fillWindow(int startPos) { 162 clearOrCreateWindow(getDatabase().getPath()); 163 mWindow.setStartPosition(startPos); 164 int count = getQuery().fillWindow(mWindow); 165 if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 166 if (Log.isLoggable(TAG, Log.DEBUG)) { 167 Log.d(TAG, "received count(*) from native_fill_window: " + count); 168 } 169 mCount = count; 170 } else if (mCount <= 0) { 171 throw new IllegalStateException("Row count should never be zero or negative " 172 + "when the start position is non-zero"); 173 } 174 } 175 176 private synchronized SQLiteQuery getQuery() { 177 return mQuery; 178 } 179 180 @Override 181 public int getColumnIndex(String columnName) { 182 // Create mColumnNameMap on demand 183 if (mColumnNameMap == null) { 184 String[] columns = mColumns; 185 int columnCount = columns.length; 186 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 187 for (int i = 0; i < columnCount; i++) { 188 map.put(columns[i], i); 189 } 190 mColumnNameMap = map; 191 } 192 193 // Hack according to bug 903852 194 final int periodIndex = columnName.lastIndexOf('.'); 195 if (periodIndex != -1) { 196 Exception e = new Exception(); 197 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 198 columnName = columnName.substring(periodIndex + 1); 199 } 200 201 Integer i = mColumnNameMap.get(columnName); 202 if (i != null) { 203 return i.intValue(); 204 } else { 205 return -1; 206 } 207 } 208 209 @Override 210 public String[] getColumnNames() { 211 return mColumns; 212 } 213 214 @Override 215 public void deactivate() { 216 super.deactivate(); 217 mDriver.cursorDeactivated(); 218 } 219 220 @Override 221 public void close() { 222 super.close(); 223 synchronized (this) { 224 mQuery.close(); 225 mDriver.cursorClosed(); 226 } 227 } 228 229 @Override 230 public boolean requery() { 231 if (isClosed()) { 232 return false; 233 } 234 long timeStart = 0; 235 if (false) { 236 timeStart = System.currentTimeMillis(); 237 } 238 239 synchronized (this) { 240 if (mWindow != null) { 241 mWindow.clear(); 242 } 243 mPos = -1; 244 SQLiteDatabase db = null; 245 try { 246 db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); 247 } catch (IllegalStateException e) { 248 // for backwards compatibility, just return false 249 Log.w(TAG, "requery() failed " + e.getMessage(), e); 250 return false; 251 } 252 if (!db.equals(mQuery.mDatabase)) { 253 // since we need to use a different database connection handle, 254 // re-compile the query 255 try { 256 db.lock(mQuery.mSql); 257 } catch (IllegalStateException e) { 258 // for backwards compatibility, just return false 259 Log.w(TAG, "requery() failed " + e.getMessage(), e); 260 return false; 261 } 262 try { 263 // close the old mQuery object and open a new one 264 mQuery.close(); 265 mQuery = new SQLiteQuery(db, mQuery); 266 } catch (IllegalStateException e) { 267 // for backwards compatibility, just return false 268 Log.w(TAG, "requery() failed " + e.getMessage(), e); 269 return false; 270 } finally { 271 db.unlock(); 272 } 273 } 274 // This one will recreate the temp table, and get its count 275 mDriver.cursorRequeried(this); 276 mCount = NO_COUNT; 277 try { 278 mQuery.requery(); 279 } catch (IllegalStateException e) { 280 // for backwards compatibility, just return false 281 Log.w(TAG, "requery() failed " + e.getMessage(), e); 282 return false; 283 } 284 } 285 286 if (false) { 287 Log.v("DatabaseWindow", "closing window in requery()"); 288 Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); 289 } 290 291 boolean result = false; 292 try { 293 result = super.requery(); 294 } catch (IllegalStateException e) { 295 // for backwards compatibility, just return false 296 Log.w(TAG, "requery() failed " + e.getMessage(), e); 297 } 298 if (false) { 299 long timeEnd = System.currentTimeMillis(); 300 Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); 301 } 302 return result; 303 } 304 305 @Override 306 public void setWindow(CursorWindow window) { 307 super.setWindow(window); 308 mCount = NO_COUNT; 309 } 310 311 /** 312 * Changes the selection arguments. The new values take effect after a call to requery(). 313 */ 314 public void setSelectionArguments(String[] selectionArgs) { 315 mDriver.setBindArguments(selectionArgs); 316 } 317 318 /** 319 * Release the native resources, if they haven't been released yet. 320 */ 321 @Override 322 protected void finalize() { 323 try { 324 // if the cursor hasn't been closed yet, close it first 325 if (mWindow != null) { 326 if (mStackTrace != null) { 327 int len = mQuery.mSql.length(); 328 StrictMode.onSqliteObjectLeaked( 329 "Finalizing a Cursor that has not been deactivated or closed. " + 330 "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + 331 ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len), 332 mStackTrace); 333 } 334 close(); 335 SQLiteDebug.notifyActiveCursorFinalized(); 336 } else { 337 if (false) { 338 Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + 339 ", table = " + mEditTable + ", query = " + mQuery.mSql); 340 } 341 } 342 } finally { 343 super.finalize(); 344 } 345 } 346} 347