/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; import android.os.StrictMode; import android.util.Log; import java.util.HashMap; import java.util.Map; /** * A Cursor implementation that exposes results from a query on a * {@link SQLiteDatabase}. * * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple * threads should perform its own synchronization when using the SQLiteCursor. */ public class SQLiteCursor extends AbstractWindowedCursor { static final String TAG = "SQLiteCursor"; static final int NO_COUNT = -1; /** The name of the table to edit */ private final String mEditTable; /** The names of the columns in the rows */ private final String[] mColumns; /** The query object for the cursor */ private SQLiteQuery mQuery; /** The compiled query this cursor came from */ private final SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ private volatile int mCount = NO_COUNT; /** A mapping of column names to column indices, to speed up lookups */ private Map<String, Integer> mColumnNameMap; /** Used to find out where a cursor was allocated in case it never got released. */ private final Throwable mStackTrace; /** * Execute a query and provide access to its result set through a Cursor * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from * {@code FROM} onward would be in the params argument. This constructor * has package scope. * * @param db a reference to a Database object that is already constructed * and opened. This param is not used any longer * @param editTable the name of the table used for this query * @param query the rest of the query terms * cursor is finalized * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead */ @Deprecated public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { this(driver, editTable, query); } /** * Execute a query and provide access to its result set through a Cursor * interface. For a query such as: {@code SELECT name, birth, phone FROM * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, * phone) would be in the projection argument and everything from * {@code FROM} onward would be in the params argument. This constructor * has package scope. * * @param editTable the name of the table used for this query * @param query the {@link SQLiteQuery} object associated with this cursor object. */ public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { if (query == null) { throw new IllegalArgumentException("query object cannot be null"); } if (query.mDatabase == null) { throw new IllegalArgumentException("query.mDatabase cannot be null"); } if (StrictMode.vmSqliteObjectLeaksEnabled()) { mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); } else { mStackTrace = null; } mDriver = driver; mEditTable = editTable; mColumnNameMap = null; mQuery = query; query.mDatabase.lock(query.mSql); try { // Setup the list of columns int columnCount = mQuery.columnCountLocked(); mColumns = new String[columnCount]; // Read in all column names for (int i = 0; i < columnCount; i++) { String columnName = mQuery.columnNameLocked(i); mColumns[i] = columnName; if (false) { Log.v("DatabaseWindow", "mColumns[" + i + "] is " + mColumns[i]); } // Make note of the row ID column index for quick access to it if ("_id".equals(columnName)) { mRowIdColumnIndex = i; } } } finally { query.mDatabase.unlock(); } } /** * @return the SQLiteDatabase that this cursor is associated with. */ public SQLiteDatabase getDatabase() { synchronized (this) { return mQuery.mDatabase; } } @Override public boolean onMove(int oldPosition, int newPosition) { // Make sure the row at newPosition is present in the window if (mWindow == null || newPosition < mWindow.getStartPosition() || newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { fillWindow(newPosition); } return true; } @Override public int getCount() { if (mCount == NO_COUNT) { fillWindow(0); } return mCount; } private void fillWindow(int startPos) { clearOrCreateWindow(getDatabase().getPath()); mWindow.setStartPosition(startPos); int count = getQuery().fillWindow(mWindow); if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "received count(*) from native_fill_window: " + count); } mCount = count; } else if (mCount <= 0) { throw new IllegalStateException("Row count should never be zero or negative " + "when the start position is non-zero"); } } private synchronized SQLiteQuery getQuery() { return mQuery; } @Override public int getColumnIndex(String columnName) { // Create mColumnNameMap on demand if (mColumnNameMap == null) { String[] columns = mColumns; int columnCount = columns.length; HashMap<String, Integer> map = new HashMap<String, Integer>(columnCount, 1); for (int i = 0; i < columnCount; i++) { map.put(columns[i], i); } mColumnNameMap = map; } // Hack according to bug 903852 final int periodIndex = columnName.lastIndexOf('.'); if (periodIndex != -1) { Exception e = new Exception(); Log.e(TAG, "requesting column name with table name -- " + columnName, e); columnName = columnName.substring(periodIndex + 1); } Integer i = mColumnNameMap.get(columnName); if (i != null) { return i.intValue(); } else { return -1; } } @Override public String[] getColumnNames() { return mColumns; } @Override public void deactivate() { super.deactivate(); mDriver.cursorDeactivated(); } @Override public void close() { super.close(); synchronized (this) { mQuery.close(); mDriver.cursorClosed(); } } @Override public boolean requery() { if (isClosed()) { return false; } long timeStart = 0; if (false) { timeStart = System.currentTimeMillis(); } synchronized (this) { if (mWindow != null) { mWindow.clear(); } mPos = -1; SQLiteDatabase db = null; try { db = mQuery.mDatabase.getDatabaseHandle(mQuery.mSql); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); return false; } if (!db.equals(mQuery.mDatabase)) { // since we need to use a different database connection handle, // re-compile the query try { db.lock(mQuery.mSql); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); return false; } try { // close the old mQuery object and open a new one mQuery.close(); mQuery = new SQLiteQuery(db, mQuery); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); return false; } finally { db.unlock(); } } // This one will recreate the temp table, and get its count mDriver.cursorRequeried(this); mCount = NO_COUNT; try { mQuery.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); return false; } } if (false) { Log.v("DatabaseWindow", "closing window in requery()"); Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); } boolean result = false; try { result = super.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); } if (false) { long timeEnd = System.currentTimeMillis(); Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); } return result; } @Override public void setWindow(CursorWindow window) { super.setWindow(window); mCount = NO_COUNT; } /** * Changes the selection arguments. The new values take effect after a call to requery(). */ public void setSelectionArguments(String[] selectionArgs) { mDriver.setBindArguments(selectionArgs); } /** * Release the native resources, if they haven't been released yet. */ @Override protected void finalize() { try { // if the cursor hasn't been closed yet, close it first if (mWindow != null) { if (mStackTrace != null) { int len = mQuery.mSql.length(); StrictMode.onSqliteObjectLeaked( "Finalizing a Cursor that has not been deactivated or closed. " + "database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + ", query = " + mQuery.mSql.substring(0, (len > 1000) ? 1000 : len), mStackTrace); } close(); SQLiteDebug.notifyActiveCursorFinalized(); } else { if (false) { Log.v(TAG, "Finalizing cursor on database = " + mQuery.mDatabase.getPath() + ", table = " + mEditTable + ", query = " + mQuery.mSql); } } } finally { super.finalize(); } } }