blob: 3f0fcb1d98dbb978d0284cdda768b8327a63ee69 [file] [log] [blame]
/*
* 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 Throwable mStackTrace;
/**
* 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();
mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
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 the cursor hasn't been closed yet, close it first
if (mWindow != null) {
close();
Log.e(TAG, "Finalizing a Cursor that has not been deactivated or closed. " +
"database = " + mDatabase.getPath() + ", table = " + mEditTable +
", query = " + mQuery.mSql, mStackTrace);
SQLiteDebug.notifyActiveCursorFinalized();
} else {
if (Config.LOGV) {
Log.v(TAG, "Finalizing cursor on database = " + mDatabase.getPath() +
", table = " + mEditTable + ", query = " + mQuery.mSql);
}
}
} finally {
super.finalize();
}
}
}