diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/database/sqlite/SQLiteCursor.java | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/database/sqlite/SQLiteCursor.java')
-rw-r--r-- | core/java/android/database/sqlite/SQLiteCursor.java | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java new file mode 100644 index 0000000..70b9b83 --- /dev/null +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -0,0 +1,606 @@ +/* + * 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(); + } + } +} |