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 java.util.HashMap; 26 import java.util.Map; 27 28 /** 29 * A Cursor implementation that exposes results from a query on a 30 * {@link SQLiteDatabase}. 31 * 32 * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple 33 * threads should perform its own synchronization when using the SQLiteCursor. 34 */ 35 public class SQLiteCursor extends AbstractWindowedCursor { 36 static final String TAG = "SQLiteCursor"; 37 static final int NO_COUNT = -1; 38 39 /** The name of the table to edit */ 40 private final String mEditTable; 41 42 /** The names of the columns in the rows */ 43 private final String[] mColumns; 44 45 /** The query object for the cursor */ 46 private final SQLiteQuery mQuery; 47 48 /** The compiled query this cursor came from */ 49 private final SQLiteCursorDriver mDriver; 50 51 /** The number of rows in the cursor */ 52 private int mCount = NO_COUNT; 53 54 /** The number of rows that can fit in the cursor window, 0 if unknown */ 55 private int mCursorWindowCapacity; 56 57 /** A mapping of column names to column indices, to speed up lookups */ 58 private Map<String, Integer> mColumnNameMap; 59 60 /** Used to find out where a cursor was allocated in case it never got released. */ 61 private final Throwable mStackTrace; 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. 69 * 70 * @param db a reference to a Database object that is already constructed 71 * and opened. This param is not used any longer 72 * @param editTable the name of the table used for this query 73 * @param query the rest of the query terms 74 * cursor is finalized 75 * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead 76 */ 77 @Deprecated 78 public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, 79 String editTable, SQLiteQuery query) { 80 this(driver, editTable, query); 81 } 82 83 /** 84 * Execute a query and provide access to its result set through a Cursor 85 * interface. For a query such as: {@code SELECT name, birth, phone FROM 86 * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, 87 * phone) would be in the projection argument and everything from 88 * {@code FROM} onward would be in the params argument. 89 * 90 * @param editTable the name of the table used for this query 91 * @param query the {@link SQLiteQuery} object associated with this cursor object. 92 */ 93 public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { 94 if (query == null) { 95 throw new IllegalArgumentException("query object cannot be null"); 96 } 97 if (StrictMode.vmSqliteObjectLeaksEnabled()) { 98 mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); 99 } else { 100 mStackTrace = null; 101 } 102 mDriver = driver; 103 mEditTable = editTable; 104 mColumnNameMap = null; 105 mQuery = query; 106 107 mColumns = query.getColumnNames(); 108 mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns); 109 } 110 111 /** 112 * Get the database that this cursor is associated with. 113 * @return the SQLiteDatabase that this cursor is associated with. 114 */ 115 public SQLiteDatabase getDatabase() { 116 return mQuery.getDatabase(); 117 } 118 119 @Override 120 public boolean onMove(int oldPosition, int newPosition) { 121 // Make sure the row at newPosition is present in the window 122 if (mWindow == null || newPosition < mWindow.getStartPosition() || 123 newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { 124 fillWindow(newPosition); 125 } 126 127 return true; 128 } 129 130 @Override 131 public int getCount() { 132 if (mCount == NO_COUNT) { 133 fillWindow(0); 134 } 135 return mCount; 136 } 137 138 private void fillWindow(int requiredPos) { 139 clearOrCreateWindow(getDatabase().getPath()); 140 141 try { 142 if (mCount == NO_COUNT) { 143 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); 144 mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); 145 mCursorWindowCapacity = mWindow.getNumRows(); 146 if (Log.isLoggable(TAG, Log.DEBUG)) { 147 Log.d(TAG, "received count(*) from native_fill_window: " + mCount); 148 } 149 } else { 150 int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 151 mCursorWindowCapacity); 152 mQuery.fillWindow(mWindow, startPos, requiredPos, false); 153 } 154 } catch (RuntimeException ex) { 155 // Close the cursor window if the query failed and therefore will 156 // not produce any results. This helps to avoid accidentally leaking 157 // the cursor window if the client does not correctly handle exceptions 158 // and fails to close the cursor. 159 closeWindow(); 160 throw ex; 161 } 162 } 163 164 @Override 165 public int getColumnIndex(String columnName) { 166 // Create mColumnNameMap on demand 167 if (mColumnNameMap == null) { 168 String[] columns = mColumns; 169 int columnCount = columns.length; 170 HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); 171 for (int i = 0; i < columnCount; i++) { 172 map.put(columns[i], i); 173 } 174 mColumnNameMap = map; 175 } 176 177 // Hack according to bug 903852 178 final int periodIndex = columnName.lastIndexOf('.'); 179 if (periodIndex != -1) { 180 Exception e = new Exception(); 181 Log.e(TAG, "requesting column name with table name -- " + columnName, e); 182 columnName = columnName.substring(periodIndex + 1); 183 } 184 185 Integer i = mColumnNameMap.get(columnName); 186 if (i != null) { 187 return i.intValue(); 188 } else { 189 return -1; 190 } 191 } 192 193 @Override 194 public String[] getColumnNames() { 195 return mColumns; 196 } 197 198 @Override 199 public void deactivate() { 200 super.deactivate(); 201 mDriver.cursorDeactivated(); 202 } 203 204 @Override 205 public void close() { 206 super.close(); 207 synchronized (this) { 208 mQuery.close(); 209 mDriver.cursorClosed(); 210 } 211 } 212 213 @Override 214 public boolean requery() { 215 if (isClosed()) { 216 return false; 217 } 218 219 synchronized (this) { 220 if (!mQuery.getDatabase().isOpen()) { 221 return false; 222 } 223 224 if (mWindow != null) { 225 mWindow.clear(); 226 } 227 mPos = -1; 228 mCount = NO_COUNT; 229 230 mDriver.cursorRequeried(this); 231 } 232 233 try { 234 return super.requery(); 235 } catch (IllegalStateException e) { 236 // for backwards compatibility, just return false 237 Log.w(TAG, "requery() failed " + e.getMessage(), e); 238 return false; 239 } 240 } 241 242 @Override 243 public void setWindow(CursorWindow window) { 244 super.setWindow(window); 245 mCount = NO_COUNT; 246 } 247 248 /** 249 * Changes the selection arguments. The new values take effect after a call to requery(). 250 */ 251 public void setSelectionArguments(String[] selectionArgs) { 252 mDriver.setBindArguments(selectionArgs); 253 } 254 255 /** 256 * Release the native resources, if they haven't been released yet. 257 */ 258 @Override 259 protected void finalize() { 260 try { 261 // if the cursor hasn't been closed yet, close it first 262 if (mWindow != null) { 263 if (mStackTrace != null) { 264 String sql = mQuery.getSql(); 265 int len = sql.length(); 266 StrictMode.onSqliteObjectLeaked( 267 "Finalizing a Cursor that has not been deactivated or closed. " + 268 "database = " + mQuery.getDatabase().getLabel() + 269 ", table = " + mEditTable + 270 ", query = " + sql.substring(0, (len > 1000) ? 1000 : len), 271 mStackTrace); 272 } 273 close(); 274 } 275 } finally { 276 super.finalize(); 277 } 278 } 279 } 280