/* * 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.database.DataSetObserver; import android.database.SQLException; import android.os.Handler; import android.os.Message; import android.os.Process; import android.text.TextUtils; import android.util.Config; import android.util.Log; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; /** * A Cursor implementation that exposes results from a query on a * {@link SQLiteDatabase}. */ public class SQLiteCursor extends AbstractWindowedCursor { static final String TAG = "Cursor"; static final int NO_COUNT = -1; /** The name of the table to edit */ private String mEditTable; /** The names of the columns in the rows */ private String[] mColumns; /** The query object for the cursor */ private SQLiteQuery mQuery; /** The database the cursor was created from */ private SQLiteDatabase mDatabase; /** The compiled query this cursor came from */ private SQLiteCursorDriver mDriver; /** The number of rows in the cursor */ private 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 StackTraceElement[] mStackTraceElements; /** * mMaxRead is the max items that each cursor window reads * default to a very high value */ private int mMaxRead = Integer.MAX_VALUE; private int mInitialRead = Integer.MAX_VALUE; private int mCursorState = 0; private ReentrantLock mLock = null; private boolean mPendingData = false; /** * support for a cursor variant that doesn't always read all results * initialRead is the initial number of items that cursor window reads * if query contains more than this number of items, a thread will be * created and handle the left over items so that caller can show * results as soon as possible * @param initialRead initial number of items that cursor read * @param maxRead leftover items read at maxRead items per time * @hide */ public void setLoadStyle(int initialRead, int maxRead) { mMaxRead = maxRead; mInitialRead = initialRead; mLock = new ReentrantLock(true); } private void queryThreadLock() { if (mLock != null) { mLock.lock(); } } private void queryThreadUnlock() { if (mLock != null) { mLock.unlock(); } } /** * @hide */ final private class QueryThread implements Runnable { private final int mThreadState; QueryThread(int version) { mThreadState = version; } private void sendMessage() { if (mNotificationHandler != null) { mNotificationHandler.sendEmptyMessage(1); mPendingData = false; } else { mPendingData = true; } } public void run() { // use cached mWindow, to avoid get null mWindow CursorWindow cw = mWindow; Process.setThreadPriority(Process.myTid(), Process.THREAD_PRIORITY_BACKGROUND); // the cursor's state doesn't change while (true) { mLock.lock(); if (mCursorState != mThreadState) { mLock.unlock(); break; } try { int count = mQuery.fillWindow(cw, mMaxRead, mCount); // return -1 means not finished if (count != 0) { if (count == NO_COUNT){ mCount += mMaxRead; sendMessage(); } else { mCount = count; sendMessage(); break; } } else { break; } } catch (Exception e) { // end the tread when the cursor is close break; } finally { mLock.unlock(); } } } } /** * @hide */ protected class MainThreadNotificationHandler extends Handler { public void handleMessage(Message msg) { notifyDataSetChange(); } } /** * @hide */ protected MainThreadNotificationHandler mNotificationHandler; public void registerDataSetObserver(DataSetObserver observer) { super.registerDataSetObserver(observer); if ((Integer.MAX_VALUE != mMaxRead || Integer.MAX_VALUE != mInitialRead) && mNotificationHandler == null) { queryThreadLock(); try { mNotificationHandler = new MainThreadNotificationHandler(); if (mPendingData) { notifyDataSetChange(); mPendingData = false; } } finally { queryThreadUnlock(); } } } /** * 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 * @param editTable the name of the table used for this query * @param query the rest of the query terms * cursor is finalized */ public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { // The AbstractCursor constructor needs to do some setup. super(); if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { mStackTraceElements = new Exception().getStackTrace(); } mDatabase = db; mDriver = driver; mEditTable = editTable; mColumnNameMap = null; mQuery = query; try { db.lock(); // 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 (Config.LOGV) { 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 { db.unlock(); } } /** * @return the SQLiteDatabase that this cursor is associated with. */ public SQLiteDatabase getDatabase() { return 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) { if (mWindow == null) { // If there isn't a window set already it will only be accessed locally mWindow = new CursorWindow(true /* the window is local only */); } else { mCursorState++; queryThreadLock(); try { mWindow.clear(); } finally { queryThreadUnlock(); } } mWindow.setStartPosition(startPos); mCount = mQuery.fillWindow(mWindow, mInitialRead, 0); // return -1 means not finished if (mCount == NO_COUNT){ mCount = startPos + mInitialRead; Thread t = new Thread(new QueryThread(mCursorState), "query thread"); t.start(); } } @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; } } /** * @hide * @deprecated */ @Override public boolean deleteRow() { checkPosition(); // Only allow deletes if there is an ID column, and the ID has been read from it if (mRowIdColumnIndex == -1 || mCurrentRowID == null) { Log.e(TAG, "Could not delete row because either the row ID column is not available or it" + "has not been read."); return false; } boolean success; /* * Ensure we don't change the state of the database when another * thread is holding the database lock. requery() and moveTo() are also * synchronized here to make sure they get the state of the database * immediately following the DELETE. */ mDatabase.lock(); try { try { mDatabase.delete(mEditTable, mColumns[mRowIdColumnIndex] + "=?", new String[] {mCurrentRowID.toString()}); success = true; } catch (SQLException e) { success = false; } int pos = mPos; requery(); /* * Ensure proper cursor state. Note that mCurrentRowID changes * in this call. */ moveToPosition(pos); } finally { mDatabase.unlock(); } if (success) { onChange(true); return true; } else { return false; } } @Override public String[] getColumnNames() { return mColumns; } /** * @hide * @deprecated */ @Override public boolean supportsUpdates() { return super.supportsUpdates() && !TextUtils.isEmpty(mEditTable); } /** * @hide * @deprecated */ @Override public boolean commitUpdates(Map<? extends Long, ? extends Map<String, Object>> additionalValues) { if (!supportsUpdates()) { Log.e(TAG, "commitUpdates not supported on this cursor, did you " + "include the _id column?"); return false; } /* * Prevent other threads from changing the updated rows while they're * being processed here. */ synchronized (mUpdatedRows) { if (additionalValues != null) { mUpdatedRows.putAll(additionalValues); } if (mUpdatedRows.size() == 0) { return true; } /* * Prevent other threads from changing the database state while * we process the updated rows, and prevents us from changing the * database behind the back of another thread. */ mDatabase.beginTransaction(); try { StringBuilder sql = new StringBuilder(128); // For each row that has been updated for (Map.Entry<Long, Map<String, Object>> rowEntry : mUpdatedRows.entrySet()) { Map<String, Object> values = rowEntry.getValue(); Long rowIdObj = rowEntry.getKey(); if (rowIdObj == null || values == null) { throw new IllegalStateException("null rowId or values found! rowId = " + rowIdObj + ", values = " + values); } if (values.size() == 0) { continue; } long rowId = rowIdObj.longValue(); Iterator<Map.Entry<String, Object>> valuesIter = values.entrySet().iterator(); sql.setLength(0); sql.append("UPDATE " + mEditTable + " SET "); // For each column value that has been updated Object[] bindings = new Object[values.size()]; int i = 0; while (valuesIter.hasNext()) { Map.Entry<String, Object> entry = valuesIter.next(); sql.append(entry.getKey()); sql.append("=?"); bindings[i] = entry.getValue(); if (valuesIter.hasNext()) { sql.append(", "); } i++; } sql.append(" WHERE " + mColumns[mRowIdColumnIndex] + '=' + rowId); sql.append(';'); mDatabase.execSQL(sql.toString(), bindings); mDatabase.rowUpdated(mEditTable, rowId); } mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); } mUpdatedRows.clear(); } // Let any change observers know about the update onChange(true); return true; } private void deactivateCommon() { if (Config.LOGV) Log.v(TAG, "<<< Releasing cursor " + this); mCursorState = 0; if (mWindow != null) { mWindow.close(); mWindow = null; } if (Config.LOGV) Log.v("DatabaseWindow", "closing window in release()"); } @Override public void deactivate() { super.deactivate(); deactivateCommon(); mDriver.cursorDeactivated(); } @Override public void close() { super.close(); deactivateCommon(); mQuery.close(); mDriver.cursorClosed(); } @Override public boolean requery() { if (isClosed()) { return false; } long timeStart = 0; if (Config.LOGV) { timeStart = System.currentTimeMillis(); } /* * Synchronize on the database lock to ensure that mCount matches the * results of mQuery.requery(). */ mDatabase.lock(); try { if (mWindow != null) { mWindow.clear(); } mPos = -1; // This one will recreate the temp table, and get its count mDriver.cursorRequeried(this); mCount = NO_COUNT; mCursorState++; queryThreadLock(); try { mQuery.requery(); } finally { queryThreadUnlock(); } } finally { mDatabase.unlock(); } if (Config.LOGV) { Log.v("DatabaseWindow", "closing window in requery()"); Log.v(TAG, "--- Requery()ed cursor " + this + ": " + mQuery); } boolean result = super.requery(); if (Config.LOGV) { long timeEnd = System.currentTimeMillis(); Log.v(TAG, "requery (" + (timeEnd - timeStart) + " ms): " + mDriver.toString()); } return result; } @Override public void setWindow(CursorWindow window) { if (mWindow != null) { mCursorState++; queryThreadLock(); try { mWindow.close(); } finally { queryThreadUnlock(); } mCount = NO_COUNT; } mWindow = window; } /** * 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 (mWindow != null) { close(); String message = "Finalizing cursor " + this + " on " + mEditTable + " that has not been deactivated or closed"; if (SQLiteDebug.DEBUG_ACTIVE_CURSOR_FINALIZATION) { Log.d(TAG, message + "\nThis cursor was created in:"); for (StackTraceElement ste : mStackTraceElements) { Log.d(TAG, " " + ste); } } SQLiteDebug.notifyActiveCursorFinalized(); throw new IllegalStateException(message); } else { if (Config.LOGV) { Log.v(TAG, "Finalizing cursor " + this + " on " + mEditTable); } } } finally { super.finalize(); } } }