diff options
author | The Android Automerger <android-build@android.com> | 2011-10-27 17:41:45 -0700 |
---|---|---|
committer | The Android Automerger <android-build@android.com> | 2011-10-27 17:41:45 -0700 |
commit | f78da99419b68ad0d0b05ad2bcbbb328f6d06446 (patch) | |
tree | 4361f522376b56c3040d3145ca2ac4c53d466337 | |
parent | 8f2eb43b4f4ed99cdcb83698ab7af938c911a55c (diff) | |
download | frameworks_base-f78da99419b68ad0d0b05ad2bcbbb328f6d06446.zip frameworks_base-f78da99419b68ad0d0b05ad2bcbbb328f6d06446.tar.gz frameworks_base-f78da99419b68ad0d0b05ad2bcbbb328f6d06446.tar.bz2 |
Revert "Clean up CursorWindow lifetime."
This reverts commit 7ce745248d4de0e6543a559c93423df899832100.
-rw-r--r-- | core/java/android/database/AbstractCursor.java | 5 | ||||
-rw-r--r-- | core/java/android/database/AbstractWindowedCursor.java | 53 | ||||
-rw-r--r-- | core/java/android/database/BulkCursorToCursorAdaptor.java | 5 | ||||
-rw-r--r-- | core/java/android/database/CursorWindow.java | 20 | ||||
-rw-r--r-- | core/java/android/database/CursorWrapper.java | 4 | ||||
-rw-r--r-- | core/java/android/database/sqlite/SQLiteCursor.java | 194 | ||||
-rw-r--r-- | core/java/android/database/sqlite/SQLiteDatabase.java | 26 | ||||
-rw-r--r-- | core/java/android/database/sqlite/SQLiteQuery.java | 30 | ||||
-rw-r--r-- | core/jni/android_database_SQLiteQuery.cpp | 328 | ||||
-rw-r--r-- | core/tests/coretests/src/android/database/DatabaseCursorTest.java | 103 |
10 files changed, 582 insertions, 186 deletions
diff --git a/core/java/android/database/AbstractCursor.java b/core/java/android/database/AbstractCursor.java index 3f23b89..5fe42db 100644 --- a/core/java/android/database/AbstractCursor.java +++ b/core/java/android/database/AbstractCursor.java @@ -64,10 +64,7 @@ public abstract class AbstractCursor implements CrossProcessCursor { /* Methods that may optionally be implemented by subclasses */ /** - * If the cursor is backed by a {@link CursorWindow}, returns a pre-filled - * window with the contents of the cursor, otherwise null. - * - * @return The pre-filled window that backs this cursor, or null if none. + * returns a pre-filled window, return NULL if no such window */ public CursorWindow getWindow() { return null; diff --git a/core/java/android/database/AbstractWindowedCursor.java b/core/java/android/database/AbstractWindowedCursor.java index bfc8123..3d95769 100644 --- a/core/java/android/database/AbstractWindowedCursor.java +++ b/core/java/android/database/AbstractWindowedCursor.java @@ -18,22 +18,8 @@ package android.database; /** * A base class for Cursors that store their data in {@link CursorWindow}s. - * <p> - * Subclasses are responsible for filling the cursor window with data during - * {@link #onMove(int, int)}, allocating a new cursor window if necessary. - * During {@link #requery()}, the existing cursor window should be cleared and - * filled with new data. - * </p><p> - * If the contents of the cursor change or become invalid, the old window must be closed - * (because it is owned by the cursor) and set to null. - * </p> */ public abstract class AbstractWindowedCursor extends AbstractCursor { - /** - * The cursor window owned by this cursor. - */ - protected CursorWindow mWindow; - @Override public byte[] getBlob(int columnIndex) { checkPosition(); @@ -140,44 +126,25 @@ public abstract class AbstractWindowedCursor extends AbstractCursor { public CursorWindow getWindow() { return mWindow; } - + /** - * Sets a new cursor window for the cursor to use. - * <p> - * The cursor takes ownership of the provided cursor window; the cursor window - * will be closed when the cursor is closed or when the cursor adopts a new - * cursor window. - * </p><p> - * If the cursor previously had a cursor window, then it is closed when the - * new cursor window is assigned. - * </p> - * - * @param window The new cursor window, typically a remote cursor window. + * Set a new cursor window to cursor, usually set a remote cursor window + * @param window cursor window */ public void setWindow(CursorWindow window) { - if (window != mWindow) { - closeWindow(); - mWindow = window; + if (mWindow != null) { + mWindow.close(); } + mWindow = window; } - - /** - * Returns true if the cursor has an associated cursor window. - * - * @return True if the cursor has an associated cursor window. - */ + public boolean hasWindow() { return mWindow != null; } /** - * Closes the cursor window and sets {@link #mWindow} to null. - * @hide + * This needs be updated in {@link #onMove} by subclasses, and + * needs to be set to NULL when the contents of the cursor change. */ - protected void closeWindow() { - if (mWindow != null) { - mWindow.close(); - mWindow = null; - } - } + protected CursorWindow mWindow; } diff --git a/core/java/android/database/BulkCursorToCursorAdaptor.java b/core/java/android/database/BulkCursorToCursorAdaptor.java index 9c1b26d..16becf5 100644 --- a/core/java/android/database/BulkCursorToCursorAdaptor.java +++ b/core/java/android/database/BulkCursorToCursorAdaptor.java @@ -154,7 +154,10 @@ public final class BulkCursorToCursorAdaptor extends AbstractWindowedCursor { false /* the window will be accessed across processes */)); if (mCount != -1) { mPos = -1; - closeWindow(); + if (mWindow != null) { + mWindow.close(); + mWindow = null; + } // super.requery() will call onChanged. Do it here instead of relying on the // observer from the far side so that observers can see a correct value for mCount diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 2e3ef28..183662f 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -105,12 +105,8 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { } @Override - protected void finalize() throws Throwable { - try { - dispose(); - } finally { - super.finalize(); - } + protected void finalize() { + dispose(); } private void dispose() { @@ -149,12 +145,10 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { /** * Gets the start position of this cursor window. - * <p> - * The start position is the zero-based index of the first row that this window contains + * The start position is the index of the first row that this window contains * relative to the entire result set of the {@link Cursor}. - * </p> * - * @return The zero-based start position. + * @return The start position. */ public int getStartPosition() { return mStartPos; @@ -162,12 +156,10 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { /** * Sets the start position of this cursor window. - * <p> - * The start position is the zero-based index of the first row that this window contains + * The start position is the index of the first row that this window contains * relative to the entire result set of the {@link Cursor}. - * </p> * - * @param pos The new zero-based start position. + * @param pos The new start position. */ public void setStartPosition(int pos) { mStartPos = pos; diff --git a/core/java/android/database/CursorWrapper.java b/core/java/android/database/CursorWrapper.java index 320733e..3c3bd43 100644 --- a/core/java/android/database/CursorWrapper.java +++ b/core/java/android/database/CursorWrapper.java @@ -33,9 +33,7 @@ public class CursorWrapper implements Cursor { } /** - * Gets the underlying cursor that is wrapped by this instance. - * - * @return The wrapped cursor. + * @return the wrapped cursor */ public Cursor getWrappedCursor() { return mCursor; diff --git a/core/java/android/database/sqlite/SQLiteCursor.java b/core/java/android/database/sqlite/SQLiteCursor.java index 81fe824..ea9346d 100644 --- a/core/java/android/database/sqlite/SQLiteCursor.java +++ b/core/java/android/database/sqlite/SQLiteCursor.java @@ -18,11 +18,16 @@ package android.database.sqlite; import android.database.AbstractWindowedCursor; import android.database.CursorWindow; +import android.database.DataSetObserver; +import android.os.Handler; +import android.os.Message; +import android.os.Process; import android.os.StrictMode; import android.util.Log; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; /** * A Cursor implementation that exposes results from a query on a @@ -55,8 +60,140 @@ public class SQLiteCursor extends AbstractWindowedCursor { /** Used to find out where a cursor was allocated in case it never got released. */ private final 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(); + try { + if (mCursorState != mThreadState) { + break; + } + + int count = getQuery().fillWindow(cw, mMaxRead, mCount); + // return -1 means there is still more data to be retrieved from the resultset + if (count != 0) { + if (count == NO_COUNT){ + mCount += mMaxRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + + mCount + " rows so far"); + } + sendMessage(); + } else { + mCount += count; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received all data from native_fill_window. read " + + mCount + " rows."); + } + 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, @@ -156,23 +293,36 @@ public class SQLiteCursor extends AbstractWindowedCursor { return mCount; } - private void fillWindow(int startPos) { + 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 { - mWindow.clear(); + mCursorState++; + queryThreadLock(); + try { + mWindow.clear(); + } finally { + queryThreadUnlock(); + } } mWindow.setStartPosition(startPos); - int count = getQuery().fillWindow(mWindow); - if (startPos == 0) { // fillWindow returns count(*) only for startPos = 0 + int count = getQuery().fillWindow(mWindow, mInitialRead, 0); + // return -1 means there is still more data to be retrieved from the resultset + if (count == NO_COUNT){ + mCount = startPos + mInitialRead; + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received -1 from native_fill_window. read " + mCount + " rows so far"); + } + Thread t = new Thread(new QueryThread(mCursorState), "query thread"); + t.start(); + } else if (startPos == 0) { // native_fill_window 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"); + throw new IllegalStateException("count should never be non-zero negative number"); } } @@ -216,7 +366,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { private void deactivateCommon() { if (false) Log.v(TAG, "<<< Releasing cursor " + this); - closeWindow(); + mCursorState = 0; + if (mWindow != null) { + mWindow.close(); + mWindow = null; + } if (false) Log.v("DatabaseWindow", "closing window in release()"); } @@ -285,12 +439,16 @@ public class SQLiteCursor extends AbstractWindowedCursor { // This one will recreate the temp table, and get its count mDriver.cursorRequeried(this); mCount = NO_COUNT; + mCursorState++; + queryThreadLock(); try { mQuery.requery(); } catch (IllegalStateException e) { // for backwards compatibility, just return false Log.w(TAG, "requery() failed " + e.getMessage(), e); return false; + } finally { + queryThreadUnlock(); } } @@ -314,9 +472,18 @@ public class SQLiteCursor extends AbstractWindowedCursor { } @Override - public void setWindow(CursorWindow window) { - super.setWindow(window); - mCount = NO_COUNT; + public void setWindow(CursorWindow window) { + if (mWindow != null) { + mCursorState++; + queryThreadLock(); + try { + mWindow.close(); + } finally { + queryThreadUnlock(); + } + mCount = NO_COUNT; + } + mWindow = window; } /** @@ -354,4 +521,11 @@ public class SQLiteCursor extends AbstractWindowedCursor { super.finalize(); } } + + /** + * this is only for testing purposes. + */ + /* package */ int getMCount() { + return mCount; + } } diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java index 00d7ce8..d23873d 100644 --- a/core/java/android/database/sqlite/SQLiteDatabase.java +++ b/core/java/android/database/sqlite/SQLiteDatabase.java @@ -1598,6 +1598,32 @@ public class SQLiteDatabase extends SQLiteClosable { } /** + * Runs the provided SQL and returns a cursor over the result set. + * The cursor will read an initial set of rows and the return to the caller. + * It will continue to read in batches and send data changed notifications + * when the later batches are ready. + * @param sql the SQL query. The SQL string must not be ; terminated + * @param selectionArgs You may include ?s in where clause in the query, + * which will be replaced by the values from selectionArgs. The + * values will be bound as Strings. + * @param initialRead set the initial count of items to read from the cursor + * @param maxRead set the count of items to read on each iteration after the first + * @return A {@link Cursor} object, which is positioned before the first entry. Note that + * {@link Cursor}s are not synchronized, see the documentation for more details. + * + * This work is incomplete and not fully tested or reviewed, so currently + * hidden. + * @hide + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + int initialRead, int maxRead) { + SQLiteCursor c = (SQLiteCursor)rawQueryWithFactory( + null, sql, selectionArgs, null); + c.setLoadStyle(initialRead, maxRead); + return c; + } + + /** * Convenience method for inserting a row into the database. * * @param table the table to insert the row into diff --git a/core/java/android/database/sqlite/SQLiteQuery.java b/core/java/android/database/sqlite/SQLiteQuery.java index 7db0914..06a41b2 100644 --- a/core/java/android/database/sqlite/SQLiteQuery.java +++ b/core/java/android/database/sqlite/SQLiteQuery.java @@ -30,11 +30,6 @@ import android.util.Log; public class SQLiteQuery extends SQLiteProgram { private static final String TAG = "SQLiteQuery"; - private static native int nativeFillWindow(int databasePtr, int statementPtr, int windowPtr, - int startPos, int offsetParam); - private static native int nativeColumnCount(int statementPtr); - private static native String nativeColumnName(int statementPtr, int columnIndex); - /** The index of the unbound OFFSET parameter */ private int mOffsetIndex = 0; @@ -73,15 +68,19 @@ public class SQLiteQuery extends SQLiteProgram { * @param window The window to fill into * @return number of total rows in the query */ - /* package */ int fillWindow(CursorWindow window) { + /* package */ int fillWindow(CursorWindow window, + int maxRead, int lastPos) { mDatabase.lock(mSql); long timeStart = SystemClock.uptimeMillis(); try { acquireReference(); try { window.acquireReference(); - int numRows = nativeFillWindow(nHandle, nStatement, window.mWindowPtr, - window.getStartPosition(), mOffsetIndex); + // if the start pos is not equal to 0, then most likely window is + // too small for the data set, loading by another thread + // is not safe in this situation. the native code will ignore maxRead + int numRows = native_fill_window(window.mWindowPtr, window.getStartPosition(), + mOffsetIndex, maxRead, lastPos); mDatabase.logTimeStat(mSql, timeStart); return numRows; } catch (IllegalStateException e){ @@ -112,7 +111,7 @@ public class SQLiteQuery extends SQLiteProgram { /* package */ int columnCountLocked() { acquireReference(); try { - return nativeColumnCount(nStatement); + return native_column_count(); } finally { releaseReference(); } @@ -128,17 +127,17 @@ public class SQLiteQuery extends SQLiteProgram { /* package */ String columnNameLocked(int columnIndex) { acquireReference(); try { - return nativeColumnName(nStatement, columnIndex); + return native_column_name(columnIndex); } finally { releaseReference(); } } - + @Override public String toString() { return "SQLiteQuery: " + mSql; } - + @Override public void close() { super.close(); @@ -154,4 +153,11 @@ public class SQLiteQuery extends SQLiteProgram { } compileAndbindAllArgs(); } + + private final native int native_fill_window(int windowPtr, + int startPos, int offsetParam, int maxRead, int lastPos); + + private final native int native_column_count(); + + private final native String native_column_name(int columnIndex); } diff --git a/core/jni/android_database_SQLiteQuery.cpp b/core/jni/android_database_SQLiteQuery.cpp index 022a64c..fe62256 100644 --- a/core/jni/android_database_SQLiteQuery.cpp +++ b/core/jni/android_database_SQLiteQuery.cpp @@ -35,20 +35,99 @@ namespace android { -static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, - jint statementPtr, jint windowPtr, jint startPos, jint offsetParam) { - sqlite3* database = reinterpret_cast<sqlite3*>(databasePtr); - sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); - CursorWindow* window = reinterpret_cast<CursorWindow*>(windowPtr); +static jfieldID gHandleField; +static jfieldID gStatementField; + + +#define GET_STATEMENT(env, object) \ + (sqlite3_stmt *)env->GetIntField(object, gStatementField) +#define GET_HANDLE(env, object) \ + (sqlite3 *)env->GetIntField(object, gHandleField) + +static int skip_rows(sqlite3_stmt *statement, int maxRows) { + int retryCount = 0; + for (int i = 0; i < maxRows; i++) { + int err = sqlite3_step(statement); + if (err == SQLITE_ROW){ + // do nothing + } else if (err == SQLITE_DONE) { + return i; + } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) { + // The table is locked, retry + LOG_WINDOW("Database locked, retrying"); + if (retryCount > 50) { + LOGE("Bailing on database busy rety"); + break; + } + // Sleep to give the thread holding the lock a chance to finish + usleep(1000); + retryCount++; + continue; + } else { + return -1; + } + } + LOG_WINDOW("skip_rows row %d", maxRows); + return maxRows; +} + +static int finish_program_and_get_row_count(sqlite3_stmt *statement) { + int numRows = 0; + int retryCount = 0; + while (true) { + int err = sqlite3_step(statement); + if (err == SQLITE_ROW){ + numRows++; + } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) { + // The table is locked, retry + LOG_WINDOW("Database locked, retrying"); + if (retryCount > 50) { + LOGE("Bailing on database busy rety"); + break; + } + // Sleep to give the thread holding the lock a chance to finish + usleep(1000); + retryCount++; + continue; + } else { + // no need to throw exception + break; + } + } + sqlite3_reset(statement); + LOG_WINDOW("finish_program_and_get_row_count row %d", numRows); + return numRows; +} + +static jint native_fill_window(JNIEnv* env, jobject object, jint windowPtr, + jint startPos, jint offsetParam, jint maxRead, jint lastPos) +{ + int err; + sqlite3_stmt * statement = GET_STATEMENT(env, object); + int numRows = lastPos; + maxRead += lastPos; + int numColumns; + int retryCount; + int boundParams; + CursorWindow * window; + bool gotAllRows = true; + bool gotException = false; + + if (statement == NULL) { + LOGE("Invalid statement in fillWindow()"); + jniThrowException(env, "java/lang/IllegalStateException", + "Attempting to access a deactivated, closed, or empty cursor"); + return 0; + } // Only do the binding if there is a valid offsetParam. If no binding needs to be done - // offsetParam will be set to 0, an invalid value. - if (offsetParam > 0) { + // offsetParam will be set to 0, an invliad value. + if(offsetParam > 0) { // Bind the offset parameter, telling the program which row to start with - int err = sqlite3_bind_int(statement, offsetParam, startPos); + err = sqlite3_bind_int(statement, offsetParam, startPos); if (err != SQLITE_OK) { LOGE("Unable to bind offset position, offsetParam = %d", offsetParam); - throw_sqlite3_exception(env, database); + throw_sqlite3_exception(env, GET_HANDLE(env, object)); return 0; } LOG_WINDOW("Bound to startPos %d", startPos); @@ -56,123 +135,136 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, LOG_WINDOW("Not binding to startPos %d", startPos); } - // We assume numRows is initially 0. - LOG_WINDOW("Window: numRows = %d, size = %d, freeSpace = %d", - window->getNumRows(), window->size(), window->freeSpace()); + // Get the native window + window = reinterpret_cast<CursorWindow*>(windowPtr); + if (!window) { + LOGE("Invalid CursorWindow"); + jniThrowException(env, "java/lang/IllegalArgumentException", + "Bad CursorWindow"); + return 0; + } + LOG_WINDOW("Window: numRows = %d, size = %d, freeSpace = %d", window->getNumRows(), window->size(), window->freeSpace()); - int numColumns = sqlite3_column_count(statement); + numColumns = sqlite3_column_count(statement); if (!window->setNumColumns(numColumns)) { LOGE("Failed to change column count from %d to %d", window->getNumColumns(), numColumns); jniThrowException(env, "java/lang/IllegalStateException", "numColumns mismatch"); return 0; } - int retryCount = 0; - int totalRows = 0; - int addedRows = 0; - bool windowFull = false; - bool gotException = false; - const bool countAllRows = (startPos == 0); // when startPos is 0, we count all rows - while (!gotException && (!windowFull || countAllRows)) { - int err = sqlite3_step(statement); + retryCount = 0; + if (startPos > 0) { + int num = skip_rows(statement, startPos); + if (num < 0) { + throw_sqlite3_exception(env, GET_HANDLE(env, object)); + return 0; + } else if (num < startPos) { + LOGE("startPos %d > actual rows %d", startPos, num); + return num; + } + } + + while(startPos != 0 || numRows < maxRead) { + err = sqlite3_step(statement); if (err == SQLITE_ROW) { - LOG_WINDOW("Stepped statement %p to row %d", statement, totalRows); + LOG_WINDOW("\nStepped statement %p to row %d", statement, startPos + numRows); retryCount = 0; - totalRows += 1; - - // Skip the row if the window is full or we haven't reached the start position yet. - if (startPos >= totalRows || windowFull) { - continue; - } // Allocate a new field directory for the row. This pointer is not reused - // since it may be possible for it to be relocated on a call to alloc() when + // since it mey be possible for it to be relocated on a call to alloc() when // the field data is being allocated. - field_slot_t* fieldDir = window->allocRow(); - if (!fieldDir) { - LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d", - startPos, addedRows); - windowFull = true; - continue; + { + field_slot_t * fieldDir = window->allocRow(); + if (!fieldDir) { + LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d", startPos, numRows); + gotAllRows = false; + goto return_count; + } } - // Pack the row into the window. - for (int i = 0; i < numColumns; i++) { + // Pack the row into the window + int i; + for (i = 0; i < numColumns; i++) { int type = sqlite3_column_type(statement, i); if (type == SQLITE_TEXT) { // TEXT data #if WINDOW_STORAGE_UTF8 - const uint8_t* text = reinterpret_cast<const uint8_t*>( - sqlite3_column_text(statement, i)); + uint8_t const * text = (uint8_t const *)sqlite3_column_text(statement, i); // SQLite does not include the NULL terminator in size, but does // ensure all strings are NULL terminated, so increase size by // one to make sure we store the terminator. size_t size = sqlite3_column_bytes(statement, i) + 1; #else - const uint8_t* text = reinterpret_cast<const uint8_t*>( - sqlite3_column_text16(statement, i)); + uint8_t const * text = (uint8_t const *)sqlite3_column_text16(statement, i); size_t size = sqlite3_column_bytes16(statement, i); #endif int offset = window->alloc(size); if (!offset) { + window->freeLastRow(); LOG_WINDOW("Failed allocating %u bytes for text/blob at %d,%d", size, - startPos + addedRows, i); - windowFull = true; - break; + startPos + numRows, i); + gotAllRows = false; + goto return_count; } + window->copyIn(offset, text, size); - field_slot_t* fieldSlot = window->getFieldSlot(addedRows, i); + // This must be updated after the call to alloc(), since that + // may move the field around in the window + field_slot_t * fieldSlot = window->getFieldSlot(numRows, i); fieldSlot->type = FIELD_TYPE_STRING; fieldSlot->data.buffer.offset = offset; fieldSlot->data.buffer.size = size; - LOG_WINDOW("%d,%d is TEXT with %u bytes", startPos + addedRows, i, size); + + LOG_WINDOW("%d,%d is TEXT with %u bytes", startPos + numRows, i, size); } else if (type == SQLITE_INTEGER) { // INTEGER data int64_t value = sqlite3_column_int64(statement, i); - if (!window->putLong(addedRows, i, value)) { + if (!window->putLong(numRows, i, value)) { + window->freeLastRow(); LOG_WINDOW("Failed allocating space for a long in column %d", i); - windowFull = true; - break; + gotAllRows = false; + goto return_count; } - LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value); + LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + numRows, i, value); } else if (type == SQLITE_FLOAT) { // FLOAT data double value = sqlite3_column_double(statement, i); - if (!window->putDouble(addedRows, i, value)) { + if (!window->putDouble(numRows, i, value)) { + window->freeLastRow(); LOG_WINDOW("Failed allocating space for a double in column %d", i); - windowFull = true; - break; + gotAllRows = false; + goto return_count; } - LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value); + LOG_WINDOW("%d,%d is FLOAT %lf", startPos + numRows, i, value); } else if (type == SQLITE_BLOB) { // BLOB data uint8_t const * blob = (uint8_t const *)sqlite3_column_blob(statement, i); size_t size = sqlite3_column_bytes16(statement, i); int offset = window->alloc(size); if (!offset) { + window->freeLastRow(); LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d", size, - startPos + addedRows, i); - windowFull = true; - break; + startPos + numRows, i); + gotAllRows = false; + goto return_count; } + window->copyIn(offset, blob, size); - field_slot_t* fieldSlot = window->getFieldSlot(addedRows, i); + // This must be updated after the call to alloc(), since that + // may move the field around in the window + field_slot_t * fieldSlot = window->getFieldSlot(numRows, i); fieldSlot->type = FIELD_TYPE_BLOB; fieldSlot->data.buffer.offset = offset; fieldSlot->data.buffer.size = size; - LOG_WINDOW("%d,%d is Blob with %u bytes @ %d", - startPos + addedRows, i, size, offset); + + LOG_WINDOW("%d,%d is Blob with %u bytes @ %d", startPos + numRows, i, size, offset); } else if (type == SQLITE_NULL) { // NULL field - if (!window->putNull(addedRows, i)) { - LOG_WINDOW("Failed allocating space for a null in column %d", i); - windowFull = true; - break; - } + window->putNull(numRows, i); - LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i); + LOG_WINDOW("%d,%d is NULL", startPos + numRows, i); } else { // Unknown data LOGE("Unknown column type when filling database window"); @@ -182,12 +274,14 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, } } - // Update the final row tally. - if (windowFull || gotException) { - window->freeLastRow(); - } else { - addedRows += 1; + if (i < numColumns) { + // Not all the fields fit in the window + // Unknown data error happened + break; } + + // Mark the row as complete in the window + numRows++; } else if (err == SQLITE_DONE) { // All rows processed, bail LOG_WINDOW("Processed all rows"); @@ -196,41 +290,63 @@ static jint nativeFillWindow(JNIEnv* env, jclass clazz, jint databasePtr, // The table is locked, retry LOG_WINDOW("Database locked, retrying"); if (retryCount > 50) { - LOGE("Bailing on database busy retry"); - throw_sqlite3_exception(env, database, "retrycount exceeded"); + LOGE("Bailing on database busy rety"); + throw_sqlite3_exception(env, GET_HANDLE(env, object), "retrycount exceeded"); gotException = true; - } else { - // Sleep to give the thread holding the lock a chance to finish - usleep(1000); - retryCount++; + break; } + + // Sleep to give the thread holding the lock a chance to finish + usleep(1000); + + retryCount++; + continue; } else { - throw_sqlite3_exception(env, database); + throw_sqlite3_exception(env, GET_HANDLE(env, object)); gotException = true; + break; } } - LOG_WINDOW("Resetting statement %p after fetching %d rows and adding %d rows" - "to the window in %d bytes", - statement, totalRows, addedRows, window->size() - window->freeSpace()); - sqlite3_reset(statement); - - // Report the total number of rows on request. - if (startPos > totalRows) { - LOGE("startPos %d > actual rows %d", startPos, totalRows); + LOG_WINDOW("Resetting statement %p after fetching %d rows in %d bytes\n\n\n\n", statement, + numRows, window->size() - window->freeSpace()); + LOG_WINDOW("Filled window with %d rows in %d bytes", numRows, + window->size() - window->freeSpace()); + if (err == SQLITE_ROW) { + // there is more data to be returned. let the caller know by returning -1 + return -1; + } + return_count: + if (startPos) { + sqlite3_reset(statement); + LOG_WINDOW("Not doing count(*) because startPos %d is non-zero", startPos); + return 0; + } else if (gotAllRows) { + sqlite3_reset(statement); + LOG_WINDOW("Not doing count(*) because we already know the count(*)"); + return numRows; + } else if (gotException) { + return 0; + } else { + // since startPos == 0, we need to get the count(*) of the result set + return numRows + 1 + finish_program_and_get_row_count(statement); } - return countAllRows ? totalRows : 0; } -static jint nativeColumnCount(JNIEnv* env, jclass clazz, jint statementPtr) { - sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); +static jint native_column_count(JNIEnv* env, jobject object) +{ + sqlite3_stmt * statement = GET_STATEMENT(env, object); + return sqlite3_column_count(statement); } -static jstring nativeColumnName(JNIEnv* env, jclass clazz, jint statementPtr, - jint columnIndex) { - sqlite3_stmt* statement = reinterpret_cast<sqlite3_stmt*>(statementPtr); - const char* name = sqlite3_column_name(statement, columnIndex); +static jstring native_column_name(JNIEnv* env, jobject object, jint columnIndex) +{ + sqlite3_stmt * statement = GET_STATEMENT(env, object); + char const * name; + + name = sqlite3_column_name(statement, columnIndex); + return env->NewStringUTF(name); } @@ -238,16 +354,30 @@ static jstring nativeColumnName(JNIEnv* env, jclass clazz, jint statementPtr, static JNINativeMethod sMethods[] = { /* name, signature, funcPtr */ - { "nativeFillWindow", "(IIIII)I", - (void*)nativeFillWindow }, - { "nativeColumnCount", "(I)I", - (void*)nativeColumnCount}, - { "nativeColumnName", "(II)Ljava/lang/String;", - (void*)nativeColumnName}, + {"native_fill_window", "(IIIII)I", + (void *)native_fill_window}, + {"native_column_count", "()I", (void*)native_column_count}, + {"native_column_name", "(I)Ljava/lang/String;", (void *)native_column_name}, }; int register_android_database_SQLiteQuery(JNIEnv * env) { + jclass clazz; + + clazz = env->FindClass("android/database/sqlite/SQLiteQuery"); + if (clazz == NULL) { + LOGE("Can't find android/database/sqlite/SQLiteQuery"); + return -1; + } + + gHandleField = env->GetFieldID(clazz, "nHandle", "I"); + gStatementField = env->GetFieldID(clazz, "nStatement", "I"); + + if (gHandleField == NULL || gStatementField == NULL) { + LOGE("Error locating fields"); + return -1; + } + return AndroidRuntime::registerNativeMethods(env, "android/database/sqlite/SQLiteQuery", sMethods, NELEM(sMethods)); } diff --git a/core/tests/coretests/src/android/database/DatabaseCursorTest.java b/core/tests/coretests/src/android/database/DatabaseCursorTest.java index 179338d..d5b9ee6 100644 --- a/core/tests/coretests/src/android/database/DatabaseCursorTest.java +++ b/core/tests/coretests/src/android/database/DatabaseCursorTest.java @@ -290,7 +290,110 @@ public class DatabaseCursorTest extends AndroidTestCase implements PerformanceTe public void onInvalidated() { } } + + //@Large + @Suppress + public void testLoadingThreadDelayRegisterData() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); + + final int count = 505; + String sql = "INSERT INTO test (data) VALUES (?);"; + SQLiteStatement s = mDatabase.compileStatement(sql); + for (int i = 0; i < count; i++) { + s.bindLong(1, i); + s.execute(); + } + + int maxRead = 500; + int initialRead = 5; + SQLiteCursor c = (SQLiteCursor)mDatabase.rawQuery("select * from test;", + null, initialRead, maxRead); + + TestObserver observer = new TestObserver(count, c); + c.getCount(); + c.registerDataSetObserver(observer); + if (!observer.quit) { + Looper.loop(); + } + c.close(); + } + + //@LargeTest + @BrokenTest("Consistently times out") + @Suppress + public void testLoadingThread() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); + + final int count = 50000; + String sql = "INSERT INTO test (data) VALUES (?);"; + SQLiteStatement s = mDatabase.compileStatement(sql); + for (int i = 0; i < count; i++) { + s.bindLong(1, i); + s.execute(); + } + int maxRead = 1000; + int initialRead = 5; + SQLiteCursor c = (SQLiteCursor)mDatabase.rawQuery("select * from test;", + null, initialRead, maxRead); + + TestObserver observer = new TestObserver(count, c); + c.registerDataSetObserver(observer); + c.getCount(); + + Looper.loop(); + c.close(); + } + + //@LargeTest + @BrokenTest("Consistently times out") + @Suppress + public void testLoadingThreadClose() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); + + final int count = 1000; + String sql = "INSERT INTO test (data) VALUES (?);"; + SQLiteStatement s = mDatabase.compileStatement(sql); + for (int i = 0; i < count; i++) { + s.bindLong(1, i); + s.execute(); + } + + int maxRead = 11; + int initialRead = 5; + SQLiteCursor c = (SQLiteCursor)mDatabase.rawQuery("select * from test;", + null, initialRead, maxRead); + + TestObserver observer = new TestObserver(count, c); + c.registerDataSetObserver(observer); + c.getCount(); + c.close(); + } + + @LargeTest + public void testLoadingThreadDeactivate() throws Exception { + mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); + + final int count = 1000; + String sql = "INSERT INTO test (data) VALUES (?);"; + SQLiteStatement s = mDatabase.compileStatement(sql); + for (int i = 0; i < count; i++) { + s.bindLong(1, i); + s.execute(); + } + + int maxRead = 11; + int initialRead = 5; + SQLiteCursor c = (SQLiteCursor)mDatabase.rawQuery("select * from test;", + null, initialRead, maxRead); + + TestObserver observer = new TestObserver(count, c); + c.registerDataSetObserver(observer); + c.getCount(); + c.deactivate(); + c.close(); + } + @LargeTest public void testManyRowsLong() throws Exception { mDatabase.execSQL("CREATE TABLE test (_id INTEGER PRIMARY KEY, data INT);"); |