From 2e0c18d7551b729ba4b1fab1aecfb58ce60491e3 Mon Sep 17 00:00:00 2001 From: Julian Winkler Date: Thu, 24 Aug 2023 12:43:13 +0200 Subject: [PATCH] add complete java side of sqlite implementation Mostly taken from https://www.sqlite.org/android which is forked from AOSPs implementation --- .../android_database_CursorWindow.h | 165 ++ ...android_database_sqlite_SQLiteConnection.h | 231 ++ .../android_database_sqlite_SQLiteDebug.h | 23 + .../android_database_sqlite_SQLiteGlobal.h | 21 + .../android/content/ContentResolver.java | 9 + src/api-impl/android/content/Context.java | 7 + .../android/database/AbstractCursor.java | 484 ++++ .../database/AbstractWindowedCursor.java | 209 ++ .../android/database/CharArrayBuffer.java | 33 + .../android/database/ContentObservable.java | 91 + .../android/database/ContentObserver.java | 250 +- .../android/database/CrossProcessCursor.java | 78 + src/api-impl/android/database/Cursor.java | 61 +- .../android/database/CursorWindow.java | 671 +++++ .../android/database/DatabaseUtils.java | 1313 ++++++++++ .../database/DefaultDatabaseErrorHandler.java | 117 + .../android/database/SQLException.java | 37 + .../DatabaseObjectNotClosedException.java | 35 + .../database/sqlite/SQLiteAbortException.java | 34 + .../sqlite/SQLiteAccessPermException.java | 33 + ...eBindOrColumnIndexOutOfRangeException.java | 32 + .../sqlite/SQLiteBlobTooBigException.java | 29 + .../SQLiteCantOpenDatabaseException.java | 29 + .../database/sqlite/SQLiteClosable.java | 112 + .../database/sqlite/SQLiteConnection.java | 1525 ++++++++++++ .../database/sqlite/SQLiteConnectionPool.java | 1086 ++++++++ .../sqlite/SQLiteConstraintException.java | 32 + .../android/database/sqlite/SQLiteCursor.java | 369 ++- .../database/sqlite/SQLiteCustomFunction.java | 57 + .../database/sqlite/SQLiteDatabase.java | 2207 ++++++++++++++++- .../sqlite/SQLiteDatabaseConfiguration.java | 177 ++ .../SQLiteDatabaseCorruptException.java | 32 + .../sqlite/SQLiteDatabaseLockedException.java | 37 + .../SQLiteDatatypeMismatchException.java | 29 + .../android/database/sqlite/SQLiteDebug.java | 176 ++ .../sqlite/SQLiteDirectCursorDriver.java | 2 +- .../sqlite/SQLiteDiskIOException.java | 33 + .../database/sqlite/SQLiteDoneException.java | 35 + .../database/sqlite/SQLiteException.java | 37 +- .../database/sqlite/SQLiteFullException.java | 32 + .../android/database/sqlite/SQLiteGlobal.java | 118 + .../sqlite/SQLiteMisuseException.java | 41 + .../database/sqlite/SQLiteOpenHelper.java | 505 +++- .../sqlite/SQLiteOutOfMemoryException.java | 29 + .../database/sqlite/SQLiteProgram.java | 358 ++- .../android/database/sqlite/SQLiteQuery.java | 56 +- .../database/sqlite/SQLiteQueryBuilder.java | 651 +++++ .../SQLiteReadOnlyDatabaseException.java | 29 + .../database/sqlite/SQLiteSession.java | 967 ++++++++ .../database/sqlite/SQLiteStatement.java | 167 ++ .../database/sqlite/SQLiteStatementInfo.java | 43 + .../sqlite/SQLiteTableLockedException.java | 29 + .../sqlite/SQLiteTransactionListener.java | 41 + .../android/os/CancellationSignal.java | 122 +- .../os/OperationCanceledException.java | 33 + src/api-impl/android/util/LruCache.java | 361 +++ src/api-impl/android/util/Pair.java | 82 + src/api-impl/meson.build | 42 +- 58 files changed, 12959 insertions(+), 685 deletions(-) create mode 100644 src/api-impl-jni/generated_headers/android_database_CursorWindow.h create mode 100644 src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteConnection.h create mode 100644 src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteDebug.h create mode 100644 src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteGlobal.h create mode 100644 src/api-impl/android/database/AbstractCursor.java create mode 100644 src/api-impl/android/database/AbstractWindowedCursor.java create mode 100644 src/api-impl/android/database/CharArrayBuffer.java create mode 100644 src/api-impl/android/database/ContentObservable.java create mode 100644 src/api-impl/android/database/CrossProcessCursor.java create mode 100644 src/api-impl/android/database/CursorWindow.java create mode 100644 src/api-impl/android/database/DatabaseUtils.java create mode 100644 src/api-impl/android/database/DefaultDatabaseErrorHandler.java create mode 100644 src/api-impl/android/database/SQLException.java create mode 100644 src/api-impl/android/database/sqlite/DatabaseObjectNotClosedException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteAbortException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteAccessPermException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteBlobTooBigException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteCantOpenDatabaseException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteClosable.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteConnection.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteConnectionPool.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteConstraintException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteCustomFunction.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDatabaseConfiguration.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDatabaseCorruptException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDatabaseLockedException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDatatypeMismatchException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDebug.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDiskIOException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteDoneException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteFullException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteGlobal.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteMisuseException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteOutOfMemoryException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteQueryBuilder.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteReadOnlyDatabaseException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteSession.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteStatement.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteStatementInfo.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteTableLockedException.java create mode 100644 src/api-impl/android/database/sqlite/SQLiteTransactionListener.java create mode 100644 src/api-impl/android/os/OperationCanceledException.java create mode 100644 src/api-impl/android/util/LruCache.java create mode 100644 src/api-impl/android/util/Pair.java diff --git a/src/api-impl-jni/generated_headers/android_database_CursorWindow.h b/src/api-impl-jni/generated_headers/android_database_CursorWindow.h new file mode 100644 index 00000000..1a505e5c --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_database_CursorWindow.h @@ -0,0 +1,165 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_database_CursorWindow */ + +#ifndef _Included_android_database_CursorWindow +#define _Included_android_database_CursorWindow +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_database_CursorWindow + * Method: nativeCreate + * Signature: (Ljava/lang/String;I)J + */ +JNIEXPORT jlong JNICALL Java_android_database_CursorWindow_nativeCreate + (JNIEnv *, jclass, jstring, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeDispose + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_database_CursorWindow_nativeDispose + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetName + * Signature: (J)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_android_database_CursorWindow_nativeGetName + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetBlob + * Signature: (JII)[B + */ +JNIEXPORT jbyteArray JNICALL Java_android_database_CursorWindow_nativeGetBlob + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetString + * Signature: (JII)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_android_database_CursorWindow_nativeGetString + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeCopyStringToBuffer + * Signature: (JIILandroid/database/CharArrayBuffer;)V + */ +JNIEXPORT void JNICALL Java_android_database_CursorWindow_nativeCopyStringToBuffer + (JNIEnv *, jclass, jlong, jint, jint, jobject); + +/* + * Class: android_database_CursorWindow + * Method: nativePutBlob + * Signature: (J[BII)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativePutBlob + (JNIEnv *, jclass, jlong, jbyteArray, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativePutString + * Signature: (JLjava/lang/String;II)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativePutString + (JNIEnv *, jclass, jlong, jstring, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeClear + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_database_CursorWindow_nativeClear + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetNumRows + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_android_database_CursorWindow_nativeGetNumRows + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeSetNumColumns + * Signature: (JI)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativeSetNumColumns + (JNIEnv *, jclass, jlong, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeAllocRow + * Signature: (J)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativeAllocRow + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeFreeLastRow + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_database_CursorWindow_nativeFreeLastRow + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetType + * Signature: (JII)I + */ +JNIEXPORT jint JNICALL Java_android_database_CursorWindow_nativeGetType + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetLong + * Signature: (JII)J + */ +JNIEXPORT jlong JNICALL Java_android_database_CursorWindow_nativeGetLong + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativeGetDouble + * Signature: (JII)D + */ +JNIEXPORT jdouble JNICALL Java_android_database_CursorWindow_nativeGetDouble + (JNIEnv *, jclass, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativePutLong + * Signature: (JJII)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativePutLong + (JNIEnv *, jclass, jlong, jlong, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativePutDouble + * Signature: (JDII)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativePutDouble + (JNIEnv *, jclass, jlong, jdouble, jint, jint); + +/* + * Class: android_database_CursorWindow + * Method: nativePutNull + * Signature: (JII)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_CursorWindow_nativePutNull + (JNIEnv *, jclass, jlong, jint, jint); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteConnection.h b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteConnection.h new file mode 100644 index 00000000..b0a7b40f --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteConnection.h @@ -0,0 +1,231 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_database_sqlite_SQLiteConnection */ + +#ifndef _Included_android_database_sqlite_SQLiteConnection +#define _Included_android_database_sqlite_SQLiteConnection +#ifdef __cplusplus +extern "C" { +#endif +#undef android_database_sqlite_SQLiteConnection_DEBUG +#define android_database_sqlite_SQLiteConnection_DEBUG 0L +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeOpen + * Signature: (Ljava/lang/String;ILjava/lang/String;ZZ)J + */ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeOpen + (JNIEnv *, jclass, jstring, jint, jstring, jboolean, jboolean); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeClose + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeClose + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeRegisterCustomFunction + * Signature: (JLandroid/database/sqlite/SQLiteCustomFunction;)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeRegisterCustomFunction + (JNIEnv *, jclass, jlong, jobject); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeRegisterLocalizedCollators + * Signature: (JLjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeRegisterLocalizedCollators + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativePrepareStatement + * Signature: (JLjava/lang/String;)J + */ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativePrepareStatement + (JNIEnv *, jclass, jlong, jstring); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeFinalizeStatement + * Signature: (JJ)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeFinalizeStatement + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeGetParameterCount + * Signature: (JJ)I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetParameterCount + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeIsReadOnly + * Signature: (JJ)Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_sqlite_SQLiteConnection_nativeIsReadOnly + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeGetColumnCount + * Signature: (JJ)I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetColumnCount + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeGetColumnName + * Signature: (JJI)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetColumnName + (JNIEnv *, jclass, jlong, jlong, jint); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeBindNull + * Signature: (JJI)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindNull + (JNIEnv *, jclass, jlong, jlong, jint); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeBindLong + * Signature: (JJIJ)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindLong + (JNIEnv *, jclass, jlong, jlong, jint, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeBindDouble + * Signature: (JJID)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindDouble + (JNIEnv *, jclass, jlong, jlong, jint, jdouble); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeBindString + * Signature: (JJILjava/lang/String;)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindString + (JNIEnv *, jclass, jlong, jlong, jint, jstring); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeBindBlob + * Signature: (JJI[B)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeBindBlob + (JNIEnv *, jclass, jlong, jlong, jint, jbyteArray); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeResetStatementAndClearBindings + * Signature: (JJ)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeResetStatementAndClearBindings + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecute + * Signature: (JJ)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecute + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForLong + * Signature: (JJ)J + */ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForLong + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForString + * Signature: (JJ)Ljava/lang/String; + */ +JNIEXPORT jstring JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForString + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForBlobFileDescriptor + * Signature: (JJ)I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForBlobFileDescriptor + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForChangedRowCount + * Signature: (JJ)I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForChangedRowCount + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForLastInsertedRowId + * Signature: (JJ)J + */ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForLastInsertedRowId + (JNIEnv *, jclass, jlong, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeExecuteForCursorWindow + * Signature: (JJLandroid/database/CursorWindow;IIZ)J + */ +JNIEXPORT jlong JNICALL Java_android_database_sqlite_SQLiteConnection_nativeExecuteForCursorWindow + (JNIEnv *, jclass, jlong, jlong, jobject, jint, jint, jboolean); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeGetDbLookaside + * Signature: (J)I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteConnection_nativeGetDbLookaside + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeCancel + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeCancel + (JNIEnv *, jclass, jlong); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeResetCancel + * Signature: (JZ)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteConnection_nativeResetCancel + (JNIEnv *, jclass, jlong, jboolean); + +/* + * Class: android_database_sqlite_SQLiteConnection + * Method: nativeHasCodec + * Signature: ()Z + */ +JNIEXPORT jboolean JNICALL Java_android_database_sqlite_SQLiteConnection_nativeHasCodec + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteDebug.h b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteDebug.h new file mode 100644 index 00000000..6274a405 --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteDebug.h @@ -0,0 +1,23 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_database_sqlite_SQLiteDebug */ + +#ifndef _Included_android_database_sqlite_SQLiteDebug +#define _Included_android_database_sqlite_SQLiteDebug +#ifdef __cplusplus +extern "C" { +#endif +#undef android_database_sqlite_SQLiteDebug_DEBUG_LOG_SLOW_QUERIES +#define android_database_sqlite_SQLiteDebug_DEBUG_LOG_SLOW_QUERIES 0L +/* + * Class: android_database_sqlite_SQLiteDebug + * Method: nativeGetPagerStats + * Signature: (Landroid/database/sqlite/SQLiteDebug/PagerStats;)V + */ +JNIEXPORT void JNICALL Java_android_database_sqlite_SQLiteDebug_nativeGetPagerStats + (JNIEnv *, jclass, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteGlobal.h b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteGlobal.h new file mode 100644 index 00000000..f6d63329 --- /dev/null +++ b/src/api-impl-jni/generated_headers/android_database_sqlite_SQLiteGlobal.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class android_database_sqlite_SQLiteGlobal */ + +#ifndef _Included_android_database_sqlite_SQLiteGlobal +#define _Included_android_database_sqlite_SQLiteGlobal +#ifdef __cplusplus +extern "C" { +#endif +/* + * Class: android_database_sqlite_SQLiteGlobal + * Method: nativeReleaseMemory + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_android_database_sqlite_SQLiteGlobal_nativeReleaseMemory + (JNIEnv *, jclass); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/api-impl/android/content/ContentResolver.java b/src/api-impl/android/content/ContentResolver.java index 309a6a3d..b46a8d42 100644 --- a/src/api-impl/android/content/ContentResolver.java +++ b/src/api-impl/android/content/ContentResolver.java @@ -6,4 +6,13 @@ import android.net.Uri; public class ContentResolver { public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer) { } + public final void unregisterContentObserver(ContentObserver observer) { + } + public void notifyChange(Uri uri, ContentObserver observer) { + } + public int getUserId() { + return 0; + } + public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer, int userHandle) { + } } diff --git a/src/api-impl/android/content/Context.java b/src/api-impl/android/content/Context.java index 62b7b635..6bfde6e1 100644 --- a/src/api-impl/android/content/Context.java +++ b/src/api-impl/android/content/Context.java @@ -360,4 +360,11 @@ public class Context extends Object { } public boolean isRestricted() {return false;} + + public File getDatabasePath(String dbName) { + File databaseDir = new File(getDataDirFile(), "databases"); + if (!databaseDir.exists()) + databaseDir.mkdirs(); + return new File(databaseDir, dbName); + } } diff --git a/src/api-impl/android/database/AbstractCursor.java b/src/api-impl/android/database/AbstractCursor.java new file mode 100644 index 00000000..4c6c467b --- /dev/null +++ b/src/api-impl/android/database/AbstractCursor.java @@ -0,0 +1,484 @@ +/* + * 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; + +import android.content.ContentResolver; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import dalvik.system.CloseGuard; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * This is an abstract cursor class that handles a lot of the common code + * that all cursors need to deal with and is provided for convenience reasons. + */ +public abstract class AbstractCursor implements CrossProcessCursor { + private static final String TAG = "Cursor"; + /** + * @removed This field should not be used. + */ + protected HashMap> mUpdatedRows; + /** + * @removed This field should not be used. + */ + protected int mRowIdColumnIndex; + /** + * @removed This field should not be used. + */ + protected Long mCurrentRowID; + /** + * @deprecated Use {@link #getPosition()} instead. + */ + @Deprecated + protected int mPos; + /** + * @deprecated Use {@link #isClosed()} instead. + */ + @Deprecated + protected boolean mClosed; + /** + * @deprecated Do not use. + */ + @Deprecated + protected ContentResolver mContentResolver; + + private Uri mNotifyUri; + private List mNotifyUris; + private final Object mSelfObserverLock = new Object(); + private ContentObserver mSelfObserver; + private boolean mSelfObserverRegistered; + private final DataSetObservable mDataSetObservable = new DataSetObservable(); + private final ContentObservable mContentObservable = new ContentObservable(); + private Bundle mExtras = Bundle.EMPTY; + /** CloseGuard to detect leaked cursor **/ + private final CloseGuard mCloseGuard = CloseGuard.get(); + /* -------------------------------------------------------- */ + /* These need to be implemented by subclasses */ + @Override + abstract public int getCount(); + @Override + abstract public String[] getColumnNames(); + @Override + abstract public String getString(int column); + @Override + abstract public short getShort(int column); + @Override + abstract public int getInt(int column); + @Override + abstract public long getLong(int column); + @Override + abstract public float getFloat(int column); + @Override + abstract public double getDouble(int column); + @Override + abstract public boolean isNull(int column); + @Override + public int getType(int column) { + // Reflects the assumption that all commonly used field types (meaning everything + // but blobs) are convertible to strings so it should be safe to call + // getString to retrieve them. + return FIELD_TYPE_STRING; + } + // TODO implement getBlob in all cursor types + @Override + public byte[] getBlob(int column) { + throw new UnsupportedOperationException("getBlob is not supported"); + } + /* -------------------------------------------------------- */ + /* 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. + */ + @Override + public CursorWindow getWindow() { + return null; + } + @Override + public int getColumnCount() { + return getColumnNames().length; + } + @Override + public void deactivate() { + onDeactivateOrClose(); + } + /** @hide */ + protected void onDeactivateOrClose() { + if (mSelfObserver != null) { + mContentResolver.unregisterContentObserver(mSelfObserver); + mSelfObserverRegistered = false; + } + mDataSetObservable.notifyInvalidated(); + } + @Override + public boolean requery() { + if (mSelfObserver != null && mSelfObserverRegistered == false) { + final int size = mNotifyUris.size(); + for (int i = 0; i < size; ++i) { + final Uri notifyUri = mNotifyUris.get(i); + mContentResolver.registerContentObserver(notifyUri, true, mSelfObserver); + } + mSelfObserverRegistered = true; + } + mDataSetObservable.notifyChanged(); + return true; + } + @Override + public boolean isClosed() { + return mClosed; + } + @Override + public void close() { + mClosed = true; + mContentObservable.unregisterAll(); + onDeactivateOrClose(); + mCloseGuard.close(); + } + /** + * This function is called every time the cursor is successfully scrolled + * to a new position, giving the subclass a chance to update any state it + * may have. If it returns false the move function will also do so and the + * cursor will scroll to the beforeFirst position. + * + * @param oldPosition the position that we're moving from + * @param newPosition the position that we're moving to + * @return true if the move is successful, false otherwise + */ + @Override + public boolean onMove(int oldPosition, int newPosition) { + return true; + } + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + // Default implementation, uses getString + String result = getString(columnIndex); + if (result != null) { + char[] data = buffer.data; + if (data == null || data.length < result.length()) { + buffer.data = result.toCharArray(); + } else { + result.getChars(0, result.length(), data, 0); + } + buffer.sizeCopied = result.length(); + } else { + buffer.sizeCopied = 0; + } + } + /* -------------------------------------------------------- */ + /* Implementation */ + public AbstractCursor() { + mPos = -1; + mCloseGuard.open("AbstractCursor.close"); + } + @Override + public final int getPosition() { + return mPos; + } + @Override + public final boolean moveToPosition(int position) { + // Make sure position isn't past the end of the cursor + final int count = getCount(); + if (position >= count) { + mPos = count; + return false; + } + // Make sure position isn't before the beginning of the cursor + if (position < 0) { + mPos = -1; + return false; + } + // Check for no-op moves, and skip the rest of the work for them + if (position == mPos) { + return true; + } + boolean result = onMove(mPos, position); + if (result == false) { + mPos = -1; + } else { + mPos = position; + } + return result; + } + @Override + public void fillWindow(int position, CursorWindow window) { + DatabaseUtils.cursorFillWindow(this, position, window); + } + @Override + public final boolean move(int offset) { + return moveToPosition(mPos + offset); + } + @Override + public final boolean moveToFirst() { + return moveToPosition(0); + } + @Override + public final boolean moveToLast() { + return moveToPosition(getCount() - 1); + } + @Override + public final boolean moveToNext() { + return moveToPosition(mPos + 1); + } + @Override + public final boolean moveToPrevious() { + return moveToPosition(mPos - 1); + } + @Override + public final boolean isFirst() { + return mPos == 0 && getCount() != 0; + } + @Override + public final boolean isLast() { + int cnt = getCount(); + return mPos == (cnt - 1) && cnt != 0; + } + @Override + public final boolean isBeforeFirst() { + if (getCount() == 0) { + return true; + } + return mPos == -1; + } + @Override + public final boolean isAfterLast() { + if (getCount() == 0) { + return true; + } + return mPos == getCount(); + } + @Override + public int getColumnIndex(String columnName) { + // 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); + } + String columnNames[] = getColumnNames(); + int length = columnNames.length; + for (int i = 0; i < length; i++) { + if (columnNames[i].equalsIgnoreCase(columnName)) { + return i; + } + } + if (false) { + if (getCount() > 0) { + Log.w("AbstractCursor", "Unknown column " + columnName); + } + } + return -1; + } + @Override + public int getColumnIndexOrThrow(String columnName) { + final int index = getColumnIndex(columnName); + if (index < 0) { + String availableColumns = ""; + try { + availableColumns = Arrays.toString(getColumnNames()); + } catch (Exception e) { + Log.d(TAG, "Cannot collect column names for debug purposes", e); + } + throw new IllegalArgumentException("column '" + columnName + + "' does not exist. Available columns: " + availableColumns); + } + return index; + } + @Override + public String getColumnName(int columnIndex) { + return getColumnNames()[columnIndex]; + } + @Override + public void registerContentObserver(ContentObserver observer) { + mContentObservable.registerObserver(observer); + } + @Override + public void unregisterContentObserver(ContentObserver observer) { + // cursor will unregister all observers when it close + if (!mClosed) { + mContentObservable.unregisterObserver(observer); + } + } + @Override + public void registerDataSetObserver(DataSetObserver observer) { + mDataSetObservable.registerObserver(observer); + } + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + mDataSetObservable.unregisterObserver(observer); + } + /** + * Subclasses must call this method when they finish committing updates to notify all + * observers. + * + * @param selfChange + */ + protected void onChange(boolean selfChange) { + synchronized (mSelfObserverLock) { + mContentObservable.dispatchChange(selfChange, null); + if (mNotifyUris != null && selfChange) { + final int size = mNotifyUris.size(); + for (int i = 0; i < size; ++i) { + final Uri notifyUri = mNotifyUris.get(i); + mContentResolver.notifyChange(notifyUri, mSelfObserver); + } + } + } + } + /** + * Specifies a content URI to watch for changes. + * + * @param cr The content resolver from the caller's context. + * @param notifyUri The URI to watch for changes. This can be a + * specific row URI, or a base URI for a whole class of content. + */ + @Override + public void setNotificationUri(ContentResolver cr, Uri notifyUri) { + setNotificationUris(cr, Arrays.asList(notifyUri)); + } + // @Override + public void setNotificationUris(ContentResolver cr, List notifyUris) { + Objects.requireNonNull(cr); + Objects.requireNonNull(notifyUris); + setNotificationUris(cr, notifyUris, cr.getUserId(), true); + } + /** + * Set the notification uri but with an observer for a particular user's view. Also allows + * disabling the use of a self observer, which is sensible if either + * a) the cursor's owner calls {@link #onChange(boolean)} whenever the content changes, or + * b) the cursor is known not to have any content observers. + * @hide + */ + public void setNotificationUris(ContentResolver cr, List notifyUris, int userHandle, + boolean registerSelfObserver) { + synchronized (mSelfObserverLock) { + mNotifyUris = notifyUris; + mNotifyUri = mNotifyUris.get(0); + mContentResolver = cr; + if (mSelfObserver != null) { + mContentResolver.unregisterContentObserver(mSelfObserver); + mSelfObserverRegistered = false; + } + if (registerSelfObserver) { + mSelfObserver = new SelfContentObserver(this); + final int size = mNotifyUris.size(); + for (int i = 0; i < size; ++i) { + final Uri notifyUri = mNotifyUris.get(i); + mContentResolver.registerContentObserver( + notifyUri, true, mSelfObserver, userHandle); + } + mSelfObserverRegistered = true; + } + } + } + @Override + public Uri getNotificationUri() { + synchronized (mSelfObserverLock) { + return mNotifyUri; + } + } + // @Override + public List getNotificationUris() { + synchronized (mSelfObserverLock) { + return mNotifyUris; + } + } + @Override + public boolean getWantsAllOnMoveCalls() { + return false; + } + // @Override + public void setExtras(Bundle extras) { + mExtras = (extras == null) ? Bundle.EMPTY : extras; + } + @Override + public Bundle getExtras() { + return mExtras; + } + @Override + public Bundle respond(Bundle extras) { + return Bundle.EMPTY; + } + /** + * @deprecated Always returns false since Cursors do not support updating rows + */ + @Deprecated + protected boolean isFieldUpdated(int columnIndex) { + return false; + } + /** + * @deprecated Always returns null since Cursors do not support updating rows + */ + @Deprecated + protected Object getUpdatedField(int columnIndex) { + return null; + } + /** + * This function throws CursorIndexOutOfBoundsException if + * the cursor position is out of bounds. Subclass implementations of + * the get functions should call this before attempting + * to retrieve data. + * + * @throws CursorIndexOutOfBoundsException + */ + protected void checkPosition() { + if (-1 == mPos || getCount() == mPos) { + // throw new CursorIndexOutOfBoundsException(mPos, getCount()); + throw new IndexOutOfBoundsException("index = " + mPos + " count = " + getCount()); + } + } + @Override + protected void finalize() { + if (mSelfObserver != null && mSelfObserverRegistered == true) { + mContentResolver.unregisterContentObserver(mSelfObserver); + } + try { + if (mCloseGuard != null) mCloseGuard.warnIfOpen(); + if (!mClosed) close(); + } catch(Exception e) { } + } + /** + * Cursors use this class to track changes others make to their URI. + */ + protected static class SelfContentObserver extends ContentObserver { + WeakReference mCursor; + public SelfContentObserver(AbstractCursor cursor) { + super(null); + mCursor = new WeakReference(cursor); + } + @Override + public boolean deliverSelfNotifications() { + return false; + } + @Override + public void onChange(boolean selfChange) { + AbstractCursor cursor = mCursor.get(); + if (cursor != null) { + cursor.onChange(false); + } + } + } +} \ No newline at end of file diff --git a/src/api-impl/android/database/AbstractWindowedCursor.java b/src/api-impl/android/database/AbstractWindowedCursor.java new file mode 100644 index 00000000..5fe07a81 --- /dev/null +++ b/src/api-impl/android/database/AbstractWindowedCursor.java @@ -0,0 +1,209 @@ +/* + * 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; + +/** + * A base class for Cursors that store their data in {@link CursorWindow}s. + *

+ * The cursor owns the cursor window it uses. When the cursor is closed, + * its window is also closed. Likewise, when the window used by the cursor is + * changed, its old window is closed. This policy of strict ownership ensures + * that cursor windows are not leaked. + *

+ * 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. + *

+ * 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. + *

+ */ +public abstract class AbstractWindowedCursor extends AbstractCursor { + /** + * The cursor window owned by this cursor. + */ + protected CursorWindow mWindow; + @Override + public byte[] getBlob(int columnIndex) { + checkPosition(); + return mWindow.getBlob(mPos, columnIndex); + } + + @Override + public String getString(int columnIndex) { + checkPosition(); + return mWindow.getString(mPos, columnIndex); + } + + @Override + public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { + checkPosition(); + mWindow.copyStringToBuffer(mPos, columnIndex, buffer); + } + + @Override + public short getShort(int columnIndex) { + checkPosition(); + return mWindow.getShort(mPos, columnIndex); + } + + @Override + public int getInt(int columnIndex) { + checkPosition(); + return mWindow.getInt(mPos, columnIndex); + } + + @Override + public long getLong(int columnIndex) { + checkPosition(); + return mWindow.getLong(mPos, columnIndex); + } + + @Override + public float getFloat(int columnIndex) { + checkPosition(); + return mWindow.getFloat(mPos, columnIndex); + } + + @Override + public double getDouble(int columnIndex) { + checkPosition(); + return mWindow.getDouble(mPos, columnIndex); + } + + @Override + public boolean isNull(int columnIndex) { + checkPosition(); + return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL; + } + + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isBlob(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_BLOB; + } + + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isString(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_STRING; + } + + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isLong(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_INTEGER; + } + + /** + * @deprecated Use {@link #getType} + */ + @Deprecated + public boolean isFloat(int columnIndex) { + return getType(columnIndex) == Cursor.FIELD_TYPE_FLOAT; + } + + @Override + public int getType(int columnIndex) { + checkPosition(); + return mWindow.getType(mPos, columnIndex); + } + + @Override + protected void checkPosition() { + super.checkPosition(); + + if (mWindow == null) { + throw new /*StaleDataException*/RuntimeException("Attempting to access a closed CursorWindow." + + "Most probable cause: cursor is deactivated prior to calling this method."); + } + } + + @Override + public CursorWindow getWindow() { + return mWindow; + } + + /** + * Sets a new cursor window for the cursor to use. + *

+ * 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. + *

+ * If the cursor previously had a cursor window, then it is closed when the + * new cursor window is assigned. + *

+ * + * @param window The new cursor window, typically a remote cursor window. + */ + public void setWindow(CursorWindow window) { + if (window != mWindow) { + closeWindow(); + 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 + */ + protected void closeWindow() { + if (mWindow != null) { + mWindow.close(); + mWindow = null; + } + } + + /** + * If there is a window, clear it. + * Otherwise, creates a new window. + * + * @param name The window name. + * @hide + */ + protected void clearOrCreateWindow(String name) { + if (mWindow == null) { + mWindow = new CursorWindow(name); + } else { + mWindow.clear(); + } + } + + /** @hide */ + @Override + protected void onDeactivateOrClose() { + super.onDeactivateOrClose(); + closeWindow(); + } +} \ No newline at end of file diff --git a/src/api-impl/android/database/CharArrayBuffer.java b/src/api-impl/android/database/CharArrayBuffer.java new file mode 100644 index 00000000..5da21032 --- /dev/null +++ b/src/api-impl/android/database/CharArrayBuffer.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2008 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; + +/** + * This is used for {@link Cursor#copyStringToBuffer} + */ +public final class CharArrayBuffer { + public CharArrayBuffer(int size) { + data = new char[size]; + } + + public CharArrayBuffer(char[] buf) { + data = buf; + } + + public char[] data; // In and out parameter + public int sizeCopied; // Out parameter +} diff --git a/src/api-impl/android/database/ContentObservable.java b/src/api-impl/android/database/ContentObservable.java new file mode 100644 index 00000000..463915b4 --- /dev/null +++ b/src/api-impl/android/database/ContentObservable.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2007 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; + +import android.net.Uri; + +/** + * A specialization of {@link Observable} for {@link ContentObserver} + * that provides methods for sending notifications to a list of + * {@link ContentObserver} objects. + */ +public class ContentObservable extends Observable { + // Even though the generic method defined in Observable would be perfectly + // fine on its own, we can't delete this overridden method because it would + // potentially break binary compatibility with existing applications. + @Override + public void registerObserver(ContentObserver observer) { + super.registerObserver(observer); + } + + /** + * Invokes {@link ContentObserver#dispatchChange(boolean)} on each observer. + *

+ * If selfChange is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + *

+ * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange(boolean, Uri)} instead. + */ + @Deprecated + public void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + + /** + * Invokes {@link ContentObserver#dispatchChange(boolean, Uri)} on each observer. + * Includes the changed content Uri when available. + *

+ * If selfChange is true, only delivers the notification + * to the observer if it has indicated that it wants to receive self-change + * notifications by implementing {@link ContentObserver#deliverSelfNotifications} + * to return true. + *

+ * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content, or null if unknown. + */ + public void dispatchChange(boolean selfChange, Uri uri) { + synchronized(mObservers) { + for (ContentObserver observer : mObservers) { + if (!selfChange || observer.deliverSelfNotifications()) { + observer.dispatchChange(selfChange, uri); + } + } + } + } + + /** + * Invokes {@link ContentObserver#onChange} on each observer. + * + * @param selfChange True if this is a self-change notification. + * + * @deprecated Use {@link #dispatchChange} instead. + */ + @Deprecated + public void notifyChange(boolean selfChange) { + synchronized(mObservers) { + for (ContentObserver observer : mObservers) { + observer.onChange(selfChange, null); + } + } + } +} \ No newline at end of file diff --git a/src/api-impl/android/database/ContentObserver.java b/src/api-impl/android/database/ContentObserver.java index b488bc60..b99be4cd 100644 --- a/src/api-impl/android/database/ContentObserver.java +++ b/src/api-impl/android/database/ContentObserver.java @@ -1,11 +1,251 @@ +/* + * Copyright (C) 2007 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; +import android.net.Uri; import android.os.Handler; +import android.os.UserHandle; +import java.util.Arrays; +import java.util.Collection; +/** + * Receives call backs for changes to content. + * Must be implemented by objects which are added to a {@link ContentObservable}. + */ +public abstract class ContentObserver { + /** + * Starting in {@link android.os.Build.VERSION_CODES#R}, there is a new + * public API overload {@link #onChange(boolean, Uri, int)} that delivers a + * {@code int flags} argument. + *

+ * Some apps may be relying on a previous hidden API that delivered a + * {@code int userId} argument, and this change is used to control delivery + * of the new {@code int flags} argument in its place. + */ + private static final long ADD_CONTENT_OBSERVER_FLAGS = 150939131L; + private final Object mLock = new Object(); + Handler mHandler; -public class ContentObserver { - public ContentObserver() { - } - + /** + * Creates a content observer. + * + * @param handler The handler to run {@link #onChange} on, or null if none. + */ public ContentObserver(Handler handler) { + mHandler = handler; } -} + + /** + * Returns true if this observer is interested receiving self-change notifications. + * + * Subclasses should override this method to indicate whether the observer + * is interested in receiving notifications for changes that it made to the + * content itself. + * + * @return True if self-change notifications should be delivered to the observer. + */ + public boolean deliverSelfNotifications() { + return false; + } + /** + * This method is called when a content change occurs. + *

+ * Subclasses should override this method to handle content changes. + *

+ * + * @param selfChange True if this is a self-change notification. + */ + public void onChange(boolean selfChange) { + // Do nothing. Subclass should override. + } + /** + * This method is called when a content change occurs. + * Includes the changed content Uri when available. + *

+ * Subclasses should override this method to handle content changes. To + * ensure correct operation on older versions of the framework that did not + * provide richer arguments, applications should implement all overloads. + *

+ * Example implementation: + *


+	 * // Implement the onChange(boolean) method to delegate the change notification to
+	 * // the onChange(boolean, Uri) method to ensure correct operation on older versions
+	 * // of the framework that did not have the onChange(boolean, Uri) method.
+	 * {@literal @Override}
+	 * public void onChange(boolean selfChange) {
+	 *     onChange(selfChange, null);
+	 * }
+	 *
+	 * // Implement the onChange(boolean, Uri) method to take advantage of the new Uri argument.
+	 * {@literal @Override}
+	 * public void onChange(boolean selfChange, Uri uri) {
+	 *     // Handle change.
+	 * }
+	 * 
+ *

+ * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content. + */ + public void onChange(boolean selfChange, Uri uri) { + onChange(selfChange); + } + /** + * This method is called when a content change occurs. Includes the changed + * content Uri when available. + *

+ * Subclasses should override this method to handle content changes. To + * ensure correct operation on older versions of the framework that did not + * provide richer arguments, applications should implement all overloads. + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content. + * @param flags Flags indicating details about this change. + */ + public void onChange(boolean selfChange, Uri uri, int flags) { + onChange(selfChange, uri); + } + /** + * This method is called when a content change occurs. Includes the changed + * content Uris when available. + *

+ * Subclasses should override this method to handle content changes. To + * ensure correct operation on older versions of the framework that did not + * provide richer arguments, applications should implement all overloads. + * + * @param selfChange True if this is a self-change notification. + * @param uris The Uris of the changed content. + * @param flags Flags indicating details about this change. + */ + public void onChange(boolean selfChange, Collection uris, + int flags) { + for (Uri uri : uris) { + onChange(selfChange, uri, flags); + } + } + /** + * This method is called when a content change occurs. Includes the changed + * content Uris when available. + *

+ * Subclasses should override this method to handle content changes. To + * ensure correct operation on older versions of the framework that did not + * provide richer arguments, applications should implement all overloads. + * + * @param selfChange True if this is a self-change notification. + * @param uris The Uris of the changed content. + * @param flags Flags indicating details about this change. + * @param user The corresponding {@link UserHandle} for the current notification. + * + * @hide + */ + public void onChange(boolean selfChange, Collection uris, + int flags, UserHandle user) { + onChange(selfChange, uris, user.getIdentifier()); + } + /** @hide */ + public void onChange(boolean selfChange, Collection uris, + int flags, int userId) { + // There are dozens of people relying on the hidden API inside the + // system UID, so hard-code the old behavior for all of them; for + // everyone else we gate based on a specific change + // if (!CompatChanges.isChangeEnabled(ADD_CONTENT_OBSERVER_FLAGS) + // || android.os.Process.myUid() == android.os.Process.SYSTEM_UID) { + // // Deliver userId through argument to preserve hidden API behavior + // onChange(selfChange, uris, flags, UserHandle.of(userId)); + // } else { + onChange(selfChange, uris, flags); + // } + } + /** + * Dispatches a change notification to the observer. + *

+ * If a {@link Handler} was supplied to the {@link ContentObserver} + * constructor, then a call to the {@link #onChange} method is posted to the + * handler's message queue. Otherwise, the {@link #onChange} method is + * invoked immediately on this thread. + * + * @deprecated Callers should migrate towards using a richer overload that + * provides more details about the change, such as + * {@link #dispatchChange(boolean, Collection, int)}. + */ + @Deprecated + public final void dispatchChange(boolean selfChange) { + dispatchChange(selfChange, null); + } + /** + * Dispatches a change notification to the observer. Includes the changed + * content Uri when available. + *

+ * If a {@link Handler} was supplied to the {@link ContentObserver} + * constructor, then a call to the {@link #onChange} method is posted to the + * handler's message queue. Otherwise, the {@link #onChange} method is + * invoked immediately on this thread. + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content. + */ + public final void dispatchChange(boolean selfChange, Uri uri) { + dispatchChange(selfChange, uri, 0); + } + /** + * Dispatches a change notification to the observer. Includes the changed + * content Uri when available. + *

+ * If a {@link Handler} was supplied to the {@link ContentObserver} + * constructor, then a call to the {@link #onChange} method is posted to the + * handler's message queue. Otherwise, the {@link #onChange} method is + * invoked immediately on this thread. + * + * @param selfChange True if this is a self-change notification. + * @param uri The Uri of the changed content. + * @param flags Flags indicating details about this change. + */ + public final void dispatchChange(boolean selfChange, Uri uri, + int flags) { + dispatchChange(selfChange, Arrays.asList(uri), flags); + } + /** + * Dispatches a change notification to the observer. Includes the changed + * content Uris when available. + *

+ * If a {@link Handler} was supplied to the {@link ContentObserver} + * constructor, then a call to the {@link #onChange} method is posted to the + * handler's message queue. Otherwise, the {@link #onChange} method is + * invoked immediately on this thread. + * + * @param selfChange True if this is a self-change notification. + * @param uris The Uri of the changed content. + * @param flags Flags indicating details about this change. + */ + public final void dispatchChange(boolean selfChange, Collection uris, + int flags) { + dispatchChange(selfChange, uris, flags, UserHandle.getCallingUserId()); + } + /** @hide */ + public final void dispatchChange(final boolean selfChange, final Collection uris, + final int flags, final int userId) { + if (mHandler == null) { + onChange(selfChange, uris, flags, userId); + } else { + mHandler.post(new Runnable(){ + @Override + public void run() { + onChange(selfChange, uris, flags, userId); + } + }); + } + } +} \ No newline at end of file diff --git a/src/api-impl/android/database/CrossProcessCursor.java b/src/api-impl/android/database/CrossProcessCursor.java new file mode 100644 index 00000000..28d49014 --- /dev/null +++ b/src/api-impl/android/database/CrossProcessCursor.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2008 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; + +/** + * A cross process cursor is an extension of a {@link Cursor} that also supports + * usage from remote processes. + *

+ * The contents of a cross process cursor are marshalled to the remote process by + * filling {@link CursorWindow} objects using {@link #fillWindow}. As an optimization, + * the cursor can provide a pre-filled window to use via {@link #getWindow} thereby + * obviating the need to copy the data to yet another cursor window. + */ +public interface CrossProcessCursor extends Cursor { + /** + * Returns a pre-filled window that contains the data within this cursor. + *

+ * In particular, the window contains the row indicated by {@link Cursor#getPosition}. + * The window's contents are automatically scrolled whenever the current + * row moved outside the range covered by the window. + *

+ * + * @return The pre-filled window, or null if none. + */ + CursorWindow getWindow(); + + /** + * Copies cursor data into the window. + *

+ * Clears the window and fills it with data beginning at the requested + * row position until all of the data in the cursor is exhausted + * or the window runs out of space. + *

+ * The filled window uses the same row indices as the original cursor. + * For example, if you fill a window starting from row 5 from the cursor, + * you can query the contents of row 5 from the window just by asking it + * for row 5 because there is a direct correspondence between the row indices + * used by the cursor and the window. + *

+ * The current position of the cursor, as returned by {@link #getPosition}, + * is not changed by this method. + *

+ * + * @param position The zero-based index of the first row to copy into the window. + * @param window The window to fill. + */ + void fillWindow(int position, CursorWindow window); + + /** + * This function is called every time the cursor is successfully scrolled + * to a new position, giving the subclass a chance to update any state it + * may have. If it returns false the move function will also do so and the + * cursor will scroll to the beforeFirst position. + *

+ * This function should be called by methods such as {@link #moveToPosition(int)}, + * so it will typically not be called from outside of the cursor class itself. + *

+ * + * @param oldPosition The position that we're moving from. + * @param newPosition The position that we're moving to. + * @return True if the move is successful, false otherwise. + */ + boolean onMove(int oldPosition, int newPosition); +} \ No newline at end of file diff --git a/src/api-impl/android/database/Cursor.java b/src/api-impl/android/database/Cursor.java index 0a370d28..060874ca 100644 --- a/src/api-impl/android/database/Cursor.java +++ b/src/api-impl/android/database/Cursor.java @@ -17,13 +17,10 @@ package android.database; import android.content.ContentResolver; -import android.database.ContentObserver; import android.net.Uri; import android.os.Bundle; import java.io.Closeable; -class CharArrayBuffer {} - /** * This interface provides random read-write access to the result set returned * by a database query. @@ -80,7 +77,7 @@ public interface Cursor extends Closeable { * * @return the current cursor position. */ - // int getPosition(); + int getPosition(); /** * Move the cursor by a relative amount, forward or backward, from the @@ -97,7 +94,7 @@ public interface Cursor extends Closeable { * @param offset the offset to be applied from the current position. * @return whether the requested move fully succeeded. */ - // boolean move(int offset); + boolean move(int offset); /** * Move the cursor to an absolute position. The valid @@ -109,7 +106,7 @@ public interface Cursor extends Closeable { * @param position the zero-based position to move to. * @return whether the requested move fully succeeded. */ - // boolean moveToPosition(int position); + boolean moveToPosition(int position); /** * Move the cursor to the first row. @@ -127,7 +124,7 @@ public interface Cursor extends Closeable { * * @return whether the move succeeded. */ - // boolean moveToLast(); + boolean moveToLast(); /** * Move the cursor to the next row. @@ -147,21 +144,21 @@ public interface Cursor extends Closeable { * * @return whether the move succeeded. */ - // boolean moveToPrevious(); + boolean moveToPrevious(); /** * Returns whether the cursor is pointing to the first row. * * @return whether the cursor is pointing at the first entry. */ - // boolean isFirst(); + boolean isFirst(); /** * Returns whether the cursor is pointing to the last row. * * @return whether the cursor is pointing at the last entry. */ - // boolean isLast(); + boolean isLast(); /** * Returns whether the cursor is pointing to the position before the first @@ -169,7 +166,7 @@ public interface Cursor extends Closeable { * * @return whether the cursor is before the first result. */ - // boolean isBeforeFirst(); + boolean isBeforeFirst(); /** * Returns whether the cursor is pointing to the position after the last @@ -202,7 +199,7 @@ public interface Cursor extends Closeable { * @see #getColumnIndex(String) * @throws IllegalArgumentException if the column does not exist */ - // int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException; + int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException; /** * Returns the column name at the given zero-based column index. @@ -210,7 +207,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return the column name for the given column index. */ - // String getColumnName(int columnIndex); + String getColumnName(int columnIndex); /** * Returns a string array holding the names of all of the columns in the @@ -218,13 +215,13 @@ public interface Cursor extends Closeable { * * @return the names of the columns returned in this query. */ - // String[] getColumnNames(); + String[] getColumnNames(); /** * Return total number of columns * @return number of columns */ - // int getColumnCount(); + int getColumnCount(); /** * Returns the value of the requested column as a byte array. @@ -236,7 +233,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return the value of that column as a byte array. */ - // byte[] getBlob(int columnIndex); + byte[] getBlob(int columnIndex); /** * Returns the value of the requested column as a String. @@ -258,7 +255,7 @@ public interface Cursor extends Closeable { * if the target column is null, return buffer * @param buffer the buffer to copy the text into. */ - // void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer); + void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer); /** * Returns the value of the requested column as a short. @@ -271,7 +268,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return the value of that column as a short. */ - // short getShort(int columnIndex); + short getShort(int columnIndex); /** * Returns the value of the requested column as an int. @@ -310,7 +307,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return the value of that column as a float. */ - // float getFloat(int columnIndex); + float getFloat(int columnIndex); /** * Returns the value of the requested column as a double. @@ -323,7 +320,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return the value of that column as a double. */ - // double getDouble(int columnIndex); + double getDouble(int columnIndex); /** * Returns data type of the given column's value. @@ -344,7 +341,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return column value type */ - // int getType(int columnIndex); + int getType(int columnIndex); /** * Returns true if the value in the indicated column is null. @@ -352,7 +349,7 @@ public interface Cursor extends Closeable { * @param columnIndex the zero-based index of the target column. * @return whether the column value is null. */ - // boolean isNull(int columnIndex); + boolean isNull(int columnIndex); /** * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called. @@ -390,7 +387,7 @@ public interface Cursor extends Closeable { * return true if the cursor is closed * @return true if the cursor is closed. */ - // boolean isClosed(); + boolean isClosed(); /** * Register an observer that is called when changes happen to the content backing this cursor. @@ -399,7 +396,7 @@ public interface Cursor extends Closeable { * @param observer the object that gets notified when the content backing the cursor changes. * @see #unregisterContentObserver(ContentObserver) */ - // void registerContentObserver(ContentObserver observer); + void registerContentObserver(ContentObserver observer); /** * Unregister an observer that has previously been registered with this @@ -408,7 +405,7 @@ public interface Cursor extends Closeable { * @param observer the object to unregister. * @see #registerContentObserver(ContentObserver) */ - // void unregisterContentObserver(ContentObserver observer); + void unregisterContentObserver(ContentObserver observer); /** * Register an observer that is called when changes happen to the contents @@ -418,7 +415,7 @@ public interface Cursor extends Closeable { * @param observer the object that gets notified when the cursors data set changes. * @see #unregisterDataSetObserver(DataSetObserver) */ - // void registerDataSetObserver(DataSetObserver observer); + void registerDataSetObserver(DataSetObserver observer); /** * Unregister an observer that has previously been registered with this @@ -427,7 +424,7 @@ public interface Cursor extends Closeable { * @param observer the object to unregister. * @see #registerDataSetObserver(DataSetObserver) */ - // void unregisterDataSetObserver(DataSetObserver observer); + void unregisterDataSetObserver(DataSetObserver observer); /** * Register to watch a content URI for changes. This can be the URI of a specific data row (for @@ -437,7 +434,7 @@ public interface Cursor extends Closeable { * this resolver will be notified. * @param uri The content URI to watch. */ - // void setNotificationUri(ContentResolver cr, Uri uri); + void setNotificationUri(ContentResolver cr, Uri uri); /** * Return the URI at which notifications of changes in this Cursor's data @@ -447,13 +444,13 @@ public interface Cursor extends Closeable { * ContentResolver.registerContentObserver} to find out about changes to this Cursor's * data. May be null if no notification URI has been set. */ - // Uri getNotificationUri(); + Uri getNotificationUri(); /** * onMove() will only be called across processes if this method returns true. * @return whether all cursor movement should result in a call to onMove(). */ - // boolean getWantsAllOnMoveCalls(); + boolean getWantsAllOnMoveCalls(); /** * Returns a bundle of extra values. This is an optional way for cursors to provide out-of-band @@ -464,7 +461,7 @@ public interface Cursor extends Closeable { * @return cursor-defined values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY} if there * are no values. Never null. */ - // Bundle getExtras(); + Bundle getExtras(); /** * This is an out-of-band way for the the user of a cursor to communicate with the cursor. The @@ -477,5 +474,5 @@ public interface Cursor extends Closeable { * @return extra values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY}. * Never null. */ - // Bundle respond(Bundle extras); + Bundle respond(Bundle extras); } diff --git a/src/api-impl/android/database/CursorWindow.java b/src/api-impl/android/database/CursorWindow.java new file mode 100644 index 00000000..5f6a1687 --- /dev/null +++ b/src/api-impl/android/database/CursorWindow.java @@ -0,0 +1,671 @@ +/* + * 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; + +import android.content.res.Resources; +import android.database.sqlite.SQLiteClosable; +import android.database.sqlite.SQLiteException; +import android.os.Binder; +import android.os.Build; +import android.os.Parcelable; +import android.os.Process; +import android.util.Log; +import android.util.LongSparseArray; +import android.util.SparseIntArray; +import dalvik.annotation.optimization.FastNative; +import dalvik.system.CloseGuard; + +/** + * A buffer containing multiple cursor rows. + *

+ * A {@link CursorWindow} is read-write when initially created and used locally. + * When sent to a remote process (by writing it to a {@link Parcel}), the remote process + * receives a read-only view of the cursor window. Typically the cursor window + * will be allocated by the producer, filled with data, and then sent to the + * consumer for reading. + *

+ */ + +public class CursorWindow extends SQLiteClosable implements Parcelable { + private static final String STATS_TAG = "CursorWindowStats"; + // This static member will be evaluated when first used. + private static int sCursorWindowSize = -1; + /** + * The native CursorWindow object pointer. (FOR INTERNAL USE ONLY) + * @hide + */ + public long mWindowPtr; + private int mStartPos; + private final String mName; + private final CloseGuard mCloseGuard = CloseGuard.get(); + // May throw CursorWindowAllocationException + private static native long nativeCreate(String name, int cursorWindowSize); + // May throw CursorWindowAllocationException + private static native void nativeDispose(long windowPtr); + private static native String nativeGetName(long windowPtr); + private static native byte[] nativeGetBlob(long windowPtr, int row, int column); + private static native String nativeGetString(long windowPtr, int row, int column); + private static native void nativeCopyStringToBuffer(long windowPtr, int row, int column, + CharArrayBuffer buffer); + private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column); + private static native boolean nativePutString(long windowPtr, String value, + int row, int column); + // Below native methods don't do unconstrained work, so are FastNative for performance + @FastNative + private static native void nativeClear(long windowPtr); + @FastNative + private static native int nativeGetNumRows(long windowPtr); + @FastNative + private static native boolean nativeSetNumColumns(long windowPtr, int columnNum); + @FastNative + private static native boolean nativeAllocRow(long windowPtr); + @FastNative + private static native void nativeFreeLastRow(long windowPtr); + @FastNative + private static native int nativeGetType(long windowPtr, int row, int column); + @FastNative + private static native long nativeGetLong(long windowPtr, int row, int column); + @FastNative + private static native double nativeGetDouble(long windowPtr, int row, int column); + @FastNative + private static native boolean nativePutLong(long windowPtr, long value, int row, int column); + @FastNative + private static native boolean nativePutDouble(long windowPtr, double value, int row, int column); + @FastNative + private static native boolean nativePutNull(long windowPtr, int row, int column); + /** + * Creates a new empty cursor window and gives it a name. + *

+ * The cursor initially has no rows or columns. Call {@link #setNumColumns(int)} to + * set the number of columns before adding any rows to the cursor. + *

+ * + * @param name The name of the cursor window, or null if none. + */ + public CursorWindow(String name) { + this(name, getCursorWindowSize()); + } + /** + * Creates a new empty cursor window and gives it a name. + *

+ * The cursor initially has no rows or columns. Call {@link #setNumColumns(int)} to + * set the number of columns before adding any rows to the cursor. + *

+ * + * @param name The name of the cursor window, or null if none. + * @param windowSizeBytes Size of cursor window in bytes. + * @throws IllegalArgumentException if {@code windowSizeBytes} is less than 0 + * @throws AssertionError if created window pointer is 0 + *

Note: Memory is dynamically allocated as data rows are added to the + * window. Depending on the amount of data stored, the actual amount of memory allocated can be + * lower than specified size, but cannot exceed it. + */ + public CursorWindow(String name, long windowSizeBytes) { + if (windowSizeBytes < 0) { + throw new IllegalArgumentException("Window size cannot be less than 0"); + } + mStartPos = 0; + mName = name != null && name.length() != 0 ? name : ""; + mWindowPtr = nativeCreate(mName, (int) windowSizeBytes); + if (mWindowPtr == 0) { + throw new AssertionError(); // Not possible, the native code won't return it. + } + mCloseGuard.open("CursorWindow.close"); + } + /** + * Creates a new empty cursor window. + *

+ * The cursor initially has no rows or columns. Call {@link #setNumColumns(int)} to + * set the number of columns before adding any rows to the cursor. + *

+ * + * @param localWindow True if this window will be used in this process only, + * false if it might be sent to another processes. This argument is ignored. + * + * @deprecated There is no longer a distinction between local and remote + * cursor windows. Use the {@link #CursorWindow(String)} constructor instead. + */ + @Deprecated + public CursorWindow(boolean localWindow) { + this((String)null); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + dispose(); + } finally { + super.finalize(); + } + } + private void dispose() { + if (mCloseGuard != null) { + mCloseGuard.close(); + } + if (mWindowPtr != 0) { + nativeDispose(mWindowPtr); + mWindowPtr = 0; + } + } + /** + * Gets the name of this cursor window, never null. + * @hide + */ + public String getName() { + return mName; + } + /** + * Clears out the existing contents of the window, making it safe to reuse + * for new data. + *

+ * The start position ({@link #getStartPosition()}), number of rows ({@link #getNumRows()}), + * and number of columns in the cursor are all reset to zero. + *

+ */ + public void clear() { + acquireReference(); + try { + mStartPos = 0; + nativeClear(mWindowPtr); + } finally { + releaseReference(); + } + } + /** + * Gets the start position of this cursor window. + *

+ * The start position is the zero-based index of the first row that this window contains + * relative to the entire result set of the {@link Cursor}. + *

+ * + * @return The zero-based start position. + */ + public int getStartPosition() { + return mStartPos; + } + /** + * Sets the start position of this cursor window. + *

+ * The start position is the zero-based index of the first row that this window contains + * relative to the entire result set of the {@link Cursor}. + *

+ * + * @param pos The new zero-based start position. + */ + public void setStartPosition(int pos) { + mStartPos = pos; + } + /** + * Gets the number of rows in this window. + * + * @return The number of rows in this cursor window. + */ + public int getNumRows() { + acquireReference(); + try { + return nativeGetNumRows(mWindowPtr); + } finally { + releaseReference(); + } + } + /** + * Sets the number of columns in this window. + *

+ * This method must be called before any rows are added to the window, otherwise + * it will fail to set the number of columns if it differs from the current number + * of columns. + *

+ * + * @param columnNum The new number of columns. + * @return True if successful. + */ + public boolean setNumColumns(int columnNum) { + acquireReference(); + try { + return nativeSetNumColumns(mWindowPtr, columnNum); + } finally { + releaseReference(); + } + } + /** + * Allocates a new row at the end of this cursor window. + * + * @return True if successful, false if the cursor window is out of memory. + */ + public boolean allocRow(){ + acquireReference(); + try { + return nativeAllocRow(mWindowPtr); + } finally { + releaseReference(); + } + } + /** + * Frees the last row in this cursor window. + */ + public void freeLastRow(){ + acquireReference(); + try { + nativeFreeLastRow(mWindowPtr); + } finally { + releaseReference(); + } + } + /** + * Returns true if the field at the specified row and column index + * has type {@link Cursor#FIELD_TYPE_NULL}. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if the field has type {@link Cursor#FIELD_TYPE_NULL}. + * @deprecated Use {@link #getType(int, int)} instead. + */ + @Deprecated + public boolean isNull(int row, int column) { + return getType(row, column) == Cursor.FIELD_TYPE_NULL; + } + /** + * Returns true if the field at the specified row and column index + * has type {@link Cursor#FIELD_TYPE_BLOB} or {@link Cursor#FIELD_TYPE_NULL}. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if the field has type {@link Cursor#FIELD_TYPE_BLOB} or + * {@link Cursor#FIELD_TYPE_NULL}. + * @deprecated Use {@link #getType(int, int)} instead. + */ + @Deprecated + public boolean isBlob(int row, int column) { + int type = getType(row, column); + return type == Cursor.FIELD_TYPE_BLOB || type == Cursor.FIELD_TYPE_NULL; + } + /** + * Returns true if the field at the specified row and column index + * has type {@link Cursor#FIELD_TYPE_INTEGER}. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if the field has type {@link Cursor#FIELD_TYPE_INTEGER}. + * @deprecated Use {@link #getType(int, int)} instead. + */ + @Deprecated + public boolean isLong(int row, int column) { + return getType(row, column) == Cursor.FIELD_TYPE_INTEGER; + } + /** + * Returns true if the field at the specified row and column index + * has type {@link Cursor#FIELD_TYPE_FLOAT}. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if the field has type {@link Cursor#FIELD_TYPE_FLOAT}. + * @deprecated Use {@link #getType(int, int)} instead. + */ + @Deprecated + public boolean isFloat(int row, int column) { + return getType(row, column) == Cursor.FIELD_TYPE_FLOAT; + } + /** + * Returns true if the field at the specified row and column index + * has type {@link Cursor#FIELD_TYPE_STRING} or {@link Cursor#FIELD_TYPE_NULL}. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if the field has type {@link Cursor#FIELD_TYPE_STRING} + * or {@link Cursor#FIELD_TYPE_NULL}. + * @deprecated Use {@link #getType(int, int)} instead. + */ + @Deprecated + public boolean isString(int row, int column) { + int type = getType(row, column); + return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL; + } + /** + * Returns the type of the field at the specified row and column index. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The field type. + */ + public int getType(int row, + int column) { + acquireReference(); + try { + return nativeGetType(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Gets the value of the field at the specified row and column index as a byte array. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is null.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then the result + * is the blob value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the array of bytes that make up the internal representation of the + * string value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER} or + * {@link Cursor#FIELD_TYPE_FLOAT}, then a {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a byte array. + */ + public byte[] getBlob(int row, int column) { + acquireReference(); + try { + return nativeGetBlob(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Gets the value of the field at the specified row and column index as a string. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is null.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the string value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is a string representation of the integer in decimal, obtained by formatting the + * value with the printf family of functions using + * format specifier %lld.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is a string representation of the floating-point value in decimal, obtained by + * formatting the value with the printf family of functions using + * format specifier %g.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a string. + */ + public String getString(int row, int column) { + acquireReference(); + try { + return nativeGetString(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Copies the text of the field at the specified row and column index into + * a {@link CharArrayBuffer}. + *

+ * The buffer is populated as follows: + *

    + *
  • If the buffer is too small for the value to be copied, then it is + * automatically resized.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the buffer + * is set to an empty string.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the buffer + * is set to the contents of the string.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the buffer + * is set to a string representation of the integer in decimal, obtained by formatting the + * value with the printf family of functions using + * format specifier %lld.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the buffer is + * set to a string representation of the floating-point value in decimal, obtained by + * formatting the value with the printf family of functions using + * format specifier %g.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @param buffer The {@link CharArrayBuffer} to hold the string. It is automatically + * resized if the requested string is larger than the buffer's current capacity. + */ + public void copyStringToBuffer(int row, int column, + CharArrayBuffer buffer) { + if (buffer == null) { + throw new IllegalArgumentException("CharArrayBuffer should not be null"); + } + acquireReference(); + try { + nativeCopyStringToBuffer(mWindowPtr, row - mStartPos, column, buffer); + } finally { + releaseReference(); + } + } + /** + * Gets the value of the field at the specified row and column index as a long. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is 0L.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the value obtained by parsing the string value with strtoll. + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is the long value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is the floating-point value converted to a long.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a long. + */ + public long getLong(int row, int column) { + acquireReference(); + try { + return nativeGetLong(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Gets the value of the field at the specified row and column index as a + * double. + *

+ * The result is determined as follows: + *

    + *
  • If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result + * is 0.0.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result + * is the value obtained by parsing the string value with strtod. + *
  • If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result + * is the integer value converted to a double.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result + * is the double value.
  • + *
  • If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a + * {@link SQLiteException} is thrown.
  • + *
+ *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a double. + */ + public double getDouble(int row, int column) { + acquireReference(); + try { + return nativeGetDouble(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Gets the value of the field at the specified row and column index as a + * short. + *

+ * The result is determined by invoking {@link #getLong} and converting the + * result to short. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as a short. + */ + public short getShort(int row, int column) { + return (short) getLong(row, column); + } + /** + * Gets the value of the field at the specified row and column index as an + * int. + *

+ * The result is determined by invoking {@link #getLong} and converting the + * result to int. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as an int. + */ + public int getInt(int row, int column) { + return (int) getLong(row, column); + } + /** + * Gets the value of the field at the specified row and column index as a + * float. + *

+ * The result is determined by invoking {@link #getDouble} and converting the + * result to float. + *

+ * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return The value of the field as an float. + */ + public float getFloat(int row, int column) { + return (float) getDouble(row, column); + } + /** + * Copies a byte array into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putBlob(byte[] value, + int row, int column) { + acquireReference(); + try { + return nativePutBlob(mWindowPtr, value, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Copies a string into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putString(String value, + int row, int column) { + acquireReference(); + try { + return nativePutString(mWindowPtr, value, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Puts a long integer into the field at the specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putLong(long value, + int row, int column) { + acquireReference(); + try { + return nativePutLong(mWindowPtr, value, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Puts a double-precision floating point value into the field at the + * specified row and column index. + * + * @param value The value to store. + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putDouble(double value, + int row, int column) { + acquireReference(); + try { + return nativePutDouble(mWindowPtr, value, row - mStartPos, column); + } finally { + releaseReference(); + } + } + /** + * Puts a null value into the field at the specified row and column index. + * + * @param row The zero-based row index. + * @param column The zero-based column index. + * @return True if successful. + */ + public boolean putNull(int row, int column) { + acquireReference(); + try { + return nativePutNull(mWindowPtr, row - mStartPos, column); + } finally { + releaseReference(); + } + } + public int describeContents() { + return 0; + } + @Override + protected void onAllReferencesReleased() { + dispose(); + } + private static int getCursorWindowSize() { + if (sCursorWindowSize < 0) { + // The cursor window size. resource xml file specifies the value in kB. + // convert it to bytes here by multiplying with 1024. + sCursorWindowSize = Resources.getSystem().getInteger( + com.android.internal.R.integer.config_cursorWindowSize) * 1024; + } + return sCursorWindowSize; + } + @Override + public String toString() { + return getName() + " {" + Long.toHexString(mWindowPtr) + "}"; + } +} \ No newline at end of file diff --git a/src/api-impl/android/database/DatabaseUtils.java b/src/api-impl/android/database/DatabaseUtils.java new file mode 100644 index 00000000..d58f6bfa --- /dev/null +++ b/src/api-impl/android/database/DatabaseUtils.java @@ -0,0 +1,1313 @@ +/* + * 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; + +import android.content.ContentValues; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteProgram; +import android.database.sqlite.SQLiteStatement; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; +import java.io.PrintStream; +import java.text.Collator; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/** + * Static utility methods for dealing with databases and {@link Cursor}s. + */ +public class DatabaseUtils { + private static final String TAG = "DatabaseUtils"; + + private static final boolean DEBUG = false; + + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_SELECT = 1; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UPDATE = 2; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ATTACH = 3; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_BEGIN = 4; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_COMMIT = 5; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_ABORT = 6; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_PRAGMA = 7; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_DDL = 8; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_UNPREPARED = 9; + /** One of the values returned by {@link #getSqlStatementType(String)}. */ + public static final int STATEMENT_OTHER = 99; + + /** + * Binds the given Object to the given SQLiteProgram using the proper + * typing. For example, bind numbers as longs/doubles, and everything else + * as a string by call toString() on it. + * + * @param prog the program to bind the object to + * @param index the 1-based index to bind at + * @param value the value to bind + */ + public static void bindObjectToProgram(SQLiteProgram prog, int index, + Object value) { + if (value == null) { + prog.bindNull(index); + } else if (value instanceof Double || value instanceof Float) { + prog.bindDouble(index, ((Number)value).doubleValue()); + } else if (value instanceof Number) { + prog.bindLong(index, ((Number)value).longValue()); + } else if (value instanceof Boolean) { + Boolean bool = (Boolean)value; + if (bool) { + prog.bindLong(index, 1); + } else { + prog.bindLong(index, 0); + } + } else if (value instanceof byte[]){ + prog.bindBlob(index, (byte[]) value); + } else { + prog.bindString(index, value.toString()); + } + } + + /** + * Returns data type of the given object's value. + *

+ * Returned values are + *

    + *
  • {@link Cursor#FIELD_TYPE_NULL}
  • + *
  • {@link Cursor#FIELD_TYPE_INTEGER}
  • + *
  • {@link Cursor#FIELD_TYPE_FLOAT}
  • + *
  • {@link Cursor#FIELD_TYPE_STRING}
  • + *
  • {@link Cursor#FIELD_TYPE_BLOB}
  • + *
+ *

+ * + * @param obj the object whose value type is to be returned + * @return object value type + * @hide + */ + public static int getTypeOfObject(Object obj) { + if (obj == null) { + return Cursor.FIELD_TYPE_NULL; + } else if (obj instanceof byte[]) { + return Cursor.FIELD_TYPE_BLOB; + } else if (obj instanceof Float || obj instanceof Double) { + return Cursor.FIELD_TYPE_FLOAT; + } else if (obj instanceof Long || obj instanceof Integer + || obj instanceof Short || obj instanceof Byte) { + return Cursor.FIELD_TYPE_INTEGER; + } else { + return Cursor.FIELD_TYPE_STRING; + } + } + + /** + * Fills the specified cursor window by iterating over the contents of the cursor. + * The window is filled until the cursor is exhausted or the window runs out + * of space. + * + * The original position of the cursor is left unchanged by this operation. + * + * @param cursor The cursor that contains the data to put in the window. + * @param position The start position for filling the window. + * @param window The window to fill. + * @hide + */ + public static void cursorFillWindow(final Cursor cursor, + int position, final CursorWindow window) { + if (position < 0 || position >= cursor.getCount()) { + return; + } + final int oldPos = cursor.getPosition(); + final int numColumns = cursor.getColumnCount(); + window.clear(); + window.setStartPosition(position); + window.setNumColumns(numColumns); + if (cursor.moveToPosition(position)) { + rowloop: do { + if (!window.allocRow()) { + break; + } + for (int i = 0; i < numColumns; i++) { + final int type = cursor.getType(i); + final boolean success; + switch (type) { + case Cursor.FIELD_TYPE_NULL: + success = window.putNull(position, i); + break; + + case Cursor.FIELD_TYPE_INTEGER: + success = window.putLong(cursor.getLong(i), position, i); + break; + + case Cursor.FIELD_TYPE_FLOAT: + success = window.putDouble(cursor.getDouble(i), position, i); + break; + + case Cursor.FIELD_TYPE_BLOB: { + final byte[] value = cursor.getBlob(i); + success = value != null ? window.putBlob(value, position, i) + : window.putNull(position, i); + break; + } + + default: // assume value is convertible to String + case Cursor.FIELD_TYPE_STRING: { + final String value = cursor.getString(i); + success = value != null ? window.putString(value, position, i) + : window.putNull(position, i); + break; + } + } + if (!success) { + window.freeLastRow(); + break rowloop; + } + } + position += 1; + } while (cursor.moveToNext()); + } + cursor.moveToPosition(oldPos); + } + + /** + * Appends an SQL string to the given StringBuilder, including the opening + * and closing single quotes. Any single quotes internal to sqlString will + * be escaped. + * + * This method is deprecated because we want to encourage everyone + * to use the "?" binding form. However, when implementing a + * ContentProvider, one may want to add WHERE clauses that were + * not provided by the caller. Since "?" is a positional form, + * using it in this case could break the caller because the + * indexes would be shifted to accomodate the ContentProvider's + * internal bindings. In that case, it may be necessary to + * construct a WHERE clause manually. This method is useful for + * those cases. + * + * @param sb the StringBuilder that the SQL string will be appended to + * @param sqlString the raw string to be appended, which may contain single + * quotes + */ + public static void appendEscapedSQLString(StringBuilder sb, String sqlString) { + sb.append('\''); + if (sqlString.indexOf('\'') != -1) { + int length = sqlString.length(); + for (int i = 0; i < length; i++) { + char c = sqlString.charAt(i); + if (c == '\'') { + sb.append('\''); + } + sb.append(c); + } + } else + sb.append(sqlString); + sb.append('\''); + } + + /** + * SQL-escape a string. + */ + public static String sqlEscapeString(String value) { + StringBuilder escaper = new StringBuilder(); + + DatabaseUtils.appendEscapedSQLString(escaper, value); + + return escaper.toString(); + } + + /** + * Appends an Object to an SQL string with the proper escaping, etc. + */ + public static final void appendValueToSql(StringBuilder sql, Object value) { + if (value == null) { + sql.append("NULL"); + } else if (value instanceof Boolean) { + Boolean bool = (Boolean)value; + if (bool) { + sql.append('1'); + } else { + sql.append('0'); + } + } else { + appendEscapedSQLString(sql, value.toString()); + } + } + + /** + * Concatenates two SQL WHERE clauses, handling empty or null values. + */ + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + /** + * return the collation key + * @param name + * @return the collation key + */ + public static String getCollationKey(String name) { + byte [] arr = getCollationKeyInBytes(name); + try { + return new String(arr, 0, getKeyLen(arr), "ISO8859_1"); + } catch (Exception ex) { + return ""; + } + } + + /** + * return the collation key in hex format + * @param name + * @return the collation key in hex format + */ + public static String getHexCollationKey(String name) { + byte[] arr = getCollationKeyInBytes(name); + char[] keys = encodeHex(arr); + return new String(keys, 0, getKeyLen(arr) * 2); + } + + + /** + * Used building output as Hex + */ + private static final char[] DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static char[] encodeHex(byte[] input) { + int l = input.length; + char[] out = new char[l << 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = DIGITS[(0xF0 & input[i]) >>> 4 ]; + out[j++] = DIGITS[ 0x0F & input[i] ]; + } + + return out; + } + + private static int getKeyLen(byte[] arr) { + if (arr[arr.length - 1] != 0) { + return arr.length; + } else { + // remove zero "termination" + return arr.length-1; + } + } + + private static byte[] getCollationKeyInBytes(String name) { + if (mColl == null) { + mColl = Collator.getInstance(); + mColl.setStrength(Collator.PRIMARY); + } + return mColl.getCollationKey(name).toByteArray(); + } + + private static Collator mColl = null; + /** + * Prints the contents of a Cursor to System.out. The position is restored + * after printing. + * + * @param cursor the cursor to print + */ + public static void dumpCursor(Cursor cursor) { + dumpCursor(cursor, System.out); + } + + /** + * Prints the contents of a Cursor to a PrintSteam. The position is restored + * after printing. + * + * @param cursor the cursor to print + * @param stream the stream to print to + */ + public static void dumpCursor(Cursor cursor, PrintStream stream) { + stream.println(">>>>> Dumping cursor " + cursor); + if (cursor != null) { + int startPos = cursor.getPosition(); + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + dumpCurrentRow(cursor, stream); + } + cursor.moveToPosition(startPos); + } + stream.println("<<<<<"); + } + + /** + * Prints the contents of a Cursor to a StringBuilder. The position + * is restored after printing. + * + * @param cursor the cursor to print + * @param sb the StringBuilder to print to + */ + public static void dumpCursor(Cursor cursor, StringBuilder sb) { + sb.append(">>>>> Dumping cursor " + cursor + "\n"); + if (cursor != null) { + int startPos = cursor.getPosition(); + + cursor.moveToPosition(-1); + while (cursor.moveToNext()) { + dumpCurrentRow(cursor, sb); + } + cursor.moveToPosition(startPos); + } + sb.append("<<<<<\n"); + } + + /** + * Prints the contents of a Cursor to a String. The position is restored + * after printing. + * + * @param cursor the cursor to print + * @return a String that contains the dumped cursor + */ + public static String dumpCursorToString(Cursor cursor) { + StringBuilder sb = new StringBuilder(); + dumpCursor(cursor, sb); + return sb.toString(); + } + + /** + * Prints the contents of a Cursor's current row to System.out. + * + * @param cursor the cursor to print from + */ + public static void dumpCurrentRow(Cursor cursor) { + dumpCurrentRow(cursor, System.out); + } + + /** + * Prints the contents of a Cursor's current row to a PrintSteam. + * + * @param cursor the cursor to print + * @param stream the stream to print to + */ + public static void dumpCurrentRow(Cursor cursor, PrintStream stream) { + String[] cols = cursor.getColumnNames(); + stream.println("" + cursor.getPosition() + " {"); + int length = cols.length; + for (int i = 0; i< length; i++) { + String value; + try { + value = cursor.getString(i); + } catch (SQLiteException e) { + // assume that if the getString threw this exception then the column is not + // representable by a string, e.g. it is a BLOB. + value = ""; + } + stream.println(" " + cols[i] + '=' + value); + } + stream.println("}"); + } + + /** + * Prints the contents of a Cursor's current row to a StringBuilder. + * + * @param cursor the cursor to print + * @param sb the StringBuilder to print to + */ + public static void dumpCurrentRow(Cursor cursor, StringBuilder sb) { + String[] cols = cursor.getColumnNames(); + sb.append("" + cursor.getPosition() + " {\n"); + int length = cols.length; + for (int i = 0; i < length; i++) { + String value; + try { + value = cursor.getString(i); + } catch (SQLiteException e) { + // assume that if the getString threw this exception then the column is not + // representable by a string, e.g. it is a BLOB. + value = ""; + } + sb.append(" " + cols[i] + '=' + value + "\n"); + } + sb.append("}\n"); + } + + /** + * Dump the contents of a Cursor's current row to a String. + * + * @param cursor the cursor to print + * @return a String that contains the dumped cursor row + */ + public static String dumpCurrentRowToString(Cursor cursor) { + StringBuilder sb = new StringBuilder(); + dumpCurrentRow(cursor, sb); + return sb.toString(); + } + + /** + * Reads a String out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorStringToContentValues(Cursor cursor, String field, + ContentValues values) { + cursorStringToContentValues(cursor, field, values, field); + } + + /** + * Reads a String out of a field in a Cursor and writes it to an InsertHelper. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param inserter The InsertHelper to bind into + * @param index the index of the bind entry in the InsertHelper + */ + public static void cursorStringToInsertHelper(Cursor cursor, String field, + InsertHelper inserter, int index) { + inserter.bind(index, cursor.getString(cursor.getColumnIndexOrThrow(field))); + } + + /** + * Reads a String out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The TEXT field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + * @param key The key to store the value with in the map + */ + public static void cursorStringToContentValues(Cursor cursor, String field, + ContentValues values, String key) { + values.put(key, cursor.getString(cursor.getColumnIndexOrThrow(field))); + } + + /** + * Reads an Integer out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values) { + cursorIntToContentValues(cursor, field, values, field); + } + + /** + * Reads a Integer out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + * @param key The key to store the value with in the map + */ + public static void cursorIntToContentValues(Cursor cursor, String field, ContentValues values, + String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + values.put(key, cursor.getInt(colIndex)); + } else { + values.put(key, (Integer) null); + } + } + + /** + * Reads a Long out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into, with the field as the key + */ + public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values) + { + cursorLongToContentValues(cursor, field, values, field); + } + + /** + * Reads a Long out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The INTEGER field to read + * @param values The {@link ContentValues} to put the value into + * @param key The key to store the value with in the map + */ + public static void cursorLongToContentValues(Cursor cursor, String field, ContentValues values, + String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + Long value = Long.valueOf(cursor.getLong(colIndex)); + values.put(key, value); + } else { + values.put(key, (Long) null); + } + } + + /** + * Reads a Double out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The REAL field to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorDoubleToCursorValues(Cursor cursor, String field, ContentValues values) + { + cursorDoubleToContentValues(cursor, field, values, field); + } + + /** + * Reads a Double out of a field in a Cursor and writes it to a Map. + * + * @param cursor The cursor to read from + * @param field The REAL field to read + * @param values The {@link ContentValues} to put the value into + * @param key The key to store the value with in the map + */ + public static void cursorDoubleToContentValues(Cursor cursor, String field, + ContentValues values, String key) { + int colIndex = cursor.getColumnIndex(field); + if (!cursor.isNull(colIndex)) { + values.put(key, cursor.getDouble(colIndex)); + } else { + values.put(key, (Double) null); + } + } + + /** + * Read the entire contents of a cursor row and store them in a ContentValues. + * + * @param cursor the cursor to read from. + * @param values the {@link ContentValues} to put the row into. + */ + public static void cursorRowToContentValues(Cursor cursor, ContentValues values) { + String[] columns = cursor.getColumnNames(); + int length = columns.length; + for (int i = 0; i < length; i++) { + if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) { + values.put(columns[i], cursor.getBlob(i)); + } else { + values.put(columns[i], cursor.getString(i)); + } + } + } + + /** + * Picks a start position for {@link Cursor#fillWindow} such that the + * window will contain the requested row and a useful range of rows + * around it. + * + * When the data set is too large to fit in a cursor window, seeking the + * cursor can become a very expensive operation since we have to run the + * query again when we move outside the bounds of the current window. + * + * We try to choose a start position for the cursor window such that + * 1/3 of the window's capacity is used to hold rows before the requested + * position and 2/3 of the window's capacity is used to hold rows after the + * requested position. + * + * @param cursorPosition The row index of the row we want to get. + * @param cursorWindowCapacity The estimated number of rows that can fit in + * a cursor window, or 0 if unknown. + * @return The recommended start position, always less than or equal to + * the requested row. + * @hide + */ + public static int cursorPickFillWindowStartPosition( + int cursorPosition, int cursorWindowCapacity) { + return Math.max(cursorPosition - cursorWindowCapacity / 3, 0); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @return the number of rows in the table + */ + public static long queryNumEntries(SQLiteDatabase db, String table) { + return queryNumEntries(db, table, null, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection) { + return queryNumEntries(db, table, selection, null); + } + + /** + * Query the table for the number of rows in the table. + * @param db the database the table is in + * @param table the name of the table to query + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE itself). + * Passing null will count all rows for the given table + * @param selectionArgs You may include ?s in selection, + * which will be replaced by the values from selectionArgs, + * in order that they appear in the selection. + * The values will be bound as Strings. + * @return the number of rows in the table filtered by the selection + */ + public static long queryNumEntries(SQLiteDatabase db, String table, String selection, + String[] selectionArgs) { + String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : ""; + return longForQuery(db, "select count(*) from " + table + s, + selectionArgs); + } + + /** + * Query the table to check whether a table is empty or not + * @param db the database the table is in + * @param table the name of the table to query + * @return True if the table is empty + * @hide + */ + public static boolean queryIsEmpty(SQLiteDatabase db, String table) { + long isEmpty = longForQuery(db, "select exists(select 1 from " + table + ")", null); + return isEmpty == 0; + } + + /** + * Utility method to run the query on the db and return the value in the + * first column of the first row. + */ + public static long longForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return longForQuery(prog, selectionArgs); + } finally { + prog.close(); + } + } + + /** + * Utility method to run the pre-compiled query and return the value in the + * first column of the first row. + */ + public static long longForQuery(SQLiteStatement prog, String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForLong(); + } + + /** + * Utility method to run the query on the db and return the value in the + * first column of the first row. + */ + public static String stringForQuery(SQLiteDatabase db, String query, String[] selectionArgs) { + SQLiteStatement prog = db.compileStatement(query); + try { + return stringForQuery(prog, selectionArgs); + } finally { + prog.close(); + } + } + + /** + * Utility method to run the pre-compiled query and return the value in the + * first column of the first row. + */ + public static String stringForQuery(SQLiteStatement prog, String[] selectionArgs) { + prog.bindAllArgsAsStrings(selectionArgs); + return prog.simpleQueryForString(); + } + + /** + * Utility method to run the query on the db and return the blob value in the + * first column of the first row. + * + * @return A read-only file descriptor for a copy of the blob value. + */ + + /** + * Reads a String out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorStringToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getString(index)); + } + } + + /** + * Reads a Long out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorLongToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getLong(index)); + } + } + + /** + * Reads a Short out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorShortToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getShort(index)); + } + } + + /** + * Reads a Integer out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorIntToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getInt(index)); + } + } + + /** + * Reads a Float out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorFloatToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getFloat(index)); + } + } + + /** + * Reads a Double out of a column in a Cursor and writes it to a ContentValues. + * Adds nothing to the ContentValues if the column isn't present or if its value is null. + * + * @param cursor The cursor to read from + * @param column The column to read + * @param values The {@link ContentValues} to put the value into + */ + public static void cursorDoubleToContentValuesIfPresent(Cursor cursor, ContentValues values, + String column) { + final int index = cursor.getColumnIndex(column); + if (index != -1 && !cursor.isNull(index)) { + values.put(column, cursor.getDouble(index)); + } + } + + /** + * This class allows users to do multiple inserts into a table using + * the same statement. + *

+ * This class is not thread-safe. + *

+ * + * @deprecated Use {@link SQLiteStatement} instead. + */ + @Deprecated + public static class InsertHelper { + private final SQLiteDatabase mDb; + private final String mTableName; + private HashMap mColumns; + private String mInsertSQL = null; + private SQLiteStatement mInsertStatement = null; + private SQLiteStatement mReplaceStatement = null; + private SQLiteStatement mPreparedStatement = null; + + /** + * {@hide} + * + * These are the columns returned by sqlite's "PRAGMA + * table_info(...)" command that we depend on. + */ + public static final int TABLE_INFO_PRAGMA_COLUMNNAME_INDEX = 1; + + /** + * This field was accidentally exposed in earlier versions of the platform + * so we can hide it but we can't remove it. + * + * @hide + */ + public static final int TABLE_INFO_PRAGMA_DEFAULT_INDEX = 4; + + /** + * @param db the SQLiteDatabase to insert into + * @param tableName the name of the table to insert into + */ + public InsertHelper(SQLiteDatabase db, String tableName) { + mDb = db; + mTableName = tableName; + } + + private void buildSQL() throws SQLException { + StringBuilder sb = new StringBuilder(128); + sb.append("INSERT INTO "); + sb.append(mTableName); + sb.append(" ("); + + StringBuilder sbv = new StringBuilder(128); + sbv.append("VALUES ("); + + int i = 1; + Cursor cur = null; + try { + cur = mDb.rawQuery("PRAGMA table_info(" + mTableName + ")", null); + mColumns = new HashMap(cur.getCount()); + while (cur.moveToNext()) { + String columnName = cur.getString(TABLE_INFO_PRAGMA_COLUMNNAME_INDEX); + String defaultValue = cur.getString(TABLE_INFO_PRAGMA_DEFAULT_INDEX); + + mColumns.put(columnName, i); + sb.append("'"); + sb.append(columnName); + sb.append("'"); + + if (defaultValue == null) { + sbv.append("?"); + } else { + sbv.append("COALESCE(?, "); + sbv.append(defaultValue); + sbv.append(")"); + } + + sb.append(i == cur.getCount() ? ") " : ", "); + sbv.append(i == cur.getCount() ? ");" : ", "); + ++i; + } + } finally { + if (cur != null) cur.close(); + } + + sb.append(sbv); + + mInsertSQL = sb.toString(); + if (DEBUG) Log.v(TAG, "insert statement is " + mInsertSQL); + } + + private SQLiteStatement getStatement(boolean allowReplace) throws SQLException { + if (allowReplace) { + if (mReplaceStatement == null) { + if (mInsertSQL == null) buildSQL(); + // chop "INSERT" off the front and prepend "INSERT OR REPLACE" instead. + String replaceSQL = "INSERT OR REPLACE" + mInsertSQL.substring(6); + mReplaceStatement = mDb.compileStatement(replaceSQL); + } + return mReplaceStatement; + } else { + if (mInsertStatement == null) { + if (mInsertSQL == null) buildSQL(); + mInsertStatement = mDb.compileStatement(mInsertSQL); + } + return mInsertStatement; + } + } + + /** + * Performs an insert, adding a new row with the given values. + * + * @param values the set of values with which to populate the + * new row + * @param allowReplace if true, the statement does "INSERT OR + * REPLACE" instead of "INSERT", silently deleting any + * previously existing rows that would cause a conflict + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + private long insertInternal(ContentValues values, boolean allowReplace) { + // Start a transaction even though we don't really need one. + // This is to help maintain compatibility with applications that + // access InsertHelper from multiple threads even though they never should have. + // The original code used to lock the InsertHelper itself which was prone + // to deadlocks. Starting a transaction achieves the same mutual exclusion + // effect as grabbing a lock but without the potential for deadlocks. + mDb.beginTransactionNonExclusive(); + try { + SQLiteStatement stmt = getStatement(allowReplace); + stmt.clearBindings(); + if (DEBUG) Log.v(TAG, "--- inserting in table " + mTableName); + for (Map.Entry e: values.valueSet()) { + final String key = e.getKey(); + int i = getColumnIndex(key); + DatabaseUtils.bindObjectToProgram(stmt, i, e.getValue()); + if (DEBUG) { + Log.v(TAG, "binding " + e.getValue() + " to column " + + i + " (" + key + ")"); + } + } + long result = stmt.executeInsert(); + mDb.setTransactionSuccessful(); + return result; + } catch (SQLException e) { + Log.e(TAG, "Error inserting " + values + " into table " + mTableName, e); + return -1; + } finally { + mDb.endTransaction(); + } + } + + /** + * Returns the index of the specified column. This is index is suitagble for use + * in calls to bind(). + * @param key the column name + * @return the index of the column + */ + public int getColumnIndex(String key) { + getStatement(false); + final Integer index = mColumns.get(key); + if (index == null) { + throw new IllegalArgumentException("column '" + key + "' is invalid"); + } + return index; + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, double value) { + mPreparedStatement.bindDouble(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, float value) { + mPreparedStatement.bindDouble(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, long value) { + mPreparedStatement.bindLong(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, int value) { + mPreparedStatement.bindLong(index, value); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, boolean value) { + mPreparedStatement.bindLong(index, value ? 1 : 0); + } + + /** + * Bind null to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + */ + public void bindNull(int index) { + mPreparedStatement.bindNull(index); + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, byte[] value) { + if (value == null) { + mPreparedStatement.bindNull(index); + } else { + mPreparedStatement.bindBlob(index, value); + } + } + + /** + * Bind the value to an index. A prepareForInsert() or prepareForReplace() + * without a matching execute() must have already have been called. + * @param index the index of the slot to which to bind + * @param value the value to bind + */ + public void bind(int index, String value) { + if (value == null) { + mPreparedStatement.bindNull(index); + } else { + mPreparedStatement.bindString(index, value); + } + } + + /** + * Performs an insert, adding a new row with the given values. + * If the table contains conflicting rows, an error is + * returned. + * + * @param values the set of values with which to populate the + * new row + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long insert(ContentValues values) { + return insertInternal(values, false); + } + + /** + * Execute the previously prepared insert or replace using the bound values + * since the last call to prepareForInsert or prepareForReplace. + * + *

Note that calling bind() and then execute() is not thread-safe. The only thread-safe + * way to use this class is to call insert() or replace(). + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long execute() { + if (mPreparedStatement == null) { + throw new IllegalStateException("you must prepare this inserter before calling " + + "execute"); + } + try { + if (DEBUG) Log.v(TAG, "--- doing insert or replace in table " + mTableName); + return mPreparedStatement.executeInsert(); + } catch (SQLException e) { + Log.e(TAG, "Error executing InsertHelper with table " + mTableName, e); + return -1; + } finally { + // you can only call this once per prepare + mPreparedStatement = null; + } + } + + /** + * Prepare the InsertHelper for an insert. The pattern for this is: + *

    + *
  • prepareForInsert() + *
  • bind(index, value); + *
  • bind(index, value); + *
  • ... + *
  • bind(index, value); + *
  • execute(); + *
+ */ + public void prepareForInsert() { + mPreparedStatement = getStatement(false); + mPreparedStatement.clearBindings(); + } + + /** + * Prepare the InsertHelper for a replace. The pattern for this is: + *
    + *
  • prepareForReplace() + *
  • bind(index, value); + *
  • bind(index, value); + *
  • ... + *
  • bind(index, value); + *
  • execute(); + *
+ */ + public void prepareForReplace() { + mPreparedStatement = getStatement(true); + mPreparedStatement.clearBindings(); + } + + /** + * Performs an insert, adding a new row with the given values. + * If the table contains conflicting rows, they are deleted + * and replaced with the new row. + * + * @param values the set of values with which to populate the + * new row + * + * @return the row ID of the newly inserted row, or -1 if an + * error occurred + */ + public long replace(ContentValues values) { + return insertInternal(values, true); + } + + /** + * Close this object and release any resources associated with + * it. The behavior of calling insert() after + * calling this method is undefined. + */ + public void close() { + if (mInsertStatement != null) { + mInsertStatement.close(); + mInsertStatement = null; + } + if (mReplaceStatement != null) { + mReplaceStatement.close(); + mReplaceStatement = null; + } + mInsertSQL = null; + mColumns = null; + } + } + + /** + * Creates a db and populates it with the sql statements in sqlStatements. + * + * @param context the context to use to create the db + * @param dbName the name of the db to create + * @param dbVersion the version to set on the db + * @param sqlStatements the statements to use to populate the db. This should be a single string + * of the form returned by sqlite3's .dump command (statements separated by + * semicolons) + */ + static public void createDbFromSqlStatements( + Context context, String dbName, int dbVersion, String sqlStatements) { + + File f = context.getDatabasePath(dbName); + f.getParentFile().mkdirs(); + SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(f, null); + + // TODO: this is not quite safe since it assumes that all semicolons at the end of a line + // terminate statements. It is possible that a text field contains ;\n. We will have to fix + // this if that turns out to be a problem. + String[] statements = TextUtils.split(sqlStatements, ";\n"); + for (String statement : statements) { + if (TextUtils.isEmpty(statement)) continue; + db.execSQL(statement); + } + db.setVersion(dbVersion); + db.close(); + } + + /** + * Returns one of the following which represent the type of the given SQL statement. + *
    + *
  1. {@link #STATEMENT_SELECT}
  2. + *
  3. {@link #STATEMENT_UPDATE}
  4. + *
  5. {@link #STATEMENT_ATTACH}
  6. + *
  7. {@link #STATEMENT_BEGIN}
  8. + *
  9. {@link #STATEMENT_COMMIT}
  10. + *
  11. {@link #STATEMENT_ABORT}
  12. + *
  13. {@link #STATEMENT_OTHER}
  14. + *
+ * @param sql the SQL statement whose type is returned by this method + * @return one of the values listed above + */ + public static int getSqlStatementType(String sql) { + sql = sql.trim(); + if (sql.length() < 3) { + return STATEMENT_OTHER; + } + String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT); + if (prefixSql.equals("SEL")) { + return STATEMENT_SELECT; + } else if (prefixSql.equals("INS") || + prefixSql.equals("UPD") || + prefixSql.equals("REP") || + prefixSql.equals("DEL")) { + return STATEMENT_UPDATE; + } else if (prefixSql.equals("ATT")) { + return STATEMENT_ATTACH; + } else if (prefixSql.equals("COM")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("END")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("ROL")) { + return STATEMENT_ABORT; + } else if (prefixSql.equals("BEG")) { + return STATEMENT_BEGIN; + } else if (prefixSql.equals("PRA")) { + return STATEMENT_PRAGMA; + } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || + prefixSql.equals("ALT")) { + return STATEMENT_DDL; + } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { + return STATEMENT_UNPREPARED; + } + return STATEMENT_OTHER; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } + + /** + * Returns column index of "_id" column, or -1 if not found. + * @hide + */ + public static int findRowIdColumnIndex(String[] columnNames) { + int length = columnNames.length; + for (int i = 0; i < length; i++) { + if (columnNames[i].equals("_id")) { + return i; + } + } + return -1; + } +} diff --git a/src/api-impl/android/database/DefaultDatabaseErrorHandler.java b/src/api-impl/android/database/DefaultDatabaseErrorHandler.java new file mode 100644 index 00000000..ae6227b8 --- /dev/null +++ b/src/api-impl/android/database/DefaultDatabaseErrorHandler.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database; + +import java.io.File; +import java.util.List; + +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.Pair; + +/** + * Default class used to define the action to take when database corruption is reported + * by sqlite. + *

+ * An application can specify an implementation of {@link DatabaseErrorHandler} on the + * following: + *

    + *
  • {@link SQLiteDatabase#openOrCreateDatabase(String, + * android.database.sqlite.SQLiteDatabase.CursorFactory, DatabaseErrorHandler)}
  • + *
  • {@link SQLiteDatabase#openDatabase(String, + * android.database.sqlite.SQLiteDatabase.CursorFactory, int, DatabaseErrorHandler)}
  • + *
+ * The specified {@link DatabaseErrorHandler} is used to handle database corruption errors, if they + * occur. + *

+ * If null is specified for the DatabaseErrorHandler param in the above calls, this class is used + * as the default {@link DatabaseErrorHandler}. + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + + // If this is a SEE build, do not delete any database files. + // It may be that the user has specified an incorrect password. + if( SQLiteDatabase.hasCodec() ) return; + + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(dbObj.getPath()); + return; + } + + List> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = dbObj.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(dbObj.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.e(TAG, "deleting the database file: " + fileName); + try { + SQLiteDatabase.deleteDatabase(new File(fileName)); + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: " + e.getMessage()); + } + } +} diff --git a/src/api-impl/android/database/SQLException.java b/src/api-impl/android/database/SQLException.java new file mode 100644 index 00000000..3ccad6fc --- /dev/null +++ b/src/api-impl/android/database/SQLException.java @@ -0,0 +1,37 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database; + +/** + * An exception that indicates there was an error with SQL parsing or execution. + */ +public class SQLException extends RuntimeException { + public SQLException() { + } + + public SQLException(String error) { + super(error); + } + + public SQLException(String error, Throwable cause) { + super(error, cause); + } +} diff --git a/src/api-impl/android/database/sqlite/DatabaseObjectNotClosedException.java b/src/api-impl/android/database/sqlite/DatabaseObjectNotClosedException.java new file mode 100644 index 00000000..93015a32 --- /dev/null +++ b/src/api-impl/android/database/sqlite/DatabaseObjectNotClosedException.java @@ -0,0 +1,35 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that garbage-collector is finalizing a database object + * that is not explicitly closed + * @hide + */ +public class DatabaseObjectNotClosedException extends RuntimeException { + private static final String s = "Application did not close the cursor or database object " + + "that was opened here"; + + public DatabaseObjectNotClosedException() { + super(s); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteAbortException.java b/src/api-impl/android/database/sqlite/SQLiteAbortException.java new file mode 100644 index 00000000..d28b495c --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteAbortException.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that the SQLite program was aborted. + * This can happen either through a call to ABORT in a trigger, + * or as the result of using the ABORT conflict clause. + */ +public class SQLiteAbortException extends SQLiteException { + public SQLiteAbortException() {} + + public SQLiteAbortException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteAccessPermException.java b/src/api-impl/android/database/sqlite/SQLiteAccessPermException.java new file mode 100644 index 00000000..c9d3cb87 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteAccessPermException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * This exception class is used when sqlite can't access the database file + * due to lack of permissions on the file. + */ +public class SQLiteAccessPermException extends SQLiteException { + public SQLiteAccessPermException() {} + + public SQLiteAccessPermException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/src/api-impl/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java new file mode 100644 index 00000000..6511d94c --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * Thrown if the the bind or column parameter index is out of range + */ +public class SQLiteBindOrColumnIndexOutOfRangeException extends SQLiteException { + public SQLiteBindOrColumnIndexOutOfRangeException() {} + + public SQLiteBindOrColumnIndexOutOfRangeException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteBlobTooBigException.java b/src/api-impl/android/database/sqlite/SQLiteBlobTooBigException.java new file mode 100644 index 00000000..70ae864d --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteBlobTooBigException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteBlobTooBigException extends SQLiteException { + public SQLiteBlobTooBigException() {} + + public SQLiteBlobTooBigException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteCantOpenDatabaseException.java b/src/api-impl/android/database/sqlite/SQLiteCantOpenDatabaseException.java new file mode 100644 index 00000000..be76e5eb --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteCantOpenDatabaseException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteCantOpenDatabaseException extends SQLiteException { + public SQLiteCantOpenDatabaseException() {} + + public SQLiteCantOpenDatabaseException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteClosable.java b/src/api-impl/android/database/sqlite/SQLiteClosable.java new file mode 100644 index 00000000..bea9ddc1 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteClosable.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2007 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import java.io.Closeable; + +/** + * An object created from a SQLiteDatabase that can be closed. + * + * This class implements a primitive reference counting scheme for database objects. + */ +public abstract class SQLiteClosable implements Closeable { + private int mReferenceCount = 1; + + /** + * Called when the last reference to the object was released by + * a call to {@link #releaseReference()} or {@link #close()}. + */ + protected abstract void onAllReferencesReleased(); + + /** + * Called when the last reference to the object was released by + * a call to {@link #releaseReferenceFromContainer()}. + * + * @deprecated Do not use. + */ + @Deprecated + protected void onAllReferencesReleasedFromContainer() { + onAllReferencesReleased(); + } + + /** + * Acquires a reference to the object. + * + * @throws IllegalStateException if the last reference to the object has already + * been released. + */ + public void acquireReference() { + synchronized(this) { + if (mReferenceCount <= 0) { + throw new IllegalStateException( + "attempt to re-open an already-closed object: " + this); + } + mReferenceCount++; + } + } + + /** + * Releases a reference to the object, closing the object if the last reference + * was released. + * + * @see #onAllReferencesReleased() + */ + public void releaseReference() { + boolean refCountIsZero = false; + synchronized(this) { + refCountIsZero = --mReferenceCount == 0; + } + if (refCountIsZero) { + onAllReferencesReleased(); + } + } + + /** + * Releases a reference to the object that was owned by the container of the object, + * closing the object if the last reference was released. + * + * @see #onAllReferencesReleasedFromContainer() + * @deprecated Do not use. + */ + @Deprecated + public void releaseReferenceFromContainer() { + boolean refCountIsZero = false; + synchronized(this) { + refCountIsZero = --mReferenceCount == 0; + } + if (refCountIsZero) { + onAllReferencesReleasedFromContainer(); + } + } + + /** + * Releases a reference to the object, closing the object if the last reference + * was released. + * + * Calling this method is equivalent to calling {@link #releaseReference}. + * + * @see #releaseReference() + * @see #onAllReferencesReleased() + */ + public void close() { + releaseReference(); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteConnection.java b/src/api-impl/android/database/sqlite/SQLiteConnection.java new file mode 100644 index 00000000..477c73d5 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteConnection.java @@ -0,0 +1,1525 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import dalvik.system.CloseGuard; + +import android.database.Cursor; +import android.database.CursorWindow; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.os.SystemClock; +import android.util.Log; +import android.util.LruCache; +import android.util.Printer; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Represents a SQLite database connection. + * Each connection wraps an instance of a native sqlite3 object. + *

+ * When database connection pooling is enabled, there can be multiple active + * connections to the same database. Otherwise there is typically only one + * connection per database. + *

+ * When the SQLite WAL feature is enabled, multiple readers and one writer + * can concurrently access the database. Without WAL, readers and writers + * are mutually exclusive. + *

+ * + *

Ownership and concurrency guarantees

+ *

+ * Connection objects are not thread-safe. They are acquired as needed to + * perform a database operation and are then returned to the pool. At any + * given time, a connection is either owned and used by a {@link SQLiteSession} + * object or the {@link SQLiteConnectionPool}. Those classes are + * responsible for serializing operations to guard against concurrent + * use of a connection. + *

+ * The guarantee of having a single owner allows this class to be implemented + * without locks and greatly simplifies resource management. + *

+ * + *

Encapsulation guarantees

+ *

+ * The connection object object owns *all* of the SQLite related native + * objects that are associated with the connection. What's more, there are + * no other objects in the system that are capable of obtaining handles to + * those native objects. Consequently, when the connection is closed, we do + * not have to worry about what other components might have references to + * its associated SQLite state -- there are none. + *

+ * Encapsulation is what ensures that the connection object's + * lifecycle does not become a tortured mess of finalizers and reference + * queues. + *

+ * + *

Reentrance

+ *

+ * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + *

+ * + * @hide + */ +public final class SQLiteConnection implements CancellationSignal.OnCancelListener { + private static final String TAG = "SQLiteConnection"; + private static final boolean DEBUG = false; + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final SQLiteConnectionPool mPool; + private final SQLiteDatabaseConfiguration mConfiguration; + private final int mConnectionId; + private final boolean mIsPrimaryConnection; + private final boolean mIsReadOnlyConnection; + private final PreparedStatementCache mPreparedStatementCache; + private PreparedStatement mPreparedStatementPool; + + // The recent operations log. + private final OperationLog mRecentOperations = new OperationLog(); + + // The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY) + private long mConnectionPtr; + + private boolean mOnlyAllowReadOnlyOperations; + + // The number of times attachCancellationSignal has been called. + // Because SQLite statement execution can be reentrant, we keep track of how many + // times we have attempted to attach a cancellation signal to the connection so that + // we can ensure that we detach the signal at the right time. + private int mCancellationSignalAttachCount; + + private static native long nativeOpen(String path, int openFlags, String label, + boolean enableTrace, boolean enableProfile); + private static native void nativeClose(long connectionPtr); + private static native void nativeRegisterCustomFunction(long connectionPtr, + SQLiteCustomFunction function); + private static native void nativeRegisterLocalizedCollators(long connectionPtr, String locale); + private static native long nativePrepareStatement(long connectionPtr, String sql); + private static native void nativeFinalizeStatement(long connectionPtr, long statementPtr); + private static native int nativeGetParameterCount(long connectionPtr, long statementPtr); + private static native boolean nativeIsReadOnly(long connectionPtr, long statementPtr); + private static native int nativeGetColumnCount(long connectionPtr, long statementPtr); + private static native String nativeGetColumnName(long connectionPtr, long statementPtr, + int index); + private static native void nativeBindNull(long connectionPtr, long statementPtr, + int index); + private static native void nativeBindLong(long connectionPtr, long statementPtr, + int index, long value); + private static native void nativeBindDouble(long connectionPtr, long statementPtr, + int index, double value); + private static native void nativeBindString(long connectionPtr, long statementPtr, + int index, String value); + private static native void nativeBindBlob(long connectionPtr, long statementPtr, + int index, byte[] value); + private static native void nativeResetStatementAndClearBindings( + long connectionPtr, long statementPtr); + private static native void nativeExecute(long connectionPtr, long statementPtr); + private static native long nativeExecuteForLong(long connectionPtr, long statementPtr); + private static native String nativeExecuteForString(long connectionPtr, long statementPtr); + private static native int nativeExecuteForBlobFileDescriptor( + long connectionPtr, long statementPtr); + private static native int nativeExecuteForChangedRowCount(long connectionPtr, long statementPtr); + private static native long nativeExecuteForLastInsertedRowId( + long connectionPtr, long statementPtr); + private static native long nativeExecuteForCursorWindow( + long connectionPtr, long statementPtr, CursorWindow win, + int startPos, int requiredPos, boolean countAllRows); + private static native int nativeGetDbLookaside(long connectionPtr); + private static native void nativeCancel(long connectionPtr); + private static native void nativeResetCancel(long connectionPtr, boolean cancelable); + + private static native boolean nativeHasCodec(); + public static boolean hasCodec(){ return nativeHasCodec(); } + + private SQLiteConnection(SQLiteConnectionPool pool, + SQLiteDatabaseConfiguration configuration, + int connectionId, boolean primaryConnection) { + mPool = pool; + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + mConnectionId = connectionId; + mIsPrimaryConnection = primaryConnection; + mIsReadOnlyConnection = (configuration.openFlags & SQLiteDatabase.OPEN_READONLY) != 0; + mPreparedStatementCache = new PreparedStatementCache( + mConfiguration.maxSqlCacheSize); + mCloseGuard.open("close"); + } + + @Override + protected void finalize() throws Throwable { + try { + if (mPool != null && mConnectionPtr != 0) { + mPool.onConnectionLeaked(); + } + + dispose(true); + } finally { + super.finalize(); + } + } + + // Called by SQLiteConnectionPool only. + static SQLiteConnection open(SQLiteConnectionPool pool, + SQLiteDatabaseConfiguration configuration, + int connectionId, boolean primaryConnection) { + SQLiteConnection connection = new SQLiteConnection(pool, configuration, + connectionId, primaryConnection); + try { + connection.open(); + return connection; + } catch (SQLiteException ex) { + connection.dispose(false); + throw ex; + } + } + + // Called by SQLiteConnectionPool only. + // Closes the database closes and releases all of its associated resources. + // Do not call methods on the connection after it is closed. It will probably crash. + void close() { + dispose(false); + } + + private void open() { + mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags, + mConfiguration.label, + SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME); + + setPageSize(); + setForeignKeyModeFromConfiguration(); + setJournalSizeLimit(); + setAutoCheckpointInterval(); + if( !nativeHasCodec() ){ + setWalModeFromConfiguration(); + setLocaleFromConfiguration(); + } + // Register custom functions. + final int functionCount = mConfiguration.customFunctions.size(); + for (int i = 0; i < functionCount; i++) { + SQLiteCustomFunction function = mConfiguration.customFunctions.get(i); + nativeRegisterCustomFunction(mConnectionPtr, function); + } + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (mConnectionPtr != 0) { + final int cookie = mRecentOperations.beginOperation("close", null, null); + try { + mPreparedStatementCache.evictAll(); + nativeClose(mConnectionPtr); + mConnectionPtr = 0; + } finally { + mRecentOperations.endOperation(cookie); + } + } + } + + private void setPageSize() { + if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { + final long newValue = SQLiteGlobal.getDefaultPageSize(); + long value = executeForLong("PRAGMA page_size", null, null); + if (value != newValue) { + execute("PRAGMA page_size=" + newValue, null, null); + } + } + } + + private void setAutoCheckpointInterval() { + if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { + final long newValue = SQLiteGlobal.getWALAutoCheckpoint(); + long value = executeForLong("PRAGMA wal_autocheckpoint", null, null); + if (value != newValue) { + executeForLong("PRAGMA wal_autocheckpoint=" + newValue, null, null); + } + } + } + + private void setJournalSizeLimit() { + if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { + final long newValue = SQLiteGlobal.getJournalSizeLimit(); + long value = executeForLong("PRAGMA journal_size_limit", null, null); + if (value != newValue) { + executeForLong("PRAGMA journal_size_limit=" + newValue, null, null); + } + } + } + + private void setForeignKeyModeFromConfiguration() { + if (!mIsReadOnlyConnection) { + final long newValue = mConfiguration.foreignKeyConstraintsEnabled ? 1 : 0; + long value = executeForLong("PRAGMA foreign_keys", null, null); + if (value != newValue) { + execute("PRAGMA foreign_keys=" + newValue, null, null); + } + } + } + + private void setWalModeFromConfiguration() { + if (!mConfiguration.isInMemoryDb() && !mIsReadOnlyConnection) { + if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { + setJournalMode("WAL"); + setSyncMode(SQLiteGlobal.getWALSyncMode()); + } else { + setJournalMode(SQLiteGlobal.getDefaultJournalMode()); + setSyncMode(SQLiteGlobal.getDefaultSyncMode()); + } + } + } + + private void setSyncMode(String newValue) { + String value = executeForString("PRAGMA synchronous", null, null); + if (!canonicalizeSyncMode(value).equalsIgnoreCase( + canonicalizeSyncMode(newValue))) { + execute("PRAGMA synchronous=" + newValue, null, null); + } + } + + private static String canonicalizeSyncMode(String value) { + if (value.equals("0")) { + return "OFF"; + } else if (value.equals("1")) { + return "NORMAL"; + } else if (value.equals("2")) { + return "FULL"; + } + return value; + } + + private void setJournalMode(String newValue) { + String value = executeForString("PRAGMA journal_mode", null, null); + if (!value.equalsIgnoreCase(newValue)) { + try { + String result = executeForString("PRAGMA journal_mode=" + newValue, null, null); + if (result.equalsIgnoreCase(newValue)) { + return; + } + // PRAGMA journal_mode silently fails and returns the original journal + // mode in some cases if the journal mode could not be changed. + } catch (SQLiteDatabaseLockedException ex) { + // This error (SQLITE_BUSY) occurs if one connection has the database + // open in WAL mode and another tries to change it to non-WAL. + } + // Because we always disable WAL mode when a database is first opened + // (even if we intend to re-enable it), we can encounter problems if + // there is another open connection to the database somewhere. + // This can happen for a variety of reasons such as an application opening + // the same database in multiple processes at the same time or if there is a + // crashing content provider service that the ActivityManager has + // removed from its registry but whose process hasn't quite died yet + // by the time it is restarted in a new process. + // + // If we don't change the journal mode, nothing really bad happens. + // In the worst case, an application that enables WAL might not actually + // get it, although it can still use connection pooling. + Log.w(TAG, "Could not change the database journal mode of '" + + mConfiguration.label + "' from '" + value + "' to '" + newValue + + "' because the database is locked. This usually means that " + + "there are other open connections to the database which prevents " + + "the database from enabling or disabling write-ahead logging mode. " + + "Proceeding without changing the journal mode."); + } + } + + private void setLocaleFromConfiguration() { + if ((mConfiguration.openFlags & SQLiteDatabase.NO_LOCALIZED_COLLATORS) != 0) { + return; + } + + // Register the localized collators. + final String newLocale = mConfiguration.locale.toString(); + nativeRegisterLocalizedCollators(mConnectionPtr, newLocale); + + // If the database is read-only, we cannot modify the android metadata table + // or existing indexes. + if (mIsReadOnlyConnection) { + return; + } + + try { + // Ensure the android metadata table exists. + execute("CREATE TABLE IF NOT EXISTS android_metadata (locale TEXT)", null, null); + + // Check whether the locale was actually changed. + final String oldLocale = executeForString("SELECT locale FROM android_metadata " + + "UNION SELECT NULL ORDER BY locale DESC LIMIT 1", null, null); + if (oldLocale != null && oldLocale.equals(newLocale)) { + return; + } + + // Go ahead and update the indexes using the new locale. + execute("BEGIN", null, null); + boolean success = false; + try { + execute("DELETE FROM android_metadata", null, null); + execute("INSERT INTO android_metadata (locale) VALUES(?)", + new Object[] { newLocale }, null); + execute("REINDEX LOCALIZED", null, null); + success = true; + } finally { + execute(success ? "COMMIT" : "ROLLBACK", null, null); + } + } catch (RuntimeException ex) { + throw new SQLiteException("Failed to change locale for db '" + mConfiguration.label + + "' to '" + newLocale + "'.", ex); + } + } + + public void enableLocalizedCollators(){ + if( nativeHasCodec() ){ + setLocaleFromConfiguration(); + } + } + + // Called by SQLiteConnectionPool only. + void reconfigure(SQLiteDatabaseConfiguration configuration) { + mOnlyAllowReadOnlyOperations = false; + + // Register custom functions. + final int functionCount = configuration.customFunctions.size(); + for (int i = 0; i < functionCount; i++) { + SQLiteCustomFunction function = configuration.customFunctions.get(i); + if (!mConfiguration.customFunctions.contains(function)) { + nativeRegisterCustomFunction(mConnectionPtr, function); + } + } + + // Remember what changed. + boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled + != mConfiguration.foreignKeyConstraintsEnabled; + boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) + & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + boolean localeChanged = !configuration.locale.equals(mConfiguration.locale); + + // Update configuration parameters. + mConfiguration.updateParametersFrom(configuration); + + // Update prepared statement cache size. + // sqlite.org: android.util.LruCache.resize() requires API level 21. + // mPreparedStatementCache.resize(configuration.maxSqlCacheSize); + + // Update foreign key mode. + if (foreignKeyModeChanged) { + setForeignKeyModeFromConfiguration(); + } + + // Update WAL. + if (walModeChanged) { + setWalModeFromConfiguration(); + } + + // Update locale. + if (localeChanged) { + setLocaleFromConfiguration(); + } + } + + // Called by SQLiteConnectionPool only. + // When set to true, executing write operations will throw SQLiteException. + // Preparing statements that might write is ok, just don't execute them. + void setOnlyAllowReadOnlyOperations(boolean readOnly) { + mOnlyAllowReadOnlyOperations = readOnly; + } + + // Called by SQLiteConnectionPool only. + // Returns true if the prepared statement cache contains the specified SQL. + boolean isPreparedStatementInCache(String sql) { + return mPreparedStatementCache.get(sql) != null; + } + + /** + * Gets the unique id of this connection. + * @return The connection id. + */ + public int getConnectionId() { + return mConnectionId; + } + + /** + * Returns true if this is the primary database connection. + * @return True if this is the primary database connection. + */ + public boolean isPrimaryConnection() { + return mIsPrimaryConnection; + } + + /** + * Prepares a statement for execution but does not bind its parameters or execute it. + *

+ * This method can be used to check for syntax errors during compilation + * prior to execution of the statement. If the {@code outStatementInfo} argument + * is not null, the provided {@link SQLiteStatementInfo} object is populated + * with information about the statement. + *

+ * A prepared statement makes no reference to the arguments that may eventually + * be bound to it, consequently it it possible to cache certain prepared statements + * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable, + * then it will be stored in the cache for later. + *

+ * To take advantage of this behavior as an optimization, the connection pool + * provides a method to acquire a connection that already has a given SQL statement + * in its prepared statement cache so that it is ready for execution. + *

+ * + * @param sql The SQL statement to prepare. + * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate + * with information about the statement, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error. + */ + public void prepare(String sql, SQLiteStatementInfo outStatementInfo) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("prepare", sql, null); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + if (outStatementInfo != null) { + outStatementInfo.numParameters = statement.mNumParameters; + outStatementInfo.readOnly = statement.mReadOnly; + + final int columnCount = nativeGetColumnCount( + mConnectionPtr, statement.mStatementPtr); + if (columnCount == 0) { + outStatementInfo.columnNames = EMPTY_STRING_ARRAY; + } else { + outStatementInfo.columnNames = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + outStatementInfo.columnNames[i] = nativeGetColumnName( + mConnectionPtr, statement.mStatementPtr, i); + } + } + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that does not return a result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public void execute(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("execute", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + nativeExecute(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single long result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a long, or zero if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLong(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForLong", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single {@link String} result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a String, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public String executeForString(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForString", sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a single BLOB result as a + * file descriptor to a shared memory region. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The file descriptor for a shared memory region that contains + * the value of the first column in the first row of the result set as a BLOB, + * or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForBlobFileDescriptor", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + int fd = nativeExecuteForBlobFileDescriptor( + mConnectionPtr, statement.mStatementPtr); + return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null; + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement that returns a count of the number of rows + * that were changed. Use for UPDATE or DELETE SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were changed. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + int changedRows = 0; + final int cookie = mRecentOperations.beginOperation("executeForChangedRowCount", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + changedRows = nativeExecuteForChangedRowCount( + mConnectionPtr, statement.mStatementPtr); + return changedRows; + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + if (mRecentOperations.endOperationDeferLog(cookie)) { + mRecentOperations.logOperation(cookie, "changedRows=" + changedRows); + } + } + } + + /** + * Executes a statement that returns the row id of the last row inserted + * by the statement. Use for INSERT SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The row id of the last row that was inserted, or 0 if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + final int cookie = mRecentOperations.beginOperation("executeForLastInsertedRowId", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + return nativeExecuteForLastInsertedRowId( + mConnectionPtr, statement.mStatementPtr); + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + mRecentOperations.endOperation(cookie); + } + } + + /** + * Executes a statement and populates the specified {@link CursorWindow} + * with a range of results. Returns the number of rows that were counted + * during query execution. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param window The cursor window to clear and fill. + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled + * so that it does. Must be greater than or equal to startPos. + * @param countAllRows True to count all rows that the query would return + * regagless of whether they fit in the window. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were counted during query execution. Might + * not be all rows in the result set unless countAllRows is true. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + if (window == null) { + throw new IllegalArgumentException("window must not be null."); + } + + window.acquireReference(); + try { + int actualPos = -1; + int countedRows = -1; + int filledRows = -1; + final int cookie = mRecentOperations.beginOperation("executeForCursorWindow", + sql, bindArgs); + try { + final PreparedStatement statement = acquirePreparedStatement(sql); + try { + throwIfStatementForbidden(statement); + bindArguments(statement, bindArgs); + applyBlockGuardPolicy(statement); + attachCancellationSignal(cancellationSignal); + try { + final long result = nativeExecuteForCursorWindow( + mConnectionPtr, statement.mStatementPtr, window, + startPos, requiredPos, countAllRows); + actualPos = (int)(result >> 32); + countedRows = (int)result; + filledRows = window.getNumRows(); + window.setStartPosition(actualPos); + return countedRows; + } finally { + detachCancellationSignal(cancellationSignal); + } + } finally { + releasePreparedStatement(statement); + } + } catch (RuntimeException ex) { + mRecentOperations.failOperation(cookie, ex); + throw ex; + } finally { + if (mRecentOperations.endOperationDeferLog(cookie)) { + mRecentOperations.logOperation(cookie, "window='" + window + + "', startPos=" + startPos + + ", actualPos=" + actualPos + + ", filledRows=" + filledRows + + ", countedRows=" + countedRows); + } + } + } finally { + window.releaseReference(); + } + } + + private PreparedStatement acquirePreparedStatement(String sql) { + PreparedStatement statement = mPreparedStatementCache.get(sql); + boolean skipCache = false; + if (statement != null) { + if (!statement.mInUse) { + return statement; + } + // The statement is already in the cache but is in use (this statement appears + // to be not only re-entrant but recursive!). So prepare a new copy of the + // statement but do not cache it. + skipCache = true; + } + + final long statementPtr = nativePrepareStatement(mConnectionPtr, sql); + try { + final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr); + final int type = DatabaseUtils.getSqlStatementType(sql); + final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); + statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly); + if (!skipCache && isCacheable(type)) { + mPreparedStatementCache.put(sql, statement); + statement.mInCache = true; + } + } catch (RuntimeException ex) { + // Finalize the statement if an exception occurred and we did not add + // it to the cache. If it is already in the cache, then leave it there. + if (statement == null || !statement.mInCache) { + nativeFinalizeStatement(mConnectionPtr, statementPtr); + } + throw ex; + } + statement.mInUse = true; + return statement; + } + + private void releasePreparedStatement(PreparedStatement statement) { + statement.mInUse = false; + if (statement.mInCache) { + try { + nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr); + } catch (SQLiteException ex) { + // The statement could not be reset due to an error. Remove it from the cache. + // When remove() is called, the cache will invoke its entryRemoved() callback, + // which will in turn call finalizePreparedStatement() to finalize and + // recycle the statement. + if (DEBUG) { + Log.d(TAG, "Could not reset prepared statement due to an exception. " + + "Removing it from the cache. SQL: " + + trimSqlForDisplay(statement.mSql), ex); + } + + mPreparedStatementCache.remove(statement.mSql); + } + } else { + finalizePreparedStatement(statement); + } + } + + private void finalizePreparedStatement(PreparedStatement statement) { + nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr); + recyclePreparedStatement(statement); + } + + private void attachCancellationSignal(CancellationSignal cancellationSignal) { + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + + mCancellationSignalAttachCount += 1; + if (mCancellationSignalAttachCount == 1) { + // Reset cancellation flag before executing the statement. + nativeResetCancel(mConnectionPtr, true /*cancelable*/); + + // After this point, onCancel() may be called concurrently. + cancellationSignal.setOnCancelListener(this); + } + } + } + + private void detachCancellationSignal(CancellationSignal cancellationSignal) { + if (cancellationSignal != null) { + assert mCancellationSignalAttachCount > 0; + + mCancellationSignalAttachCount -= 1; + if (mCancellationSignalAttachCount == 0) { + // After this point, onCancel() cannot be called concurrently. + cancellationSignal.setOnCancelListener(null); + + // Reset cancellation flag after executing the statement. + nativeResetCancel(mConnectionPtr, false /*cancelable*/); + } + } + } + + // CancellationSignal.OnCancelListener callback. + // This method may be called on a different thread than the executing statement. + // However, it will only be called between calls to attachCancellationSignal and + // detachCancellationSignal, while a statement is executing. We can safely assume + // that the SQLite connection is still alive. + @Override + public void onCancel() { + nativeCancel(mConnectionPtr); + } + + private void bindArguments(PreparedStatement statement, Object[] bindArgs) { + final int count = bindArgs != null ? bindArgs.length : 0; + if (count != statement.mNumParameters) { + throw new SQLiteBindOrColumnIndexOutOfRangeException( + "Expected " + statement.mNumParameters + " bind arguments but " + + count + " were provided."); + } + if (count == 0) { + return; + } + + final long statementPtr = statement.mStatementPtr; + for (int i = 0; i < count; i++) { + final Object arg = bindArgs[i]; + switch (DatabaseUtils.getTypeOfObject(arg)) { + case Cursor.FIELD_TYPE_NULL: + nativeBindNull(mConnectionPtr, statementPtr, i + 1); + break; + case Cursor.FIELD_TYPE_INTEGER: + nativeBindLong(mConnectionPtr, statementPtr, i + 1, + ((Number)arg).longValue()); + break; + case Cursor.FIELD_TYPE_FLOAT: + nativeBindDouble(mConnectionPtr, statementPtr, i + 1, + ((Number)arg).doubleValue()); + break; + case Cursor.FIELD_TYPE_BLOB: + nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg); + break; + case Cursor.FIELD_TYPE_STRING: + default: + if (arg instanceof Boolean) { + // Provide compatibility with legacy applications which may pass + // Boolean values in bind args. + nativeBindLong(mConnectionPtr, statementPtr, i + 1, + ((Boolean)arg).booleanValue() ? 1 : 0); + } else { + nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString()); + } + break; + } + } + } + + private void throwIfStatementForbidden(PreparedStatement statement) { + if (mOnlyAllowReadOnlyOperations && !statement.mReadOnly) { + throw new SQLiteException("Cannot execute this statement because it " + + "might modify the database but the connection is read-only."); + } + } + + private static boolean isCacheable(int statementType) { + if (statementType == DatabaseUtils.STATEMENT_UPDATE + || statementType == DatabaseUtils.STATEMENT_SELECT) { + return true; + } + return false; + } + + private void applyBlockGuardPolicy(PreparedStatement statement) { + } + + /** + * Dumps debugging information about this connection. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + public void dump(Printer printer, boolean verbose) { + dumpUnsafe(printer, verbose); + } + + /** + * Dumps debugging information about this connection, in the case where the + * caller might not actually own the connection. + * + * This function is written so that it may be called by a thread that does not + * own the connection. We need to be very careful because the connection state is + * not synchronized. + * + * At worst, the method may return stale or slightly wrong data, however + * it should not crash. This is ok as it is only used for diagnostic purposes. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + void dumpUnsafe(Printer printer, boolean verbose) { + printer.println("Connection #" + mConnectionId + ":"); + if (verbose) { + printer.println(" connectionPtr: 0x" + Long.toHexString(mConnectionPtr)); + } + printer.println(" isPrimaryConnection: " + mIsPrimaryConnection); + printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations); + + mRecentOperations.dump(printer, verbose); + + if (verbose) { + mPreparedStatementCache.dump(printer); + } + } + + /** + * Describes the currently executing operation, in the case where the + * caller might not actually own the connection. + * + * This function is written so that it may be called by a thread that does not + * own the connection. We need to be very careful because the connection state is + * not synchronized. + * + * At worst, the method may return stale or slightly wrong data, however + * it should not crash. This is ok as it is only used for diagnostic purposes. + * + * @return A description of the current operation including how long it has been running, + * or null if none. + */ + String describeCurrentOperationUnsafe() { + return mRecentOperations.describeCurrentOperation(); + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + void collectDbStats(ArrayList dbStatsList) { + // Get information about the main database. + int lookaside = nativeGetDbLookaside(mConnectionPtr); + long pageCount = 0; + long pageSize = 0; + try { + pageCount = executeForLong("PRAGMA page_count;", null, null); + pageSize = executeForLong("PRAGMA page_size;", null, null); + } catch (SQLiteException ex) { + // Ignore. + } + dbStatsList.add(getMainDbStatsUnsafe(lookaside, pageCount, pageSize)); + + // Get information about attached databases. + // We ignore the first row in the database list because it corresponds to + // the main database which we have already described. + CursorWindow window = new CursorWindow("collectDbStats"); + try { + executeForCursorWindow("PRAGMA database_list;", null, window, 0, 0, false, null); + for (int i = 1; i < window.getNumRows(); i++) { + String name = window.getString(i, 1); + String path = window.getString(i, 2); + pageCount = 0; + pageSize = 0; + try { + pageCount = executeForLong("PRAGMA " + name + ".page_count;", null, null); + pageSize = executeForLong("PRAGMA " + name + ".page_size;", null, null); + } catch (SQLiteException ex) { + // Ignore. + } + String label = " (attached) " + name; + if (!path.isEmpty()) { + label += ": " + path; + } + dbStatsList.add(new DbStats(label, pageCount, pageSize, 0, 0, 0, 0)); + } + } catch (SQLiteException ex) { + // Ignore. + } finally { + window.close(); + } + } + + /** + * Collects statistics about database connection memory usage, in the case where the + * caller might not actually own the connection. + * + * @return The statistics object, never null. + */ + void collectDbStatsUnsafe(ArrayList dbStatsList) { + dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0)); + } + + private DbStats getMainDbStatsUnsafe(int lookaside, long pageCount, long pageSize) { + // The prepared statement cache is thread-safe so we can access its statistics + // even if we do not own the database connection. + String label = mConfiguration.path; + if (!mIsPrimaryConnection) { + label += " (" + mConnectionId + ")"; + } + return new DbStats(label, pageCount, pageSize, lookaside, + mPreparedStatementCache.hitCount(), + mPreparedStatementCache.missCount(), + mPreparedStatementCache.size()); + } + + @Override + public String toString() { + return "SQLiteConnection: " + mConfiguration.path + " (" + mConnectionId + ")"; + } + + private PreparedStatement obtainPreparedStatement(String sql, long statementPtr, + int numParameters, int type, boolean readOnly) { + PreparedStatement statement = mPreparedStatementPool; + if (statement != null) { + mPreparedStatementPool = statement.mPoolNext; + statement.mPoolNext = null; + statement.mInCache = false; + } else { + statement = new PreparedStatement(); + } + statement.mSql = sql; + statement.mStatementPtr = statementPtr; + statement.mNumParameters = numParameters; + statement.mType = type; + statement.mReadOnly = readOnly; + return statement; + } + + private void recyclePreparedStatement(PreparedStatement statement) { + statement.mSql = null; + statement.mPoolNext = mPreparedStatementPool; + mPreparedStatementPool = statement; + } + + private static String trimSqlForDisplay(String sql) { + // Note: Creating and caching a regular expression is expensive at preload-time + // and stops compile-time initialization. This pattern is only used when + // dumping the connection, which is a rare (mainly error) case. So: + // DO NOT CACHE. + return sql.replaceAll("[\\s]*\\n+[\\s]*", " "); + } + + /** + * Holder type for a prepared statement. + * + * Although this object holds a pointer to a native statement object, it + * does not have a finalizer. This is deliberate. The {@link SQLiteConnection} + * owns the statement object and will take care of freeing it when needed. + * In particular, closing the connection requires a guarantee of deterministic + * resource disposal because all native statement objects must be freed before + * the native database object can be closed. So no finalizers here. + */ + private static final class PreparedStatement { + // Next item in pool. + public PreparedStatement mPoolNext; + + // The SQL from which the statement was prepared. + public String mSql; + + // The native sqlite3_stmt object pointer. + // Lifetime is managed explicitly by the connection. + public long mStatementPtr; + + // The number of parameters that the prepared statement has. + public int mNumParameters; + + // The statement type. + public int mType; + + // True if the statement is read-only. + public boolean mReadOnly; + + // True if the statement is in the cache. + public boolean mInCache; + + // True if the statement is in use (currently executing). + // We need this flag because due to the use of custom functions in triggers, it's + // possible for SQLite calls to be re-entrant. Consequently we need to prevent + // in use statements from being finalized until they are no longer in use. + public boolean mInUse; + } + + private final class PreparedStatementCache + extends LruCache { + public PreparedStatementCache(int size) { + super(size); + } + + @Override + protected void entryRemoved(boolean evicted, String key, + PreparedStatement oldValue, PreparedStatement newValue) { + oldValue.mInCache = false; + if (!oldValue.mInUse) { + finalizePreparedStatement(oldValue); + } + } + + public void dump(Printer printer) { + printer.println(" Prepared statement cache:"); + Map cache = snapshot(); + if (!cache.isEmpty()) { + int i = 0; + for (Map.Entry entry : cache.entrySet()) { + PreparedStatement statement = entry.getValue(); + if (statement.mInCache) { // might be false due to a race with entryRemoved + String sql = entry.getKey(); + printer.println(" " + i + ": statementPtr=0x" + + Long.toHexString(statement.mStatementPtr) + + ", numParameters=" + statement.mNumParameters + + ", type=" + statement.mType + + ", readOnly=" + statement.mReadOnly + + ", sql=\"" + trimSqlForDisplay(sql) + "\""); + } + i += 1; + } + } else { + printer.println(" "); + } + } + } + + private static final class OperationLog { + private static final int MAX_RECENT_OPERATIONS = 20; + private static final int COOKIE_GENERATION_SHIFT = 8; + private static final int COOKIE_INDEX_MASK = 0xff; + + private final Operation[] mOperations = new Operation[MAX_RECENT_OPERATIONS]; + private int mIndex; + private int mGeneration; + + public int beginOperation(String kind, String sql, Object[] bindArgs) { + synchronized (mOperations) { + final int index = (mIndex + 1) % MAX_RECENT_OPERATIONS; + Operation operation = mOperations[index]; + if (operation == null) { + operation = new Operation(); + mOperations[index] = operation; + } else { + operation.mFinished = false; + operation.mException = null; + if (operation.mBindArgs != null) { + operation.mBindArgs.clear(); + } + } + operation.mStartWallTime = System.currentTimeMillis(); + operation.mStartTime = SystemClock.uptimeMillis(); + operation.mKind = kind; + operation.mSql = sql; + if (bindArgs != null) { + if (operation.mBindArgs == null) { + operation.mBindArgs = new ArrayList(); + } else { + operation.mBindArgs.clear(); + } + for (int i = 0; i < bindArgs.length; i++) { + final Object arg = bindArgs[i]; + if (arg != null && arg instanceof byte[]) { + // Don't hold onto the real byte array longer than necessary. + operation.mBindArgs.add(EMPTY_BYTE_ARRAY); + } else { + operation.mBindArgs.add(arg); + } + } + } + operation.mCookie = newOperationCookieLocked(index); + mIndex = index; + return operation.mCookie; + } + } + + public void failOperation(int cookie, Exception ex) { + synchronized (mOperations) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mException = ex; + } + } + } + + public void endOperation(int cookie) { + synchronized (mOperations) { + if (endOperationDeferLogLocked(cookie)) { + logOperationLocked(cookie, null); + } + } + } + + public boolean endOperationDeferLog(int cookie) { + synchronized (mOperations) { + return endOperationDeferLogLocked(cookie); + } + } + + public void logOperation(int cookie, String detail) { + synchronized (mOperations) { + logOperationLocked(cookie, detail); + } + } + + private boolean endOperationDeferLogLocked(int cookie) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mEndTime = SystemClock.uptimeMillis(); + operation.mFinished = true; + return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( + operation.mEndTime - operation.mStartTime); + } + return false; + } + + private void logOperationLocked(int cookie, String detail) { + final Operation operation = getOperationLocked(cookie); + StringBuilder msg = new StringBuilder(); + operation.describe(msg, false); + if (detail != null) { + msg.append(", ").append(detail); + } + Log.d(TAG, msg.toString()); + } + + private int newOperationCookieLocked(int index) { + final int generation = mGeneration++; + return generation << COOKIE_GENERATION_SHIFT | index; + } + + private Operation getOperationLocked(int cookie) { + final int index = cookie & COOKIE_INDEX_MASK; + final Operation operation = mOperations[index]; + return operation.mCookie == cookie ? operation : null; + } + + public String describeCurrentOperation() { + synchronized (mOperations) { + final Operation operation = mOperations[mIndex]; + if (operation != null && !operation.mFinished) { + StringBuilder msg = new StringBuilder(); + operation.describe(msg, false); + return msg.toString(); + } + return null; + } + } + + public void dump(Printer printer, boolean verbose) { + synchronized (mOperations) { + printer.println(" Most recently executed operations:"); + int index = mIndex; + Operation operation = mOperations[index]; + if (operation != null) { + int n = 0; + do { + StringBuilder msg = new StringBuilder(); + msg.append(" ").append(n).append(": ["); + msg.append(operation.getFormattedStartTime()); + msg.append("] "); + operation.describe(msg, verbose); + printer.println(msg.toString()); + + if (index > 0) { + index -= 1; + } else { + index = MAX_RECENT_OPERATIONS - 1; + } + n += 1; + operation = mOperations[index]; + } while (operation != null && n < MAX_RECENT_OPERATIONS); + } else { + printer.println(" "); + } + } + } + } + + private static final class Operation { + // Trim all SQL statements to 256 characters inside the trace marker. + // This limit gives plenty of context while leaving space for other + // entries in the trace buffer (and ensures atrace doesn't truncate the + // marker for us, potentially losing metadata in the process). + private static final int MAX_TRACE_METHOD_NAME_LEN = 256; + + public long mStartWallTime; // in System.currentTimeMillis() + public long mStartTime; // in SystemClock.uptimeMillis(); + public long mEndTime; // in SystemClock.uptimeMillis(); + public String mKind; + public String mSql; + public ArrayList mBindArgs; + public boolean mFinished; + public Exception mException; + public int mCookie; + + public void describe(StringBuilder msg, boolean verbose) { + msg.append(mKind); + if (mFinished) { + msg.append(" took ").append(mEndTime - mStartTime).append("ms"); + } else { + msg.append(" started ").append(System.currentTimeMillis() - mStartWallTime) + .append("ms ago"); + } + msg.append(" - ").append(getStatus()); + if (mSql != null) { + msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\""); + } + if (verbose && mBindArgs != null && mBindArgs.size() != 0) { + msg.append(", bindArgs=["); + final int count = mBindArgs.size(); + for (int i = 0; i < count; i++) { + final Object arg = mBindArgs.get(i); + if (i != 0) { + msg.append(", "); + } + if (arg == null) { + msg.append("null"); + } else if (arg instanceof byte[]) { + msg.append(""); + } else if (arg instanceof String) { + msg.append("\"").append((String)arg).append("\""); + } else { + msg.append(arg); + } + } + msg.append("]"); + } + if (mException != null) { + msg.append(", exception=\"").append(mException.getMessage()).append("\""); + } + } + + private String getStatus() { + if (!mFinished) { + return "running"; + } + return mException != null ? "failed" : "succeeded"; + } + + private String getTraceMethodName() { + String methodName = mKind + " " + mSql; + if (methodName.length() > MAX_TRACE_METHOD_NAME_LEN) + return methodName.substring(0, MAX_TRACE_METHOD_NAME_LEN); + return methodName; + } + + private String getFormattedStartTime() { + // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, and is + // relatively expensive to create during preloading. This method is only used + // when dumping a connection, which is a rare (mainly error) case. So: + // DO NOT CACHE. + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(mStartWallTime)); + } + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteConnectionPool.java b/src/api-impl/android/database/sqlite/SQLiteConnectionPool.java new file mode 100644 index 00000000..19a0b40b --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteConnectionPool.java @@ -0,0 +1,1086 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import dalvik.system.CloseGuard; + +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.os.SystemClock; +import android.util.Log; +import android.util.Printer; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +/** + * Maintains a pool of active SQLite database connections. + *

+ * At any given time, a connection is either owned by the pool, or it has been + * acquired by a {@link SQLiteSession}. When the {@link SQLiteSession} is + * finished with the connection it is using, it must return the connection + * back to the pool. + *

+ * The pool holds strong references to the connections it owns. However, + * it only holds weak references to the connections that sessions + * have acquired from it. Using weak references in the latter case ensures + * that the connection pool can detect when connections have been improperly + * abandoned so that it can create new connections to replace them if needed. + *

+ * The connection pool is thread-safe (but the connections themselves are not). + *

+ * + *

Exception safety

+ *

+ * This code attempts to maintain the invariant that opened connections are + * always owned. Unfortunately that means it needs to handle exceptions + * all over to ensure that broken connections get cleaned up. Most + * operations invokving SQLite can throw {@link SQLiteException} or other + * runtime exceptions. This is a bit of a pain to deal with because the compiler + * cannot help us catch missing exception handling code. + *

+ * The general rule for this file: If we are making calls out to + * {@link SQLiteConnection} then we must be prepared to handle any + * runtime exceptions it might throw at us. Note that out-of-memory + * is an {@link Error}, not a {@link RuntimeException}. We don't trouble ourselves + * handling out of memory because it is hard to do anything at all sensible then + * and most likely the VM is about to crash. + *

+ * + * @hide + */ +public final class SQLiteConnectionPool implements Closeable { + private static final String TAG = "SQLiteConnectionPool"; + + // Amount of time to wait in milliseconds before unblocking acquireConnection + // and logging a message about the connection pool being busy. + private static final long CONNECTION_POOL_BUSY_MILLIS = 30 * 1000; // 30 seconds + + private final CloseGuard mCloseGuard = CloseGuard.get(); + + private final Object mLock = new Object(); + private final AtomicBoolean mConnectionLeaked = new AtomicBoolean(); + private final SQLiteDatabaseConfiguration mConfiguration; + private int mMaxConnectionPoolSize; + private boolean mIsOpen; + private int mNextConnectionId; + + private ConnectionWaiter mConnectionWaiterPool; + private ConnectionWaiter mConnectionWaiterQueue; + + // Strong references to all available connections. + private final ArrayList mAvailableNonPrimaryConnections = + new ArrayList(); + private SQLiteConnection mAvailablePrimaryConnection; + + // Describes what should happen to an acquired connection when it is returned to the pool. + enum AcquiredConnectionStatus { + // The connection should be returned to the pool as usual. + NORMAL, + + // The connection must be reconfigured before being returned. + RECONFIGURE, + + // The connection must be closed and discarded. + DISCARD, + } + + // Weak references to all acquired connections. The associated value + // indicates whether the connection must be reconfigured before being + // returned to the available connection list or discarded. + // For example, the prepared statement cache size may have changed and + // need to be updated in preparation for the next client. + private final WeakHashMap mAcquiredConnections = + new WeakHashMap(); + + /** + * Connection flag: Read-only. + *

+ * This flag indicates that the connection will only be used to + * perform read-only operations. + *

+ */ + public static final int CONNECTION_FLAG_READ_ONLY = 1 << 0; + + /** + * Connection flag: Primary connection affinity. + *

+ * This flag indicates that the primary connection is required. + * This flag helps support legacy applications that expect most data modifying + * operations to be serialized by locking the primary database connection. + * Setting this flag essentially implements the old "db lock" concept by preventing + * an operation from being performed until it can obtain exclusive access to + * the primary connection. + *

+ */ + public static final int CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY = 1 << 1; + + /** + * Connection flag: Connection is being used interactively. + *

+ * This flag indicates that the connection is needed by the UI thread. + * The connection pool can use this flag to elevate the priority + * of the database connection request. + *

+ */ + public static final int CONNECTION_FLAG_INTERACTIVE = 1 << 2; + + private SQLiteConnectionPool(SQLiteDatabaseConfiguration configuration) { + mConfiguration = new SQLiteDatabaseConfiguration(configuration); + setMaxConnectionPoolSizeLocked(); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } + } + + /** + * Opens a connection pool for the specified database. + * + * @param configuration The database configuration. + * @return The connection pool. + * + * @throws SQLiteException if a database error occurs. + */ + public static SQLiteConnectionPool open(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + // Create the pool. + SQLiteConnectionPool pool = new SQLiteConnectionPool(configuration); + pool.open(); // might throw + return pool; + } + + // Might throw + private void open() { + // Open the primary connection. + // This might throw if the database is corrupt. + mAvailablePrimaryConnection = openConnectionLocked(mConfiguration, + true /*primaryConnection*/); // might throw + + // Mark the pool as being open for business. + mIsOpen = true; + mCloseGuard.open("close"); + } + + /** + * Closes the connection pool. + *

+ * When the connection pool is closed, it will refuse all further requests + * to acquire connections. All connections that are currently available in + * the pool are closed immediately. Any connections that are still in use + * will be closed as soon as they are returned to the pool. + *

+ * + * @throws IllegalStateException if the pool has been closed. + */ + public void close() { + dispose(false); + } + + private void dispose(boolean finalized) { + if (mCloseGuard != null) { + if (finalized) { + mCloseGuard.warnIfOpen(); + } + mCloseGuard.close(); + } + + if (!finalized) { + // Close all connections. We don't need (or want) to do this + // when finalized because we don't know what state the connections + // themselves will be in. The finalizer is really just here for CloseGuard. + // The connections will take care of themselves when their own finalizers run. + synchronized (mLock) { + throwIfClosedLocked(); + + mIsOpen = false; + + closeAvailableConnectionsAndLogExceptionsLocked(); + + final int pendingCount = mAcquiredConnections.size(); + if (pendingCount != 0) { + Log.i(TAG, "The connection pool for " + mConfiguration.label + + " has been closed but there are still " + + pendingCount + " connections in use. They will be closed " + + "as they are released back to the pool."); + } + + wakeConnectionWaitersLocked(); + } + } + } + + /** + * Reconfigures the database configuration of the connection pool and all of its + * connections. + *

+ * Configuration changes are propagated down to connections immediately if + * they are available or as soon as they are released. This includes changes + * that affect the size of the pool. + *

+ * + * @param configuration The new configuration. + * + * @throws IllegalStateException if the pool has been closed. + */ + public void reconfigure(SQLiteDatabaseConfiguration configuration) { + if (configuration == null) { + throw new IllegalArgumentException("configuration must not be null."); + } + + synchronized (mLock) { + throwIfClosedLocked(); + + boolean walModeChanged = ((configuration.openFlags ^ mConfiguration.openFlags) + & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0; + if (walModeChanged) { + // WAL mode can only be changed if there are no acquired connections + // because we need to close all but the primary connection first. + if (!mAcquiredConnections.isEmpty()) { + throw new IllegalStateException("Write Ahead Logging (WAL) mode cannot " + + "be enabled or disabled while there are transactions in " + + "progress. Finish all transactions and release all active " + + "database connections first."); + } + + // Close all non-primary connections. This should happen immediately + // because none of them are in use. + closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); + assert mAvailableNonPrimaryConnections.isEmpty(); + } + + boolean foreignKeyModeChanged = configuration.foreignKeyConstraintsEnabled + != mConfiguration.foreignKeyConstraintsEnabled; + if (foreignKeyModeChanged) { + // Foreign key constraints can only be changed if there are no transactions + // in progress. To make this clear, we throw an exception if there are + // any acquired connections. + if (!mAcquiredConnections.isEmpty()) { + throw new IllegalStateException("Foreign Key Constraints cannot " + + "be enabled or disabled while there are transactions in " + + "progress. Finish all transactions and release all active " + + "database connections first."); + } + } + + if (mConfiguration.openFlags != configuration.openFlags) { + // If we are changing open flags and WAL mode at the same time, then + // we have no choice but to close the primary connection beforehand + // because there can only be one connection open when we change WAL mode. + if (walModeChanged) { + closeAvailableConnectionsAndLogExceptionsLocked(); + } + + // Try to reopen the primary connection using the new open flags then + // close and discard all existing connections. + // This might throw if the database is corrupt or cannot be opened in + // the new mode in which case existing connections will remain untouched. + SQLiteConnection newPrimaryConnection = openConnectionLocked(configuration, + true /*primaryConnection*/); // might throw + + closeAvailableConnectionsAndLogExceptionsLocked(); + discardAcquiredConnectionsLocked(); + + mAvailablePrimaryConnection = newPrimaryConnection; + mConfiguration.updateParametersFrom(configuration); + setMaxConnectionPoolSizeLocked(); + } else { + // Reconfigure the database connections in place. + mConfiguration.updateParametersFrom(configuration); + setMaxConnectionPoolSizeLocked(); + + closeExcessConnectionsAndLogExceptionsLocked(); + reconfigureAllConnectionsLocked(); + } + + wakeConnectionWaitersLocked(); + } + } + + /** + * Acquires a connection from the pool. + *

+ * The caller must call {@link #releaseConnection} to release the connection + * back to the pool when it is finished. Failure to do so will result + * in much unpleasantness. + *

+ * + * @param sql If not null, try to find a connection that already has + * the specified SQL statement in its prepared statement cache. + * @param connectionFlags The connection request flags. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The connection that was acquired, never null. + * + * @throws IllegalStateException if the pool has been closed. + * @throws SQLiteException if a database error occurs. + * @throws OperationCanceledException if the operation was canceled. + */ + public SQLiteConnection acquireConnection(String sql, int connectionFlags, + CancellationSignal cancellationSignal) { + return waitForConnection(sql, connectionFlags, cancellationSignal); + } + + /** + * Releases a connection back to the pool. + *

+ * It is ok to call this method after the pool has closed, to release + * connections that were still in use at the time of closure. + *

+ * + * @param connection The connection to release. Must not be null. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public void releaseConnection(SQLiteConnection connection) { + synchronized (mLock) { + AcquiredConnectionStatus status = mAcquiredConnections.remove(connection); + if (status == null) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + closeConnectionAndLogExceptionsLocked(connection); + } else if (connection.isPrimaryConnection()) { + if (recycleConnectionLocked(connection, status)) { + assert mAvailablePrimaryConnection == null; + mAvailablePrimaryConnection = connection; + } + wakeConnectionWaitersLocked(); + } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) { + closeConnectionAndLogExceptionsLocked(connection); + } else { + if (recycleConnectionLocked(connection, status)) { + mAvailableNonPrimaryConnections.add(connection); + } + wakeConnectionWaitersLocked(); + } + } + } + + // Can't throw. + private boolean recycleConnectionLocked(SQLiteConnection connection, + AcquiredConnectionStatus status) { + if (status == AcquiredConnectionStatus.RECONFIGURE) { + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released connection, closing it: " + + connection, ex); + status = AcquiredConnectionStatus.DISCARD; + } + } + if (status == AcquiredConnectionStatus.DISCARD) { + closeConnectionAndLogExceptionsLocked(connection); + return false; + } + return true; + } + + /** + * Returns true if the session should yield the connection due to + * contention over available database connections. + * + * @param connection The connection owned by the session. + * @param connectionFlags The connection request flags. + * @return True if the session should yield its connection. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) { + synchronized (mLock) { + if (!mAcquiredConnections.containsKey(connection)) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + return false; + } + + return isSessionBlockingImportantConnectionWaitersLocked( + connection.isPrimaryConnection(), connectionFlags); + } + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + public void collectDbStats(ArrayList dbStatsList) { + synchronized (mLock) { + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAvailableNonPrimaryConnections) { + connection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + connection.collectDbStatsUnsafe(dbStatsList); + } + } + } + + // Might throw. + private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration, + boolean primaryConnection) { + final int connectionId = mNextConnectionId++; + return SQLiteConnection.open(this, configuration, + connectionId, primaryConnection); // might throw + } + + void onConnectionLeaked() { + // This code is running inside of the SQLiteConnection finalizer. + // + // We don't know whether it is just the connection that has been finalized (and leaked) + // or whether the connection pool has also been or is about to be finalized. + // Consequently, it would be a bad idea to try to grab any locks or to + // do any significant work here. So we do the simplest possible thing and + // set a flag. waitForConnection() periodically checks this flag (when it + // times out) so that it can recover from leaked connections and wake + // itself or other threads up if necessary. + // + // You might still wonder why we don't try to do more to wake up the waiters + // immediately. First, as explained above, it would be hard to do safely + // unless we started an extra Thread to function as a reference queue. Second, + // this is never supposed to happen in normal operation. Third, there is no + // guarantee that the GC will actually detect the leak in a timely manner so + // it's not all that important that we recover from the leak in a timely manner + // either. Fourth, if a badly behaved application finds itself hung waiting for + // several seconds while waiting for a leaked connection to be detected and recreated, + // then perhaps its authors will have added incentive to fix the problem! + + Log.w(TAG, "A SQLiteConnection object for database '" + + mConfiguration.label + "' was leaked! Please fix your application " + + "to end transactions in progress properly and to close the database " + + "when it is no longer needed."); + + mConnectionLeaked.set(true); + } + + // Can't throw. + private void closeAvailableConnectionsAndLogExceptionsLocked() { + closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); + + if (mAvailablePrimaryConnection != null) { + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + } + + // Can't throw. + private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i)); + } + mAvailableNonPrimaryConnections.clear(); + } + + // Can't throw. + private void closeExcessConnectionsAndLogExceptionsLocked() { + int availableCount = mAvailableNonPrimaryConnections.size(); + while (availableCount-- > mMaxConnectionPoolSize - 1) { + SQLiteConnection connection = + mAvailableNonPrimaryConnections.remove(availableCount); + closeConnectionAndLogExceptionsLocked(connection); + } + } + + // Can't throw. + private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) { + try { + connection.close(); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to close connection, its fate is now in the hands " + + "of the merciful GC: " + connection, ex); + } + } + + // Can't throw. + private void discardAcquiredConnectionsLocked() { + markAcquiredConnectionsLocked(AcquiredConnectionStatus.DISCARD); + } + + // Can't throw. + private void reconfigureAllConnectionsLocked() { + if (mAvailablePrimaryConnection != null) { + try { + mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " + + mAvailablePrimaryConnection, ex); + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + } + + int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i); + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + mAvailableNonPrimaryConnections.remove(i--); + count -= 1; + } + } + + markAcquiredConnectionsLocked(AcquiredConnectionStatus.RECONFIGURE); + } + + // Can't throw. + private void markAcquiredConnectionsLocked(AcquiredConnectionStatus status) { + if (!mAcquiredConnections.isEmpty()) { + ArrayList keysToUpdate = new ArrayList( + mAcquiredConnections.size()); + for (Map.Entry entry + : mAcquiredConnections.entrySet()) { + AcquiredConnectionStatus oldStatus = entry.getValue(); + if (status != oldStatus + && oldStatus != AcquiredConnectionStatus.DISCARD) { + keysToUpdate.add(entry.getKey()); + } + } + final int updateCount = keysToUpdate.size(); + for (int i = 0; i < updateCount; i++) { + mAcquiredConnections.put(keysToUpdate.get(i), status); + } + } + } + + // Might throw. + private SQLiteConnection waitForConnection(String sql, int connectionFlags, + CancellationSignal cancellationSignal) { + final boolean wantPrimaryConnection = + (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; + + final ConnectionWaiter waiter; + final int nonce; + synchronized (mLock) { + throwIfClosedLocked(); + + // Abort if canceled. + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + // Try to acquire a connection. + SQLiteConnection connection = null; + if (!wantPrimaryConnection) { + connection = tryAcquireNonPrimaryConnectionLocked( + sql, connectionFlags); // might throw + } + if (connection == null) { + connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw + } + if (connection != null) { + return connection; + } + + // No connections available. Enqueue a waiter in priority order. + final int priority = getPriority(connectionFlags); + final long startTime = SystemClock.uptimeMillis(); + waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, + priority, wantPrimaryConnection, sql, connectionFlags); + ConnectionWaiter predecessor = null; + ConnectionWaiter successor = mConnectionWaiterQueue; + while (successor != null) { + if (priority > successor.mPriority) { + waiter.mNext = successor; + break; + } + predecessor = successor; + successor = successor.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter; + } else { + mConnectionWaiterQueue = waiter; + } + + nonce = waiter.mNonce; + } + + // Set up the cancellation listener. + if (cancellationSignal != null) { + cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { + @Override + public void onCancel() { + synchronized (mLock) { + if (waiter.mNonce == nonce) { + cancelConnectionWaiterLocked(waiter); + } + } + } + }); + } + try { + // Park the thread until a connection is assigned or the pool is closed. + // Rethrow an exception from the wait, if we got one. + long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis; + for (;;) { + // Detect and recover from connection leaks. + if (mConnectionLeaked.compareAndSet(true, false)) { + synchronized (mLock) { + wakeConnectionWaitersLocked(); + } + } + + // Wait to be unparked (may already have happened), a timeout, or interruption. + LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L); + + // Clear the interrupted flag, just in case. + Thread.interrupted(); + + // Check whether we are done waiting yet. + synchronized (mLock) { + throwIfClosedLocked(); + + final SQLiteConnection connection = waiter.mAssignedConnection; + final RuntimeException ex = waiter.mException; + if (connection != null || ex != null) { + recycleConnectionWaiterLocked(waiter); + if (connection != null) { + return connection; + } + throw ex; // rethrow! + } + + final long now = SystemClock.uptimeMillis(); + if (now < nextBusyTimeoutTime) { + busyTimeoutMillis = now - nextBusyTimeoutTime; + } else { + logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); + busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + nextBusyTimeoutTime = now + busyTimeoutMillis; + } + } + } + } finally { + // Remove the cancellation listener. + if (cancellationSignal != null) { + cancellationSignal.setOnCancelListener(null); + } + } + } + + // Can't throw. + private void cancelConnectionWaiterLocked(ConnectionWaiter waiter) { + if (waiter.mAssignedConnection != null || waiter.mException != null) { + // Waiter is done waiting but has not woken up yet. + return; + } + + // Waiter must still be waiting. Dequeue it. + ConnectionWaiter predecessor = null; + ConnectionWaiter current = mConnectionWaiterQueue; + while (current != waiter) { + assert current != null; + predecessor = current; + current = current.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter.mNext; + } else { + mConnectionWaiterQueue = waiter.mNext; + } + + // Send the waiter an exception and unpark it. + waiter.mException = new OperationCanceledException(); + LockSupport.unpark(waiter.mThread); + + // Check whether removing this waiter will enable other waiters to make progress. + wakeConnectionWaitersLocked(); + } + + // Can't throw. + private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { + final Thread thread = Thread.currentThread(); + StringBuilder msg = new StringBuilder(); + msg.append("The connection pool for database '").append(mConfiguration.label); + msg.append("' has been unable to grant a connection to thread "); + msg.append(thread.getId()).append(" (").append(thread.getName()).append(") "); + msg.append("with flags 0x").append(Integer.toHexString(connectionFlags)); + msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n"); + + ArrayList requests = new ArrayList(); + int activeConnections = 0; + int idleConnections = 0; + if (!mAcquiredConnections.isEmpty()) { + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + String description = connection.describeCurrentOperationUnsafe(); + if (description != null) { + requests.add(description); + activeConnections += 1; + } else { + idleConnections += 1; + } + } + } + int availableConnections = mAvailableNonPrimaryConnections.size(); + if (mAvailablePrimaryConnection != null) { + availableConnections += 1; + } + + msg.append("Connections: ").append(activeConnections).append(" active, "); + msg.append(idleConnections).append(" idle, "); + msg.append(availableConnections).append(" available.\n"); + + if (!requests.isEmpty()) { + msg.append("\nRequests in progress:\n"); + for (String request : requests) { + msg.append(" ").append(request).append("\n"); + } + } + + Log.w(TAG, msg.toString()); + } + + // Can't throw. + private void wakeConnectionWaitersLocked() { + // Unpark all waiters that have requests that we can fulfill. + // This method is designed to not throw runtime exceptions, although we might send + // a waiter an exception for it to rethrow. + ConnectionWaiter predecessor = null; + ConnectionWaiter waiter = mConnectionWaiterQueue; + boolean primaryConnectionNotAvailable = false; + boolean nonPrimaryConnectionNotAvailable = false; + while (waiter != null) { + boolean unpark = false; + if (!mIsOpen) { + unpark = true; + } else { + try { + SQLiteConnection connection = null; + if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) { + connection = tryAcquireNonPrimaryConnectionLocked( + waiter.mSql, waiter.mConnectionFlags); // might throw + if (connection == null) { + nonPrimaryConnectionNotAvailable = true; + } + } + if (connection == null && !primaryConnectionNotAvailable) { + connection = tryAcquirePrimaryConnectionLocked( + waiter.mConnectionFlags); // might throw + if (connection == null) { + primaryConnectionNotAvailable = true; + } + } + if (connection != null) { + waiter.mAssignedConnection = connection; + unpark = true; + } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) { + // There are no connections available and the pool is still open. + // We cannot fulfill any more connection requests, so stop here. + break; + } + } catch (RuntimeException ex) { + // Let the waiter handle the exception from acquiring a connection. + waiter.mException = ex; + unpark = true; + } + } + + final ConnectionWaiter successor = waiter.mNext; + if (unpark) { + if (predecessor != null) { + predecessor.mNext = successor; + } else { + mConnectionWaiterQueue = successor; + } + waiter.mNext = null; + + LockSupport.unpark(waiter.mThread); + } else { + predecessor = waiter; + } + waiter = successor; + } + } + + // Might throw. + private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) { + // If the primary connection is available, acquire it now. + SQLiteConnection connection = mAvailablePrimaryConnection; + if (connection != null) { + mAvailablePrimaryConnection = null; + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Make sure that the primary connection actually exists and has just been acquired. + for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) { + if (acquiredConnection.isPrimaryConnection()) { + return null; + } + } + + // Uhoh. No primary connection! Either this is the first time we asked + // for it, or maybe it leaked? + connection = openConnectionLocked(mConfiguration, + true /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private SQLiteConnection tryAcquireNonPrimaryConnectionLocked( + String sql, int connectionFlags) { + // Try to acquire the next connection in the queue. + SQLiteConnection connection; + final int availableCount = mAvailableNonPrimaryConnections.size(); + if (availableCount > 1 && sql != null) { + // If we have a choice, then prefer a connection that has the + // prepared statement in its cache. + for (int i = 0; i < availableCount; i++) { + connection = mAvailableNonPrimaryConnections.get(i); + if (connection.isPreparedStatementInCache(sql)) { + mAvailableNonPrimaryConnections.remove(i); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + } + } + if (availableCount > 0) { + // Otherwise, just grab the next one. + connection = mAvailableNonPrimaryConnections.remove(availableCount - 1); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Expand the pool if needed. + int openConnections = mAcquiredConnections.size(); + if (mAvailablePrimaryConnection != null) { + openConnections += 1; + } + if (openConnections >= mMaxConnectionPoolSize) { + return null; + } + connection = openConnectionLocked(mConfiguration, + false /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) { + try { + final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0; + connection.setOnlyAllowReadOnlyOperations(readOnly); + + mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL); + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " + + connection +", connectionFlags=" + connectionFlags); + closeConnectionAndLogExceptionsLocked(connection); + throw ex; // rethrow! + } + } + + private boolean isSessionBlockingImportantConnectionWaitersLocked( + boolean holdingPrimaryConnection, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterQueue; + if (waiter != null) { + final int priority = getPriority(connectionFlags); + do { + // Only worry about blocked connections that have same or lower priority. + if (priority > waiter.mPriority) { + break; + } + + // If we are holding the primary connection then we are blocking the waiter. + // Likewise, if we are holding a non-primary connection and the waiter + // would accept a non-primary connection, then we are blocking the waier. + if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) { + return true; + } + + waiter = waiter.mNext; + } while (waiter != null); + } + return false; + } + + private static int getPriority(int connectionFlags) { + return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0; + } + + private void setMaxConnectionPoolSizeLocked() { + if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { + mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize(); + } else { + // TODO: We don't actually need to restrict the connection pool size to 1 + // for non-WAL databases. There might be reasons to use connection pooling + // with other journal modes. For now, enabling connection pooling and + // using WAL are the same thing in the API. + mMaxConnectionPoolSize = 1; + } + } + + private void throwIfClosedLocked() { + if (!mIsOpen) { + throw new IllegalStateException("Cannot perform this operation " + + "because the connection pool has been closed."); + } + } + + private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime, + int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterPool; + if (waiter != null) { + mConnectionWaiterPool = waiter.mNext; + waiter.mNext = null; + } else { + waiter = new ConnectionWaiter(); + } + waiter.mThread = thread; + waiter.mStartTime = startTime; + waiter.mPriority = priority; + waiter.mWantPrimaryConnection = wantPrimaryConnection; + waiter.mSql = sql; + waiter.mConnectionFlags = connectionFlags; + return waiter; + } + + private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) { + waiter.mNext = mConnectionWaiterPool; + waiter.mThread = null; + waiter.mSql = null; + waiter.mAssignedConnection = null; + waiter.mException = null; + waiter.mNonce += 1; + mConnectionWaiterPool = waiter; + } + + public void enableLocalizedCollators() { + synchronized (mLock) { + if( !mAcquiredConnections.isEmpty() || mAvailablePrimaryConnection==null ) { + throw new IllegalStateException( + "Cannot enable localized collators while database is in use" + ); + } + mAvailablePrimaryConnection.enableLocalizedCollators(); + } + } + + /** + * Dumps debugging information about this connection pool. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + public void dump(Printer printer, boolean verbose) { + Printer indentedPrinter = printer; + synchronized (mLock) { + printer.println("Connection pool for " + mConfiguration.path + ":"); + printer.println(" Open: " + mIsOpen); + printer.println(" Max connections: " + mMaxConnectionPoolSize); + + printer.println(" Available primary connection:"); + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.dump(indentedPrinter, verbose); + } else { + indentedPrinter.println(""); + } + + printer.println(" Available non-primary connections:"); + if (!mAvailableNonPrimaryConnections.isEmpty()) { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose); + } + } else { + indentedPrinter.println(""); + } + + printer.println(" Acquired connections:"); + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry entry : + mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + connection.dumpUnsafe(indentedPrinter, verbose); + indentedPrinter.println(" Status: " + entry.getValue()); + } + } else { + indentedPrinter.println(""); + } + + printer.println(" Connection waiters:"); + if (mConnectionWaiterQueue != null) { + int i = 0; + final long now = SystemClock.uptimeMillis(); + for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null; + waiter = waiter.mNext, i++) { + indentedPrinter.println(i + ": waited for " + + ((now - waiter.mStartTime) * 0.001f) + + " ms - thread=" + waiter.mThread + + ", priority=" + waiter.mPriority + + ", sql='" + waiter.mSql + "'"); + } + } else { + indentedPrinter.println(""); + } + } + } + + @Override + public String toString() { + return "SQLiteConnectionPool: " + mConfiguration.path; + } + + private static final class ConnectionWaiter { + public ConnectionWaiter mNext; + public Thread mThread; + public long mStartTime; + public int mPriority; + public boolean mWantPrimaryConnection; + public String mSql; + public int mConnectionFlags; + public SQLiteConnection mAssignedConnection; + public RuntimeException mException; + public int mNonce; + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteConstraintException.java b/src/api-impl/android/database/sqlite/SQLiteConstraintException.java new file mode 100644 index 00000000..3b87d8a6 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteConstraintException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that an integrity constraint was violated. + */ +public class SQLiteConstraintException extends SQLiteException { + public SQLiteConstraintException() {} + + public SQLiteConstraintException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteCursor.java b/src/api-impl/android/database/sqlite/SQLiteCursor.java index 031e8b51..3583c576 100644 --- a/src/api-impl/android/database/sqlite/SQLiteCursor.java +++ b/src/api-impl/android/database/sqlite/SQLiteCursor.java @@ -13,17 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ package android.database.sqlite; -// import android.database.AbstractWindowedCursor; -// import android.database.CursorWindow; -// import android.database.DatabaseUtils; -// import android.os.StrictMode; -import android.database.Cursor; +import android.database.AbstractWindowedCursor; +import android.database.CursorWindow; +import android.database.DatabaseUtils; + 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}. @@ -31,49 +36,30 @@ import java.util.Map; * 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*/ implements Cursor { +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 = "ZZZ"; + /** The name of the table to edit */ + private final String mEditTable; - /** - * The names of the columns in the rows - */ - private final String[] mColumns = {"AAA"}; + /** The names of the columns in the rows */ + private final String[] mColumns; - /** - * The query object for the cursor - */ - private final SQLiteQuery mQuery = null; + /** The query object for the cursor */ + private final SQLiteQuery mQuery; - /** - * The compiled query this cursor came from - */ - private final SQLiteCursorDriver mDriver = null; + /** The compiled query this cursor came from */ + private final SQLiteCursorDriver mDriver; - /** - * The number of rows in the cursor - */ + /** The number of rows in the cursor */ private int mCount = NO_COUNT; - /** - * The number of rows that can fit in the cursor window, 0 if unknown - */ - private int mCursorWindowCapacity = -1; + /** The number of rows that can fit in the cursor window, 0 if unknown */ + private int mCursorWindowCapacity; - /** - * A mapping of column names to column indices, to speed up lookups - */ - private Map mColumnNameMap = null; - - /** - * Used to find out where a cursor was allocated in case it never got released. - */ - private final Throwable mStackTrace = null; + /** A mapping of column names to column indices, to speed up lookups */ + private Map mColumnNameMap; /** * Execute a query and provide access to its result set through a Cursor @@ -105,23 +91,16 @@ public class SQLiteCursor /*extends AbstractWindowedCursor*/ implements Cursor { * @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 (StrictMode.vmSqliteObjectLeaksEnabled()) { - mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace(); - } else { - mStackTrace = null; - } - mDriver = driver; - mEditTable = editTable; - mColumnNameMap = null; - mQuery = query; + public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { + if (query == null) { + throw new IllegalArgumentException("query object cannot be null"); + } + mDriver = driver; + mEditTable = editTable; + mColumnNameMap = null; + mQuery = query; - mColumns = query.getColumnNames(); - mRowIdColumnIndex = DatabaseUtils.findRowIdColumnIndex(mColumns); - */ + mColumns = query.getColumnNames(); } /** @@ -132,141 +111,152 @@ public class SQLiteCursor /*extends AbstractWindowedCursor*/ implements Cursor { return mQuery.getDatabase(); } - // @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); - } + @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;*/ - return 1; + @Override + public int getCount() { + if (mCount == NO_COUNT) { + fillWindow(0); + } + return mCount; } - private void fillWindow(int requiredPos) { /* - clearOrCreateWindow(getDatabase().getPath()); - - try { - if (mCount == NO_COUNT) { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); - mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); - mCursorWindowCapacity = mWindow.getNumRows(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d(TAG, "received count(*) from native_fill_window: " + mCount); - } - } else { - int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, - mCursorWindowCapacity); - mQuery.fillWindow(mWindow, startPos, requiredPos, false); - } - } catch (RuntimeException ex) { - // Close the cursor window if the query failed and therefore will - // not produce any results. This helps to avoid accidentally leaking - // the cursor window if the client does not correctly handle exceptions - // and fails to close the cursor. - closeWindow(); - throw ex; - } - */ + /* + ** The AbstractWindowClass contains protected methods clearOrCreateWindow() and + ** closeWindow(), which are used by the android.database.sqlite.* version of this + ** class. But, since they are marked with "@hide", the following replacement + ** versions are required. + */ + private void awc_clearOrCreateWindow(String name){ + CursorWindow win = getWindow(); + if( win==null ){ + win = new CursorWindow(name); + setWindow(win); + }else{ + win.clear(); + } + } + private void awc_closeWindow(){ + setWindow(null); } - // @Override - public int getColumnIndex(String columnName) { /* - // Create mColumnNameMap on demand - if (mColumnNameMap == null) { - String[] columns = mColumns; - int columnCount = columns.length; - HashMap map = new HashMap(columnCount, 1); - for (int i = 0; i < columnCount; i++) { - map.put(columns[i], i); - } - mColumnNameMap = map; - } + private void fillWindow(int requiredPos) { + awc_clearOrCreateWindow(getDatabase().getPath()); - // 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; - }*/ - return -1; + try { + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); + } + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + mQuery.fillWindow(mWindow, startPos, requiredPos, false); + } + } catch (RuntimeException ex) { + // Close the cursor window if the query failed and therefore will + // not produce any results. This helps to avoid accidentally leaking + // the cursor window if the client does not correctly handle exceptions + // and fails to close the cursor. + awc_closeWindow(); + throw ex; + } } - // @Override + @Override + public int getColumnIndex(String columnName) { + // Create mColumnNameMap on demand + if (mColumnNameMap == null) { + String[] columns = mColumns; + int columnCount = columns.length; + HashMap map = new HashMap(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 + @Override public void deactivate() { - // super.deactivate(); + super.deactivate(); mDriver.cursorDeactivated(); } - // @Override - public void close() { /* - super.close(); - synchronized (this) { - mQuery.close(); - mDriver.cursorClosed(); - } - */ + @Override + public void close() { + super.close(); + synchronized (this) { + mQuery.close(); + mDriver.cursorClosed(); + } } - // @Override - public boolean requery() { /* - if (isClosed()) { - return false; - } + @Override + public boolean requery() { + if (isClosed()) { + return false; + } - synchronized (this) { - if (!mQuery.getDatabase().isOpen()) { - return false; - } + synchronized (this) { + if (!mQuery.getDatabase().isOpen()) { + return false; + } - if (mWindow != null) { - mWindow.clear(); - } - mPos = -1; - mCount = NO_COUNT; + if (mWindow != null) { + mWindow.clear(); + } + mPos = -1; + mCount = NO_COUNT; - mDriver.cursorRequeried(this); - } + mDriver.cursorRequeried(this); + } - try { - return super.requery(); - } catch (IllegalStateException e) { - // for backwards compatibility, just return false - Log.w(TAG, "requery() failed " + e.getMessage(), e); - return false; - } - */ - return false; + try { + return super.requery(); + } catch (IllegalStateException e) { + // for backwards compatibility, just return false + Log.w(TAG, "requery() failed " + e.getMessage(), e); + return false; + } } - // @Override - public void setWindow(CursorWindow window) { /* - super.setWindow(window); - mCount = NO_COUNT; - */ + @Override + public void setWindow(CursorWindow window) { + super.setWindow(window); + mCount = NO_COUNT; } /** @@ -279,50 +269,15 @@ public class SQLiteCursor /*extends AbstractWindowedCursor*/ implements Cursor { /** * 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) { - String sql = mQuery.getSql(); - int len = sql.length(); - StrictMode.onSqliteObjectLeaked( - "Finalizing a Cursor that has not been deactivated or closed. " + - "database = " + mQuery.getDatabase().getLabel() + - ", table = " + mEditTable + - ", query = " + sql.substring(0, (len > 1000) ? 1000 : len), - mStackTrace); - } - close(); - } - } finally { - super.finalize(); - } - */ - } - // --- - public boolean moveToFirst() { - return false; - } - - public boolean moveToNext() { - return false; - } - - public boolean isAfterLast() { - return true; - } - - public long getLong(int columnIndex) { - return 1; - } - - public String getString(int columnIndex) { - return "BBB"; - } - - public int getInt(int columnIndex) { - return 1; + @Override + protected void finalize() { + try { + // if the cursor hasn't been closed yet, close it first + if (mWindow != null) { + close(); + } + } finally { + super.finalize(); + } } } diff --git a/src/api-impl/android/database/sqlite/SQLiteCustomFunction.java b/src/api-impl/android/database/sqlite/SQLiteCustomFunction.java new file mode 100644 index 00000000..6ecb097f --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteCustomFunction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * Describes a custom SQL function. + * + * @hide + */ +public final class SQLiteCustomFunction { + public final String name; + public final int numArgs; + public final SQLiteDatabase.CustomFunction callback; + + /** + * Create custom function. + * + * @param name The name of the sqlite3 function. + * @param numArgs The number of arguments for the function, or -1 to + * support any number of arguments. + * @param callback The callback to invoke when the function is executed. + */ + public SQLiteCustomFunction(String name, int numArgs, + SQLiteDatabase.CustomFunction callback) { + if (name == null) { + throw new IllegalArgumentException("name must not be null."); + } + + this.name = name; + this.numArgs = numArgs; + this.callback = callback; + } + + // Called from native. + @SuppressWarnings("unused") + private void dispatchCallback(String[] args) { + callback.callback(args); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDatabase.java b/src/api-impl/android/database/sqlite/SQLiteDatabase.java index 3e75ec2e..05ea78ca 100644 --- a/src/api-impl/android/database/sqlite/SQLiteDatabase.java +++ b/src/api-impl/android/database/sqlite/SQLiteDatabase.java @@ -1,11 +1,135 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + package android.database.sqlite; +import dalvik.system.CloseGuard; + import android.content.ContentValues; import android.database.Cursor; +import android.database.DatabaseErrorHandler; +import android.database.DatabaseUtils; +import android.database.DefaultDatabaseErrorHandler; +import android.database.SQLException; +import android.database.sqlite.SQLiteDebug.DbStats; import android.os.CancellationSignal; +import android.os.Looper; +import android.os.OperationCanceledException; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; +import android.util.Printer; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Exposes methods to manage a SQLite database. + * + *

+ * SQLiteDatabase has methods to create, delete, execute SQL commands, and + * perform other common database management tasks. + *

+ * See the Notepad sample application in the SDK for an example of creating + * and managing a database. + *

+ * Database names must be unique within an application, not across all applications. + *

+ * + *

Localized Collation - ORDER BY

+ *

+ * In addition to SQLite's default BINARY collator, Android supplies + * two more, LOCALIZED, which changes with the system's current locale, + * and UNICODE, which is the Unicode Collation Algorithm and not tailored + * to the current locale. + *

+ */ +public final class SQLiteDatabase extends SQLiteClosable { + private static final String TAG = "SQLiteDatabase"; + + private static final int EVENT_DB_CORRUPT = 75004; + + // Stores reference to all databases opened in the current process. + // (The referent Object is not used at this time.) + // INVARIANT: Guarded by sActiveDatabases. + private static WeakHashMap sActiveDatabases = + new WeakHashMap(); + + // Thread-local for database sessions that belong to this database. + // Each thread has its own database session. + // INVARIANT: Immutable. + private final ThreadLocal mThreadSession = new ThreadLocal() { + @Override + protected SQLiteSession initialValue() { + return createSession(); + } + }; + + // The optional factory to use when creating new Cursors. May be null. + // INVARIANT: Immutable. + private final CursorFactory mCursorFactory; + + // Error handler to be used when SQLite returns corruption errors. + // INVARIANT: Immutable. + private final DatabaseErrorHandler mErrorHandler; + + // Shared database state lock. + // This lock guards all of the shared state of the database, such as its + // configuration, whether it is open or closed, and so on. This lock should + // be held for as little time as possible. + // + // The lock MUST NOT be held while attempting to acquire database connections or + // while executing SQL statements on behalf of the client as it can lead to deadlock. + // + // It is ok to hold the lock while reconfiguring the connection pool or dumping + // statistics because those operations are non-reentrant and do not try to acquire + // connections that might be held by other threads. + // + // Basic rule: grab the lock, access or modify global state, release the lock, then + // do the required SQL work. + private final Object mLock = new Object(); + + // Warns if the database is finalized without being closed properly. + // INVARIANT: Guarded by mLock. + private final CloseGuard mCloseGuardLocked = CloseGuard.get(); + + // The database configuration. + // INVARIANT: Guarded by mLock. + private final SQLiteDatabaseConfiguration mConfigurationLocked; + + // The connection pool for the database, null when closed. + // The pool itself is thread-safe, but the reference to it can only be acquired + // when the lock is held. + // INVARIANT: Guarded by mLock. + private SQLiteConnectionPool mConnectionPoolLocked; + + // True if the database has attached databases. + // INVARIANT: Guarded by mLock. + private boolean mHasAttachedDbsLocked; -public final class SQLiteDatabase /*extends SQLiteClosable*/ { - // --- constants from android source /** * When a constraint violation occurs, an immediate ROLLBACK occurs, * thus ending the current transaction, and the command aborts with a @@ -59,7 +183,8 @@ public final class SQLiteDatabase /*extends SQLiteClosable*/ { */ public static final int CONFLICT_NONE = 0; - private static final String[] CONFLICT_VALUES = new String[] {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; + private static final String[] CONFLICT_VALUES = new String[] + {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; /** * Maximum Length Of A LIKE Or GLOB Pattern @@ -83,15 +208,15 @@ public final class SQLiteDatabase /*extends SQLiteClosable*/ { * * {@more} Note that the value of this flag is 0, so it is the default. */ - public static final int OPEN_READWRITE = 0x00000000; // update native code if changing + public static final int OPEN_READWRITE = 0x00000000; // update native code if changing /** * Open flag: Flag for {@link #openDatabase} to open the database for reading only. * This is the only reliable way to open a database if the disk may be full. */ - public static final int OPEN_READONLY = 0x00000001; // update native code if changing + public static final int OPEN_READONLY = 0x00000001; // update native code if changing - private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing + private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing /** * Open flag: Flag for {@link #openDatabase} to open the database without support for @@ -101,13 +226,13 @@ public final class SQLiteDatabase /*extends SQLiteClosable*/ { * You must be consistent when using this flag to use the setting the database was * created with. If this is set, {@link #setLocale} will do nothing. */ - public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing + public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing /** * Open flag: Flag for {@link #openDatabase} to create the database file if it does not * already exist. */ - public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing + public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing /** * Open flag: Flag for {@link #openDatabase} to open the database file with @@ -128,160 +253,1970 @@ public final class SQLiteDatabase /*extends SQLiteClosable*/ { * SQL statement & schema. A large SQL cache may use a significant amount of memory. */ public static final int MAX_SQL_CACHE_SIZE = 100; - // --- - private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, DatabaseErrorHandler errorHandler) { - /* mCursorFactory = cursorFactory; - mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); - mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags);*/ + private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, + DatabaseErrorHandler errorHandler) { + mCursorFactory = cursorFactory; + mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); + mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); } - public static SQLiteDatabase create(CursorFactory factory) { - // This is a magic string with special meaning for SQLite. - return openDatabase(":memory:", factory, CREATE_IF_NECESSARY); - } - - private void open() { /* - try { - try { - openInner(); - } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); - openInner(); - } - } catch (SQLiteException ex) { - Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); - close(); - throw ex; - } - */ - } - - public long insert(String table, String nullColumnHack, ContentValues values) { - /* try { - return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); - } catch (SQLException e) { - Log.e(TAG, "Error inserting " + values, e); - return -1; - }*/ - return -1; - } - - // --- - public Cursor query(boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit) { - return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, - groupBy, having, orderBy, limit, null); - } - - public Cursor query(boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit, CancellationSignal cancellationSignal) { - return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, - groupBy, having, orderBy, limit, cancellationSignal); - } - - public Cursor query(String table, String[] columns, String selection, - String[] selectionArgs, String groupBy, String having, - String orderBy) { - - return query(false, table, columns, selection, selectionArgs, groupBy, - having, orderBy, null /* limit */); - } - - public Cursor query(String table, String[] columns, String selection, - String[] selectionArgs, String groupBy, String having, - String orderBy, String limit) { - - return query(false, table, columns, selection, selectionArgs, groupBy, - having, orderBy, limit); - } - // --- - public Cursor queryWithFactory(CursorFactory cursorFactory, - boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit) { - return queryWithFactory(cursorFactory, distinct, table, columns, selection, - selectionArgs, groupBy, having, orderBy, limit, null); - } - - public Cursor queryWithFactory(CursorFactory cursorFactory, - boolean distinct, String table, String[] columns, - String selection, String[] selectionArgs, String groupBy, - String having, String orderBy, String limit, CancellationSignal cancellationSignal) { /* - acquireReference(); - try { - String sql = SQLiteQueryBuilder.buildQueryString( - distinct, table, columns, selection, groupBy, having, orderBy, limit); - - return rawQueryWithFactory(cursorFactory, sql, selectionArgs, - findEditTable(table), cancellationSignal); - } finally { - releaseReference(); - }*/ - return rawQueryWithFactory(cursorFactory, "XXX", selectionArgs, table, cancellationSignal); - } - // --- - public Cursor rawQueryWithFactory( - CursorFactory cursorFactory, String sql, String[] selectionArgs, - String editTable) { - return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null); - } - - public Cursor rawQueryWithFactory( - CursorFactory cursorFactory, String sql, String[] selectionArgs, - String editTable, CancellationSignal cancellationSignal) { - // acquireReference(); + @Override + protected void finalize() throws Throwable { try { - SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable, - cancellationSignal); - return driver.query(cursorFactory /* != null ? cursorFactory : mCursorFactory*/, - selectionArgs); + dispose(true); } finally { - // releaseReference(); + super.finalize(); } } - // --- + + @Override + protected void onAllReferencesReleased() { + dispose(false); + } + + private void dispose(boolean finalized) { + final SQLiteConnectionPool pool; + synchronized (mLock) { + if (mCloseGuardLocked != null) { + if (finalized) { + mCloseGuardLocked.warnIfOpen(); + } + mCloseGuardLocked.close(); + } + + pool = mConnectionPoolLocked; + mConnectionPoolLocked = null; + } + + if (!finalized) { + synchronized (sActiveDatabases) { + sActiveDatabases.remove(this); + } + + if (pool != null) { + pool.close(); + } + } + } + + /** + * Attempts to release memory that SQLite holds but does not require to + * operate properly. Typically this memory will come from the page cache. + * + * @return the number of bytes actually released + */ + public static int releaseMemory() { + return SQLiteGlobal.releaseMemory(); + } + + /** + * Control whether or not the SQLiteDatabase is made thread-safe by using locks + * around critical sections. This is pretty expensive, so if you know that your + * DB will only be used by a single thread then you should set this to false. + * The default is true. + * @param lockingEnabled set to true to enable locks, false otherwise + * + * @deprecated This method now does nothing. Do not use. + */ + @Deprecated + public void setLockingEnabled(boolean lockingEnabled) { + } + + /** + * Gets a label to use when describing the database in log messages. + * @return The label. + */ + String getLabel() { + synchronized (mLock) { + return mConfigurationLocked.label; + } + } + + /** + * Sends a corruption message to the database error handler. + */ + void onCorruption() { + // EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel()); + mErrorHandler.onCorruption(this); + } + + /** + * Gets the {@link SQLiteSession} that belongs to this thread for this database. + * Once a thread has obtained a session, it will continue to obtain the same + * session even after the database has been closed (although the session will not + * be usable). However, a thread that does not already have a session cannot + * obtain one after the database has been closed. + * + * The idea is that threads that have active connections to the database may still + * have work to complete even after the call to {@link #close}. Active database + * connections are not actually disposed until they are released by the threads + * that own them. + * + * @return The session, never null. + * + * @throws IllegalStateException if the thread does not yet have a session and + * the database is not open. + */ + SQLiteSession getThreadSession() { + return mThreadSession.get(); // initialValue() throws if database closed + } + + SQLiteSession createSession() { + final SQLiteConnectionPool pool; + synchronized (mLock) { + throwIfNotOpenLocked(); + pool = mConnectionPoolLocked; + } + return new SQLiteSession(pool); + } + + /** + * Gets default connection flags that are appropriate for this thread, taking into + * account whether the thread is acting on behalf of the UI. + * + * @param readOnly True if the connection should be read-only. + * @return The connection flags. + */ + int getThreadDefaultConnectionFlags(boolean readOnly) { + int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY : + SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY; + if (isMainThread()) { + flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE; + } + return flags; + } + + private static boolean isMainThread() { + // FIXME: There should be a better way to do this. + // Would also be nice to have something that would work across Binder calls. + Looper looper = Looper.myLooper(); + return looper != null && looper == Looper.getMainLooper(); + } + + /** + * Begins a transaction in EXCLUSIVE mode. + *

+ * Transactions can be nested. + * When the outer transaction is ended all of + * the work done in that transaction and all of the nested transactions will be committed or + * rolled back. The changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. + *

+ *

Here is the standard idiom for transactions: + * + *

+	 *   db.beginTransaction();
+	 *   try {
+	 *     ...
+	 *     db.setTransactionSuccessful();
+	 *   } finally {
+	 *     db.endTransaction();
+	 *   }
+	 * 
+ */ + public void beginTransaction() { + beginTransaction(null /* transactionStatusCallback */, true); + } + + /** + * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When + * the outer transaction is ended all of the work done in that transaction + * and all of the nested transactions will be committed or rolled back. The + * changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they + * will be committed. + *

+ * Here is the standard idiom for transactions: + * + *

+	 *   db.beginTransactionNonExclusive();
+	 *   try {
+	 *     ...
+	 *     db.setTransactionSuccessful();
+	 *   } finally {
+	 *     db.endTransaction();
+	 *   }
+	 * 
+ */ + public void beginTransactionNonExclusive() { + beginTransaction(null /* transactionStatusCallback */, false); + } + + /** + * Begins a transaction in EXCLUSIVE mode. + *

+ * Transactions can be nested. + * When the outer transaction is ended all of + * the work done in that transaction and all of the nested transactions will be committed or + * rolled back. The changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they will be committed. + *

+ *

Here is the standard idiom for transactions: + * + *

+	 *   db.beginTransactionWithListener(listener);
+	 *   try {
+	 *     ...
+	 *     db.setTransactionSuccessful();
+	 *   } finally {
+	 *     db.endTransaction();
+	 *   }
+	 * 
+ * + * @param transactionListener listener that should be notified when the transaction begins, + * commits, or is rolled back, either explicitly or by a call to + * {@link #yieldIfContendedSafely}. + */ + public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { + beginTransaction(transactionListener, true); + } + + /** + * Begins a transaction in IMMEDIATE mode. Transactions can be nested. When + * the outer transaction is ended all of the work done in that transaction + * and all of the nested transactions will be committed or rolled back. The + * changes will be rolled back if any transaction is ended without being + * marked as clean (by calling setTransactionSuccessful). Otherwise they + * will be committed. + *

+ * Here is the standard idiom for transactions: + * + *

+	 *   db.beginTransactionWithListenerNonExclusive(listener);
+	 *   try {
+	 *     ...
+	 *     db.setTransactionSuccessful();
+	 *   } finally {
+	 *     db.endTransaction();
+	 *   }
+	 * 
+ * + * @param transactionListener listener that should be notified when the + * transaction begins, commits, or is rolled back, either + * explicitly or by a call to {@link #yieldIfContendedSafely}. + */ + public void beginTransactionWithListenerNonExclusive( + SQLiteTransactionListener transactionListener) { + beginTransaction(transactionListener, false); + } + + private void beginTransaction(SQLiteTransactionListener transactionListener, + boolean exclusive) { + acquireReference(); + try { + getThreadSession().beginTransaction( + exclusive ? SQLiteSession.TRANSACTION_MODE_EXCLUSIVE : + SQLiteSession.TRANSACTION_MODE_IMMEDIATE, + transactionListener, + getThreadDefaultConnectionFlags(false /*readOnly*/), null); + } finally { + releaseReference(); + } + } + + /** + * End a transaction. See beginTransaction for notes about how to use this and when transactions + * are committed and rolled back. + */ + public void endTransaction() { + acquireReference(); + try { + getThreadSession().endTransaction(null); + } finally { + releaseReference(); + } + } + + /** + * Marks the current transaction as successful. Do not do any more database work between + * calling this and calling endTransaction. Do as little non-database work as possible in that + * situation too. If any errors are encountered between this and endTransaction the transaction + * will still be committed. + * + * @throws IllegalStateException if the current thread is not in a transaction or the + * transaction is already marked as successful. + */ + public void setTransactionSuccessful() { + acquireReference(); + try { + getThreadSession().setTransactionSuccessful(); + } finally { + releaseReference(); + } + } + + /** + * Returns true if the current thread has a transaction pending. + * + * @return True if the current thread is in a transaction. + */ + public boolean inTransaction() { + acquireReference(); + try { + return getThreadSession().hasTransaction(); + } finally { + releaseReference(); + } + } + + /** + * Returns true if the current thread is holding an active connection to the database. + *

+ * The name of this method comes from a time when having an active connection + * to the database meant that the thread was holding an actual lock on the + * database. Nowadays, there is no longer a true "database lock" although threads + * may block if they cannot acquire a database connection to perform a + * particular operation. + *

+ * + * @return True if the current thread is holding an active connection to the database. + */ + public boolean isDbLockedByCurrentThread() { + acquireReference(); + try { + return getThreadSession().hasConnection(); + } finally { + releaseReference(); + } + } + + /** + * Always returns false. + *

+ * There is no longer the concept of a database lock, so this method always returns false. + *

+ * + * @return False. + * @deprecated Always returns false. Do not use this method. + */ + @Deprecated + public boolean isDbLockedByOtherThreads() { + return false; + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. + * @return true if the transaction was yielded + * @deprecated if the db is locked more than once (becuase of nested transactions) then the lock + * will not be yielded. Use yieldIfContendedSafely instead. + */ + @Deprecated + public boolean yieldIfContended() { + return yieldIfContendedHelper(false /* do not check yielding */, + -1 /* sleepAfterYieldDelay */); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * throw an exception if that is not the case. + * @return true if the transaction was yielded + */ + public boolean yieldIfContendedSafely() { + return yieldIfContendedHelper(true /* check yielding */, -1 /* sleepAfterYieldDelay*/); + } + + /** + * Temporarily end the transaction to let other threads run. The transaction is assumed to be + * successful so far. Do not call setTransactionSuccessful before calling this. When this + * returns a new transaction will have been created but not marked as successful. This assumes + * that there are no nested transactions (beginTransaction has only been called once) and will + * throw an exception if that is not the case. + * @param sleepAfterYieldDelay if > 0, sleep this long before starting a new transaction if + * the lock was actually yielded. This will allow other background threads to make some + * more progress than they would if we started the transaction immediately. + * @return true if the transaction was yielded + */ + public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { + return yieldIfContendedHelper(true /* check yielding */, sleepAfterYieldDelay); + } + + private boolean yieldIfContendedHelper(boolean throwIfUnsafe, long sleepAfterYieldDelay) { + acquireReference(); + try { + return getThreadSession().yieldTransaction(sleepAfterYieldDelay, throwIfUnsafe, null); + } finally { + releaseReference(); + } + } + + /** + * Deprecated. + * @deprecated This method no longer serves any useful purpose and has been deprecated. + */ + @Deprecated + public Map getSyncedTables() { + return new HashMap(0); + } + + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param path to database file to open and/or create + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode + * @return the newly opened database + * @throws SQLiteException if the database cannot be opened + */ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) { return openDatabase(path, factory, flags, null); } - public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, DatabaseErrorHandler errorHandler) { + /** + * Open the database according to the flags {@link #OPEN_READWRITE} + * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + *

Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.

+ * + * @param path to database file to open and/or create + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called, or null for default + * @param flags to control database access mode + * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption + * when sqlite reports database corruption + * @return the newly opened database + * @throws SQLiteException if the database cannot be opened + */ + public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags, + DatabaseErrorHandler errorHandler) { SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler); db.open(); return db; } - // --- + /** + * Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) { + return openOrCreateDatabase(file.getPath(), factory); + } + + /** + * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory) { + return openDatabase(path, factory, CREATE_IF_NECESSARY, null); + } + + /** + * Equivalent to openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler). + */ + public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory, + DatabaseErrorHandler errorHandler) { + return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler); + } + + /** + * Deletes a database including its journal file and other auxiliary files + * that may have been created by the database engine. + * + * @param file The database file path. + * @return True if the database was successfully deleted. + */ + public static boolean deleteDatabase(File file) { + if (file == null) { + throw new IllegalArgumentException("file must not be null"); + } + + boolean deleted = false; + deleted |= file.delete(); + deleted |= new File(file.getPath() + "-journal").delete(); + deleted |= new File(file.getPath() + "-shm").delete(); + deleted |= new File(file.getPath() + "-wal").delete(); + + File dir = file.getParentFile(); + if (dir != null) { + final String prefix = file.getName() + "-mj"; + File[] files = dir.listFiles(new FileFilter() { + @Override + public boolean accept(File candidate) { + return candidate.getName().startsWith(prefix); + } + }); + if (files != null) { + for (File masterJournal : files) { + deleted |= masterJournal.delete(); + } + } + } + return deleted; + } + + /** + * Reopens the database in read-write mode. + * If the database is already read-write, does nothing. + * + * @throws SQLiteException if the database could not be reopened as requested, in which + * case it remains open in read only mode. + * @throws IllegalStateException if the database is not open. + * + * @see #isReadOnly() + * @hide + */ + public void reopenReadWrite() { + synchronized (mLock) { + throwIfNotOpenLocked(); + + if (!isReadOnlyLocked()) { + return; // nothing to do + } + + // Reopen the database in read-write mode. + final int oldOpenFlags = mConfigurationLocked.openFlags; + mConfigurationLocked.openFlags = (mConfigurationLocked.openFlags & ~OPEN_READ_MASK) + | OPEN_READWRITE; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.openFlags = oldOpenFlags; + throw ex; + } + } + } + + private void open() { + try { + try { + openInner(); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + openInner(); + } + } catch (SQLiteException ex) { + Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex); + close(); + throw ex; + } + } + + private void openInner() { + synchronized (mLock) { + assert mConnectionPoolLocked == null; + mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked); + mCloseGuardLocked.open("close"); + } + + synchronized (sActiveDatabases) { + sActiveDatabases.put(this, null); + } + } + + /** + * Create a memory backed SQLite database. Its contents will be destroyed + * when the database is closed. + * + *

Sets the locale of the database to the the system's current locale. + * Call {@link #setLocale} if you would like something else.

+ * + * @param factory an optional factory class that is called to instantiate a + * cursor when query is called + * @return a SQLiteDatabase object, or null if the database can't be created + */ + public static SQLiteDatabase create(CursorFactory factory) { + // This is a magic string with special meaning for SQLite. + return openDatabase(SQLiteDatabaseConfiguration.MEMORY_DB_PATH, + factory, CREATE_IF_NECESSARY); + } + + /** + * Registers a CustomFunction callback as a function that can be called from + * SQLite database triggers. + * + * @param name the name of the sqlite3 function + * @param numArgs the number of arguments for the function + * @param function callback to call when the function is executed + * @hide + */ + public void addCustomFunction(String name, int numArgs, CustomFunction function) { + // Create wrapper (also validates arguments). + SQLiteCustomFunction wrapper = new SQLiteCustomFunction(name, numArgs, function); + + synchronized (mLock) { + throwIfNotOpenLocked(); + + mConfigurationLocked.customFunctions.add(wrapper); + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.customFunctions.remove(wrapper); + throw ex; + } + } + } + + /** + * Gets the database version. + * + * @return the database version + */ + public int getVersion() { + return ((Long) DatabaseUtils.longForQuery(this, "PRAGMA user_version;", null)).intValue(); + } + + /** + * Sets the database version. + * + * @param version the new database version + */ + public void setVersion(int version) { + execSQL("PRAGMA user_version = " + version); + } + + /** + * Returns the maximum size the database may grow to. + * + * @return the new maximum database size + */ + public long getMaximumSize() { + long pageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count;", null); + return pageCount * getPageSize(); + } + + /** + * Sets the maximum size the database will grow to. The maximum size cannot + * be set below the current size. + * + * @param numBytes the maximum database size, in bytes + * @return the new maximum database size + */ + public long setMaximumSize(long numBytes) { + long pageSize = getPageSize(); + long numPages = numBytes / pageSize; + // If numBytes isn't a multiple of pageSize, bump up a page + if ((numBytes % pageSize) != 0) { + numPages++; + } + long newPageCount = DatabaseUtils.longForQuery(this, "PRAGMA max_page_count = " + numPages, + null); + return newPageCount * pageSize; + } + + /** + * Returns the current database page size, in bytes. + * + * @return the database page size, in bytes + */ + public long getPageSize() { + return DatabaseUtils.longForQuery(this, "PRAGMA page_size;", null); + } + + /** + * Sets the database page size. The page size must be a power of two. This + * method does not work if any data has been written to the database file, + * and must be called right after the database has been created. + * + * @param numBytes the database page size, in bytes + */ + public void setPageSize(long numBytes) { + execSQL("PRAGMA page_size = " + numBytes); + } + + /** + * Mark this table as syncable. When an update occurs in this table the + * _sync_dirty field will be set to ensure proper syncing operation. + * + * @param table the table to mark as syncable + * @param deletedTable The deleted table that corresponds to the + * syncable table + * @deprecated This method no longer serves any useful purpose and has been deprecated. + */ + @Deprecated + public void markTableSyncable(String table, String deletedTable) { + } + + /** + * Mark this table as syncable, with the _sync_dirty residing in another + * table. When an update occurs in this table the _sync_dirty field of the + * row in updateTable with the _id in foreignKey will be set to + * ensure proper syncing operation. + * + * @param table an update on this table will trigger a sync time removal + * @param foreignKey this is the column in table whose value is an _id in + * updateTable + * @param updateTable this is the table that will have its _sync_dirty + * @deprecated This method no longer serves any useful purpose and has been deprecated. + */ + @Deprecated + public void markTableSyncable(String table, String foreignKey, String updateTable) { + } + + /** + * Finds the name of the first table, which is editable. + * + * @param tables a list of tables + * @return the first table listed + */ + public static String findEditTable(String tables) { + if (!TextUtils.isEmpty(tables)) { + // find the first word terminated by either a space or a comma + int spacepos = tables.indexOf(' '); + int commapos = tables.indexOf(','); + + if (spacepos > 0 && (spacepos < commapos || commapos < 0)) { + return tables.substring(0, spacepos); + } else if (commapos > 0 && (commapos < spacepos || spacepos < 0) ) { + return tables.substring(0, commapos); + } + return tables; + } else { + throw new IllegalStateException("Invalid tables"); + } + } + + /** + * Compiles an SQL statement into a reusable pre-compiled statement object. + * The parameters are identical to {@link #execSQL(String)}. You may put ?s in the + * statement and fill in those values with {@link SQLiteProgram#bindString} + * and {@link SQLiteProgram#bindLong} each time you want to run the + * statement. Statements may not return result sets larger than 1x1. + *

+ * No two threads should be using the same {@link SQLiteStatement} at the same time. + * + * @param sql The raw SQL statement, may contain ? for unknown values to be + * bound later. + * @return A pre-compiled {@link SQLiteStatement} object. Note that + * {@link SQLiteStatement}s are not synchronized, see the documentation for more details. + */ + public SQLiteStatement compileStatement(String sql) throws SQLException { + acquireReference(); + try { + return new SQLiteStatement(this, sql, null); + } finally { + releaseReference(); + } + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @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. + * @see Cursor + */ + public Cursor query(boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit) { + return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, + groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @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. + * @see Cursor + */ + public Cursor query(boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancellationSignal cancellationSignal) { + return queryWithFactory(null, distinct, table, columns, selection, selectionArgs, + groupBy, having, orderBy, limit, cancellationSignal); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @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. + * @see Cursor + */ + public Cursor queryWithFactory(CursorFactory cursorFactory, + boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit) { + return queryWithFactory(cursorFactory, distinct, table, columns, selection, + selectionArgs, groupBy, having, orderBy, limit, null); + } + + /** + * Query the given URL, returning a {@link Cursor} over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @param distinct true if you want each row to be unique, false otherwise. + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @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. + * @see Cursor + */ + public Cursor queryWithFactory(CursorFactory cursorFactory, + boolean distinct, String table, String[] columns, + String selection, String[] selectionArgs, String groupBy, + String having, String orderBy, String limit, CancellationSignal cancellationSignal) { + acquireReference(); + try { + String sql = SQLiteQueryBuilder.buildQueryString( + distinct, table, columns, selection, groupBy, having, orderBy, limit); + + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, + findEditTable(table), cancellationSignal); + } finally { + releaseReference(); + } + } + + /** + * Query the given table, returning a {@link Cursor} over the result set. + * + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @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. + * @see Cursor + */ + public Cursor query(String table, String[] columns, String selection, + String[] selectionArgs, String groupBy, String having, + String orderBy) { + + return query(false, table, columns, selection, selectionArgs, groupBy, + having, orderBy, null /* limit */); + } + + /** + * Query the given table, returning a {@link Cursor} over the result set. + * + * @param table The table name to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, formatted as an + * SQL WHERE clause (excluding the WHERE itself). Passing null + * will return all rows for the given table. + * @param selectionArgs You may include ?s in selection, which will be + * replaced by the values from selectionArgs, in order that they + * appear in the selection. The values will be bound as Strings. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @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. + * @see Cursor + */ + public Cursor query(String table, String[] columns, String selection, + String[] selectionArgs, String groupBy, String having, + String orderBy, String limit) { + + return query(false, table, columns, selection, selectionArgs, groupBy, + having, orderBy, limit); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @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. + * @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. + */ + public Cursor rawQuery(String sql, String[] selectionArgs) { + return rawQueryWithFactory(null, sql, selectionArgs, null, null); + } + + /** + * Runs the provided SQL and returns a {@link Cursor} over the result set. + * + * @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 cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @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. + */ + public Cursor rawQuery(String sql, String[] selectionArgs, + CancellationSignal cancellationSignal) { + return rawQueryWithFactory(null, sql, selectionArgs, null, cancellationSignal); + } + + /** + * Runs the provided SQL and returns a cursor over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @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 editTable the name of the first table, which is editable + * @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. + */ + public Cursor rawQueryWithFactory( + CursorFactory cursorFactory, String sql, String[] selectionArgs, + String editTable) { + return rawQueryWithFactory(cursorFactory, sql, selectionArgs, editTable, null); + } + + /** + * Runs the provided SQL and returns a cursor over the result set. + * + * @param cursorFactory the cursor factory to use, or null for the default factory + * @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 editTable the name of the first table, which is editable + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @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. + */ + public Cursor rawQueryWithFactory( + CursorFactory cursorFactory, String sql, String[] selectionArgs, + String editTable, CancellationSignal cancellationSignal) { + acquireReference(); + try { + SQLiteCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable, + cancellationSignal); + return driver.query(cursorFactory != null ? cursorFactory : mCursorFactory, + selectionArgs); + } finally { + releaseReference(); + } + } + + /** + * Convenience method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack optional; may be null. + * SQL doesn't allow inserting a completely empty row without + * naming at least one column name. If your provided values is + * empty, no column names are known and an empty row can't be inserted. + * If not set to null, the nullColumnHack parameter + * provides the name of nullable column name to explicitly insert a NULL into + * in the case where your values is empty. + * @param values this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long insert(String table, String nullColumnHack, ContentValues values) { + try { + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); + } catch (SQLException e) { + Log.e(TAG, "Error inserting " + values, e); + return -1; + } + } + + /** + * Convenience method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack optional; may be null. + * SQL doesn't allow inserting a completely empty row without + * naming at least one column name. If your provided values is + * empty, no column names are known and an empty row can't be inserted. + * If not set to null, the nullColumnHack parameter + * provides the name of nullable column name to explicitly insert a NULL into + * in the case where your values is empty. + * @param values this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @throws SQLException + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long insertOrThrow(String table, String nullColumnHack, ContentValues values) + throws SQLException { + return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE); + } + + /** + * Convenience method for replacing a row in the database. + * Inserts a new row if a row does not already exist. + * + * @param table the table in which to replace the row + * @param nullColumnHack optional; may be null. + * SQL doesn't allow inserting a completely empty row without + * naming at least one column name. If your provided initialValues is + * empty, no column names are known and an empty row can't be inserted. + * If not set to null, the nullColumnHack parameter + * provides the name of nullable column name to explicitly insert a NULL into + * in the case where your initialValues is empty. + * @param initialValues this map contains the initial column values for + * the row. The keys should be the column names and the values the column values. + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long replace(String table, String nullColumnHack, ContentValues initialValues) { + try { + return insertWithOnConflict(table, nullColumnHack, initialValues, + CONFLICT_REPLACE); + } catch (SQLException e) { + Log.e(TAG, "Error inserting " + initialValues, e); + return -1; + } + } + + /** + * Convenience method for replacing a row in the database. + * Inserts a new row if a row does not already exist. + * + * @param table the table in which to replace the row + * @param nullColumnHack optional; may be null. + * SQL doesn't allow inserting a completely empty row without + * naming at least one column name. If your provided initialValues is + * empty, no column names are known and an empty row can't be inserted. + * If not set to null, the nullColumnHack parameter + * provides the name of nullable column name to explicitly insert a NULL into + * in the case where your initialValues is empty. + * @param initialValues this map contains the initial column values for + * the row. The keys should be the column names and the values the column values. + * @throws SQLException + * @return the row ID of the newly inserted row, or -1 if an error occurred + */ + public long replaceOrThrow(String table, String nullColumnHack, + ContentValues initialValues) throws SQLException { + return insertWithOnConflict(table, nullColumnHack, initialValues, + CONFLICT_REPLACE); + } + + /** + * General method for inserting a row into the database. + * + * @param table the table to insert the row into + * @param nullColumnHack optional; may be null. + * SQL doesn't allow inserting a completely empty row without + * naming at least one column name. If your provided initialValues is + * empty, no column names are known and an empty row can't be inserted. + * If not set to null, the nullColumnHack parameter + * provides the name of nullable column name to explicitly insert a NULL into + * in the case where your initialValues is empty. + * @param initialValues this map contains the initial column values for the + * row. The keys should be the column names and the values the + * column values + * @param conflictAlgorithm for insert conflict resolver + * @return the row ID of the newly inserted row OR -1 if either the + * input parameter conflictAlgorithm = {@link #CONFLICT_IGNORE} + * or an error occurred. + */ + public long insertWithOnConflict(String table, String nullColumnHack, + ContentValues initialValues, int conflictAlgorithm) { + acquireReference(); + try { + StringBuilder sql = new StringBuilder(); + sql.append("INSERT"); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); + sql.append(" INTO "); + sql.append(table); + sql.append('('); + + Object[] bindArgs = null; + int size = (initialValues != null && initialValues.size() > 0) + ? initialValues.size() : 0; + if (size > 0) { + bindArgs = new Object[size]; + int i = 0; + for (String colName : initialValues.keySet()) { + sql.append((i > 0) ? "," : ""); + sql.append(colName); + bindArgs[i++] = initialValues.get(colName); + } + sql.append(')'); + sql.append(" VALUES ("); + for (i = 0; i < size; i++) { + sql.append((i > 0) ? ",?" : "?"); + } + } else { + sql.append(nullColumnHack + ") VALUES (NULL"); + } + sql.append(')'); + + SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); + try { + return statement.executeInsert(); + } finally { + statement.close(); + } + } finally { + releaseReference(); + } + } + + /** + * Convenience method for deleting rows in the database. + * + * @param table the table to delete from + * @param whereClause the optional WHERE clause to apply when deleting. + * Passing null will delete all rows. + * @param whereArgs You may include ?s in the where clause, which + * will be replaced by the values from whereArgs. The values + * will be bound as Strings. + * @return the number of rows affected if a whereClause is passed in, 0 + * otherwise. To remove all rows and get a count pass "1" as the + * whereClause. + */ + public int delete(String table, String whereClause, String[] whereArgs) { + acquireReference(); + try { + SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table + + (!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs); + try { + return statement.executeUpdateDelete(); + } finally { + statement.close(); + } + } finally { + releaseReference(); + } + } + + /** + * Convenience method for updating rows in the database. + * + * @param table the table to update in + * @param values a map from column names to new column values. null is a + * valid value that will be translated to NULL. + * @param whereClause the optional WHERE clause to apply when updating. + * Passing null will update all rows. + * @param whereArgs You may include ?s in the where clause, which + * will be replaced by the values from whereArgs. The values + * will be bound as Strings. + * @return the number of rows affected + */ + public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { + return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE); + } + + /** + * Convenience method for updating rows in the database. + * + * @param table the table to update in + * @param values a map from column names to new column values. null is a + * valid value that will be translated to NULL. + * @param whereClause the optional WHERE clause to apply when updating. + * Passing null will update all rows. + * @param whereArgs You may include ?s in the where clause, which + * will be replaced by the values from whereArgs. The values + * will be bound as Strings. + * @param conflictAlgorithm for update conflict resolver + * @return the number of rows affected + */ + public int updateWithOnConflict(String table, ContentValues values, + String whereClause, String[] whereArgs, int conflictAlgorithm) { + if (values == null || values.size() == 0) { + throw new IllegalArgumentException("Empty values"); + } + + acquireReference(); + try { + StringBuilder sql = new StringBuilder(120); + sql.append("UPDATE "); + sql.append(CONFLICT_VALUES[conflictAlgorithm]); + sql.append(table); + sql.append(" SET "); + + // move all bind args to one array + int setValuesSize = values.size(); + int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); + Object[] bindArgs = new Object[bindArgsSize]; + int i = 0; + for (String colName : values.keySet()) { + sql.append((i > 0) ? "," : ""); + sql.append(colName); + bindArgs[i++] = values.get(colName); + sql.append("=?"); + } + if (whereArgs != null) { + for (i = setValuesSize; i < bindArgsSize; i++) { + bindArgs[i] = whereArgs[i - setValuesSize]; + } + } + if (!TextUtils.isEmpty(whereClause)) { + sql.append(" WHERE "); + sql.append(whereClause); + } + + SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs); + try { + return statement.executeUpdateDelete(); + } finally { + statement.close(); + } + } finally { + releaseReference(); + } + } + + /** + * Execute a single SQL statement that is NOT a SELECT + * or any other SQL statement that returns data. + *

+ * It has no means to return any data (such as the number of affected rows). + * Instead, you're encouraged to use {@link #insert(String, String, ContentValues)}, + * {@link #update(String, ContentValues, String, String[])}, et al, when possible. + *

+ *

+ * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'" statement if your app is using + * {@link #enableWriteAheadLogging()} + *

+ * + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. + * @throws SQLException if the SQL string is invalid + */ + public void execSQL(String sql) throws SQLException { + executeSql(sql, null); + } + + /** + * Execute a single SQL statement that is NOT a SELECT/INSERT/UPDATE/DELETE. + *

+ * For INSERT statements, use any of the following instead. + *

    + *
  • {@link #insert(String, String, ContentValues)}
  • + *
  • {@link #insertOrThrow(String, String, ContentValues)}
  • + *
  • {@link #insertWithOnConflict(String, String, ContentValues, int)}
  • + *
+ *

+ * For UPDATE statements, use any of the following instead. + *

    + *
  • {@link #update(String, ContentValues, String, String[])}
  • + *
  • {@link #updateWithOnConflict(String, ContentValues, String, String[], int)}
  • + *
+ *

+ * For DELETE statements, use any of the following instead. + *

    + *
  • {@link #delete(String, String, String[])}
  • + *
+ *

+ * For example, the following are good candidates for using this method: + *

    + *
  • ALTER TABLE
  • + *
  • CREATE or DROP table / trigger / view / index / virtual table
  • + *
  • REINDEX
  • + *
  • RELEASE
  • + *
  • SAVEPOINT
  • + *
  • PRAGMA that returns no data
  • + *
+ *

+ *

+ * When using {@link #enableWriteAheadLogging()}, journal_mode is + * automatically managed by this class. So, do not set journal_mode + * using "PRAGMA journal_mode'" statement if your app is using + * {@link #enableWriteAheadLogging()} + *

+ * + * @param sql the SQL statement to be executed. Multiple statements separated by semicolons are + * not supported. + * @param bindArgs only byte[], String, Long and Double are supported in bindArgs. + * @throws SQLException if the SQL string is invalid + */ + public void execSQL(String sql, Object[] bindArgs) throws SQLException { + if (bindArgs == null) { + throw new IllegalArgumentException("Empty bindArgs"); + } + executeSql(sql, bindArgs); + } + + private int executeSql(String sql, Object[] bindArgs) throws SQLException { + acquireReference(); + try { + if (DatabaseUtils.getSqlStatementType(sql) == DatabaseUtils.STATEMENT_ATTACH) { + boolean disableWal = false; + synchronized (mLock) { + if (!mHasAttachedDbsLocked) { + mHasAttachedDbsLocked = true; + disableWal = true; + } + } + if (disableWal) { + disableWriteAheadLogging(); + } + } + + SQLiteStatement statement = new SQLiteStatement(this, sql, bindArgs); + try { + return statement.executeUpdateDelete(); + } finally { + statement.close(); + } + } finally { + releaseReference(); + } + } + + /** + * Verifies that a SQL SELECT statement is valid by compiling it. + * If the SQL statement is not valid, this method will throw a {@link SQLiteException}. + * + * @param sql SQL to be validated + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @throws SQLiteException if {@code sql} is invalid + */ + public void validateSql(String sql, CancellationSignal cancellationSignal) { + getThreadSession().prepare(sql, + getThreadDefaultConnectionFlags(/* readOnly =*/ true), cancellationSignal, null); + } + + /** + * Returns true if the database is opened as read only. + * + * @return True if database is opened as read only. + */ + public boolean isReadOnly() { + synchronized (mLock) { + return isReadOnlyLocked(); + } + } + + private boolean isReadOnlyLocked() { + return (mConfigurationLocked.openFlags & OPEN_READ_MASK) == OPEN_READONLY; + } + + /** + * Returns true if the database is in-memory db. + * + * @return True if the database is in-memory. + * @hide + */ + public boolean isInMemoryDatabase() { + synchronized (mLock) { + return mConfigurationLocked.isInMemoryDb(); + } + } + + /** + * Returns true if the database is currently open. + * + * @return True if the database is currently open (has not been closed). + */ + public boolean isOpen() { + synchronized (mLock) { + return mConnectionPoolLocked != null; + } + } + + /** + * Returns true if the new version code is greater than the current database version. + * + * @param newVersion The new version code. + * @return True if the new version code is greater than the current database version. + */ + public boolean needUpgrade(int newVersion) { + return newVersion > getVersion(); + } + + /** + * Gets the path to the database file. + * + * @return The path to the database file. + */ + public final String getPath() { + synchronized (mLock) { + return mConfigurationLocked.path; + } + } + + /** + * Sets the locale for this database. Does nothing if this database has + * the {@link #NO_LOCALIZED_COLLATORS} flag set or was opened read only. + * + * @param locale The new locale. + * + * @throws SQLException if the locale could not be set. The most common reason + * for this is that there is no collator available for the locale you requested. + * In this case the database remains unchanged. + */ + public void setLocale(Locale locale) { + if (locale == null) { + throw new IllegalArgumentException("locale must not be null."); + } + + synchronized (mLock) { + throwIfNotOpenLocked(); + + final Locale oldLocale = mConfigurationLocked.locale; + mConfigurationLocked.locale = locale; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.locale = oldLocale; + throw ex; + } + } + } + + /** + * Sets the maximum size of the prepared-statement cache for this database. + * (size of the cache = number of compiled-sql-statements stored in the cache). + *

+ * Maximum cache size can ONLY be increased from its current size (default = 10). + * If this method is called with smaller size than the current maximum value, + * then IllegalStateException is thrown. + *

+ * This method is thread-safe. + * + * @param cacheSize the size of the cache. can be (0 to {@link #MAX_SQL_CACHE_SIZE}) + * @throws IllegalStateException if input cacheSize > {@link #MAX_SQL_CACHE_SIZE}. + */ + public void setMaxSqlCacheSize(int cacheSize) { + if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) { + throw new IllegalStateException( + "expected value between 0 and " + MAX_SQL_CACHE_SIZE); + } + + synchronized (mLock) { + throwIfNotOpenLocked(); + + final int oldMaxSqlCacheSize = mConfigurationLocked.maxSqlCacheSize; + mConfigurationLocked.maxSqlCacheSize = cacheSize; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.maxSqlCacheSize = oldMaxSqlCacheSize; + throw ex; + } + } + } + + /** + * Sets whether foreign key constraints are enabled for the database. + *

+ * By default, foreign key constraints are not enforced by the database. + * This method allows an application to enable foreign key constraints. + * It must be called each time the database is opened to ensure that foreign + * key constraints are enabled for the session. + *

+ * A good time to call this method is right after calling {@link #openOrCreateDatabase} + * or in the {@link SQLiteOpenHelper#onConfigure} callback. + *

+ * When foreign key constraints are disabled, the database does not check whether + * changes to the database will violate foreign key constraints. Likewise, when + * foreign key constraints are disabled, the database will not execute cascade + * delete or update triggers. As a result, it is possible for the database + * state to become inconsistent. To perform a database integrity check, + * call {@link #isDatabaseIntegrityOk}. + *

+ * This method must not be called while a transaction is in progress. + *

+ * See also SQLite Foreign Key Constraints + * for more details about foreign key constraint support. + *

+ * + * @param enable True to enable foreign key constraints, false to disable them. + * + * @throws IllegalStateException if the are transactions is in progress + * when this method is called. + */ + public void setForeignKeyConstraintsEnabled(boolean enable) { + synchronized (mLock) { + throwIfNotOpenLocked(); + + if (mConfigurationLocked.foreignKeyConstraintsEnabled == enable) { + return; + } + + mConfigurationLocked.foreignKeyConstraintsEnabled = enable; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.foreignKeyConstraintsEnabled = !enable; + throw ex; + } + } + } + + /** + * This method enables parallel execution of queries from multiple threads on the + * same database. It does this by opening multiple connections to the database + * and using a different database connection for each query. The database + * journal mode is also changed to enable writes to proceed concurrently with reads. + *

+ * When write-ahead logging is not enabled (the default), it is not possible for + * reads and writes to occur on the database at the same time. Before modifying the + * database, the writer implicitly acquires an exclusive lock on the database which + * prevents readers from accessing the database until the write is completed. + *

+ * In contrast, when write-ahead logging is enabled (by calling this method), write + * operations occur in a separate log file which allows reads to proceed concurrently. + * While a write is in progress, readers on other threads will perceive the state + * of the database as it was before the write began. When the write completes, readers + * on other threads will then perceive the new state of the database. + *

+ * It is a good idea to enable write-ahead logging whenever a database will be + * concurrently accessed and modified by multiple threads at the same time. + * However, write-ahead logging uses significantly more memory than ordinary + * journaling because there are multiple connections to the same database. + * So if a database will only be used by a single thread, or if optimizing + * concurrency is not very important, then write-ahead logging should be disabled. + *

+ * After calling this method, execution of queries in parallel is enabled as long as + * the database remains open. To disable execution of queries in parallel, either + * call {@link #disableWriteAheadLogging} or close the database and reopen it. + *

+ * The maximum number of connections used to execute queries in parallel is + * dependent upon the device memory and possibly other properties. + *

+ * If a query is part of a transaction, then it is executed on the same database handle the + * transaction was begun. + *

+ * Writers should use {@link #beginTransactionNonExclusive()} or + * {@link #beginTransactionWithListenerNonExclusive(SQLiteTransactionListener)} + * to start a transaction. Non-exclusive mode allows database file to be in readable + * by other threads executing queries. + *

+ * If the database has any attached databases, then execution of queries in parallel is NOT + * possible. Likewise, write-ahead logging is not supported for read-only databases + * or memory databases. In such cases, {@link #enableWriteAheadLogging} returns false. + *

+ * The best way to enable write-ahead logging is to pass the + * {@link #ENABLE_WRITE_AHEAD_LOGGING} flag to {@link #openDatabase}. This is + * more efficient than calling {@link #enableWriteAheadLogging}. + *

+	 *     SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+	 *             SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
+	 *             myDatabaseErrorHandler);
+	 *     db.enableWriteAheadLogging();
+	 * 
+ *

+ * Another way to enable write-ahead logging is to call {@link #enableWriteAheadLogging} + * after opening the database. + *

+	 *     SQLiteDatabase db = SQLiteDatabase.openDatabase("db_filename", cursorFactory,
+	 *             SQLiteDatabase.CREATE_IF_NECESSARY, myDatabaseErrorHandler);
+	 *     db.enableWriteAheadLogging();
+	 * 
+ *

+ * See also SQLite Write-Ahead Logging for + * more details about how write-ahead logging works. + *

+ * + * @return True if write-ahead logging is enabled. + * + * @throws IllegalStateException if there are transactions in progress at the + * time this method is called. WAL mode can only be changed when there are no + * transactions in progress. + * + * @see #ENABLE_WRITE_AHEAD_LOGGING + * @see #disableWriteAheadLogging + */ + public boolean enableWriteAheadLogging() { + synchronized (mLock) { + throwIfNotOpenLocked(); + + if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0) { + return true; + } + + if (isReadOnlyLocked()) { + // WAL doesn't make sense for readonly-databases. + // TODO: True, but connection pooling does still make sense... + return false; + } + + if (mConfigurationLocked.isInMemoryDb()) { + Log.i(TAG, "can't enable WAL for memory databases."); + return false; + } + + // make sure this database has NO attached databases because sqlite's write-ahead-logging + // doesn't work for databases with attached databases + if (mHasAttachedDbsLocked) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "this database: " + mConfigurationLocked.label + + " has attached databases. can't enable WAL."); + } + return false; + } + + mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING; + throw ex; + } + } + return true; + } + + /** + * This method disables the features enabled by {@link #enableWriteAheadLogging()}. + * + * @throws IllegalStateException if there are transactions in progress at the + * time this method is called. WAL mode can only be changed when there are no + * transactions in progress. + * + * @see #enableWriteAheadLogging + */ + public void disableWriteAheadLogging() { + synchronized (mLock) { + throwIfNotOpenLocked(); + + if ((mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) == 0) { + return; + } + + mConfigurationLocked.openFlags &= ~ENABLE_WRITE_AHEAD_LOGGING; + try { + mConnectionPoolLocked.reconfigure(mConfigurationLocked); + } catch (RuntimeException ex) { + mConfigurationLocked.openFlags |= ENABLE_WRITE_AHEAD_LOGGING; + throw ex; + } + } + } + + /** + * Returns true if write-ahead logging has been enabled for this database. + * + * @return True if write-ahead logging has been enabled for this database. + * + * @see #enableWriteAheadLogging + * @see #ENABLE_WRITE_AHEAD_LOGGING + */ + public boolean isWriteAheadLoggingEnabled() { + synchronized (mLock) { + throwIfNotOpenLocked(); + + return (mConfigurationLocked.openFlags & ENABLE_WRITE_AHEAD_LOGGING) != 0; + } + } + + /** + * Collect statistics about all open databases in the current process. + * Used by bug report. + */ + static ArrayList getDbStats() { + ArrayList dbStatsList = new ArrayList(); + for (SQLiteDatabase db : getActiveDatabases()) { + db.collectDbStats(dbStatsList); + } + return dbStatsList; + } + + private void collectDbStats(ArrayList dbStatsList) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + mConnectionPoolLocked.collectDbStats(dbStatsList); + } + } + } + + private static ArrayList getActiveDatabases() { + ArrayList databases = new ArrayList(); + synchronized (sActiveDatabases) { + databases.addAll(sActiveDatabases.keySet()); + } + return databases; + } + + /** + * Dump detailed information about all open databases in the current process. + * Used by bug report. + */ + static void dumpAll(Printer printer, boolean verbose) { + for (SQLiteDatabase db : getActiveDatabases()) { + db.dump(printer, verbose); + } + } + + private void dump(Printer printer, boolean verbose) { + synchronized (mLock) { + if (mConnectionPoolLocked != null) { + printer.println(""); + mConnectionPoolLocked.dump(printer, verbose); + } + } + } + + /** + * Returns list of full pathnames of all attached databases including the main database + * by executing 'pragma database_list' on the database. + * + * @return ArrayList of pairs of (database name, database file path) or null if the database + * is not open. + */ + public List> getAttachedDbs() { + ArrayList> attachedDbs = new ArrayList>(); + synchronized (mLock) { + if (mConnectionPoolLocked == null) { + return null; // not open + } + + if (!mHasAttachedDbsLocked) { + // No attached databases. + // There is a small window where attached databases exist but this flag is not + // set yet. This can occur when this thread is in a race condition with another + // thread that is executing the SQL statement: "attach database as " + // If this thread is NOT ok with such a race condition (and thus possibly not + // receivethe entire list of attached databases), then the caller should ensure + // that no thread is executing any SQL statements while a thread is calling this + // method. Typically, this method is called when 'adb bugreport' is done or the + // caller wants to collect stats on the database and all its attached databases. + attachedDbs.add(new Pair("main", mConfigurationLocked.path)); + return attachedDbs; + } + + acquireReference(); + } + + try { + // has attached databases. query sqlite to get the list of attached databases. + Cursor c = null; + try { + c = rawQuery("pragma database_list;", null); + while (c.moveToNext()) { + // sqlite returns a row for each database in the returned list of databases. + // in each row, + // 1st column is the database name such as main, or the database + // name specified on the "ATTACH" command + // 2nd column is the database file path. + attachedDbs.add(new Pair(c.getString(1), c.getString(2))); + } + } finally { + if (c != null) { + c.close(); + } + } + return attachedDbs; + } finally { + releaseReference(); + } + } + + /** + * Runs 'pragma integrity_check' on the given database (and all the attached databases) + * and returns true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + *

+ * If the result is false, then this method logs the errors reported by the integrity_check + * command execution. + *

+ * Note that 'pragma integrity_check' on a database can take a long time. + * + * @return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + */ + public boolean isDatabaseIntegrityOk() { + acquireReference(); + try { + List> attachedDbs = null; + try { + attachedDbs = getAttachedDbs(); + if (attachedDbs == null) { + throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " + + "be retrieved. probably because the database is closed"); + } + } catch (SQLiteException e) { + // can't get attachedDb list. do integrity check on the main database + attachedDbs = new ArrayList>(); + attachedDbs.add(new Pair("main", getPath())); + } + + for (int i = 0; i < attachedDbs.size(); i++) { + Pair p = attachedDbs.get(i); + SQLiteStatement prog = null; + try { + prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);"); + String rslt = prog.simpleQueryForString(); + if (!rslt.equalsIgnoreCase("ok")) { + // integrity_checker failed on main or attached databases + Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + return false; + } + } finally { + if (prog != null) prog.close(); + } + } + } finally { + releaseReference(); + } + return true; + } + + @Override + public String toString() { + return "SQLiteDatabase: " + getPath(); + } + + private void throwIfNotOpenLocked() { + if (mConnectionPoolLocked == null) { + throw new IllegalStateException("The database '" + mConfigurationLocked.label + + "' is not open."); + } + } + + /** + * Used to allow returning sub-classes of {@link Cursor} when calling query. + */ public interface CursorFactory { /** * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. */ public Cursor newCursor(SQLiteDatabase db, - SQLiteCursorDriver masterQuery, String editTable, - SQLiteQuery query); - } - // --- - - public int update(String table, ContentValues values, String whereClause, String[] whereArgs) { - return -1; + SQLiteCursorDriver masterQuery, String editTable, + SQLiteQuery query); } - // --- - - public int delete(String table, String whereClause, String[] whereArgs) { - return 0; + /** + * A callback interface for a custom sqlite3 function. + * This can be used to create a function that can be called from + * sqlite3 database triggers. + * @hide + */ + public interface CustomFunction { + public void callback(String[] args); } - // TODO: this belongs in SQLiteClosable - public void close() {} - - public Cursor rawQuery(String sql, String[] selectionArgs, CancellationSignal cancellationSignal) { - return null; + public static boolean hasCodec() { + return SQLiteConnection.hasCodec(); } - public Cursor rawQuery(String sql, String[] selectionArgs) { - return null; + public void enableLocalizedCollators() { + mConnectionPoolLocked.enableLocalizedCollators(); } } diff --git a/src/api-impl/android/database/sqlite/SQLiteDatabaseConfiguration.java b/src/api-impl/android/database/sqlite/SQLiteDatabaseConfiguration.java new file mode 100644 index 00000000..aef40bfb --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDatabaseConfiguration.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import java.util.ArrayList; +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Describes how to configure a database. + *

+ * The purpose of this object is to keep track of all of the little + * configuration settings that are applied to a database after it + * is opened so that they can be applied to all connections in the + * connection pool uniformly. + *

+ * Each connection maintains its own copy of this object so it can + * keep track of which settings have already been applied. + *

+ * + * @hide + */ +public final class SQLiteDatabaseConfiguration { + // The pattern we use to strip email addresses from database paths + // when constructing a label to use in log messages. + private static final Pattern EMAIL_IN_DB_PATTERN = + Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+"); + + /** + * Special path used by in-memory databases. + */ + public static final String MEMORY_DB_PATH = ":memory:"; + + /** + * The database path. + */ + public final String path; + + /** + * The label to use to describe the database when it appears in logs. + * This is derived from the path but is stripped to remove PII. + */ + public final String label; + + /** + * The flags used to open the database. + */ + public int openFlags; + + /** + * The maximum size of the prepared statement cache for each database connection. + * Must be non-negative. + * + * Default is 25. + */ + public int maxSqlCacheSize; + + /** + * The database locale. + * + * Default is the value returned by {@link Locale#getDefault()}. + */ + public Locale locale; + + /** + * True if foreign key constraints are enabled. + * + * Default is false. + */ + public boolean foreignKeyConstraintsEnabled; + + /** + * The custom functions to register. + */ + public final ArrayList customFunctions = + new ArrayList(); + + /** + * Creates a database configuration with the required parameters for opening a + * database and default values for all other parameters. + * + * @param path The database path. + * @param openFlags Open flags for the database, such as {@link SQLiteDatabase#OPEN_READWRITE}. + */ + public SQLiteDatabaseConfiguration(String path, int openFlags) { + if (path == null) { + throw new IllegalArgumentException("path must not be null."); + } + + this.path = path; + label = stripPathForLogs(path); + this.openFlags = openFlags; + + // Set default values for optional parameters. + maxSqlCacheSize = 25; + locale = Locale.getDefault(); + } + + /** + * Creates a database configuration as a copy of another configuration. + * + * @param other The other configuration. + */ + public SQLiteDatabaseConfiguration(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + + this.path = other.path; + this.label = other.label; + updateParametersFrom(other); + } + + /** + * Updates the non-immutable parameters of this configuration object + * from the other configuration object. + * + * @param other The object from which to copy the parameters. + */ + public void updateParametersFrom(SQLiteDatabaseConfiguration other) { + if (other == null) { + throw new IllegalArgumentException("other must not be null."); + } + if (!path.equals(other.path)) { + throw new IllegalArgumentException("other configuration must refer to " + + "the same database."); + } + + openFlags = other.openFlags; + maxSqlCacheSize = other.maxSqlCacheSize; + locale = other.locale; + foreignKeyConstraintsEnabled = other.foreignKeyConstraintsEnabled; + customFunctions.clear(); + customFunctions.addAll(other.customFunctions); + } + + /** + * Returns true if the database is in-memory. + * @return True if the database is in-memory. + */ + public boolean isInMemoryDb() { + return path.equalsIgnoreCase(MEMORY_DB_PATH); + } + + private static String stripPathForLogs(String path) { + /* Strip off all URI parameters. This is in case a SEE database is + * opened with the password specified as a URI parameter. We do not + * want the password to appear in any log files. */ + int iIdx = path.indexOf('?'); + if( iIdx>=0 ){ + path = (String) path.subSequence(0, iIdx); + } + + if (path.indexOf('@') == -1) { + return path; + } + return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY"); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDatabaseCorruptException.java b/src/api-impl/android/database/sqlite/SQLiteDatabaseCorruptException.java new file mode 100644 index 00000000..9314a653 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDatabaseCorruptException.java @@ -0,0 +1,32 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that the SQLite database file is corrupt. + */ +public class SQLiteDatabaseCorruptException extends SQLiteException { + public SQLiteDatabaseCorruptException() {} + + public SQLiteDatabaseCorruptException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDatabaseLockedException.java b/src/api-impl/android/database/sqlite/SQLiteDatabaseLockedException.java new file mode 100644 index 00000000..248eb41f --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDatabaseLockedException.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * Thrown if the database engine was unable to acquire the + * database locks it needs to do its job. If the statement is a [COMMIT] + * or occurs outside of an explicit transaction, then you can retry the + * statement. If the statement is not a [COMMIT] and occurs within a + * explicit transaction then you should rollback the transaction before + * continuing. + */ +public class SQLiteDatabaseLockedException extends SQLiteException { + public SQLiteDatabaseLockedException() {} + + public SQLiteDatabaseLockedException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDatatypeMismatchException.java b/src/api-impl/android/database/sqlite/SQLiteDatatypeMismatchException.java new file mode 100644 index 00000000..e16ca117 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDatatypeMismatchException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteDatatypeMismatchException extends SQLiteException { + public SQLiteDatatypeMismatchException() {} + + public SQLiteDatatypeMismatchException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDebug.java b/src/api-impl/android/database/sqlite/SQLiteDebug.java new file mode 100644 index 00000000..94835df3 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDebug.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2007 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import java.util.ArrayList; + +import android.util.Log; +import android.util.Printer; + +/** + * Provides debugging info about all SQLite databases running in the current process. + * + * {@hide} + */ +public final class SQLiteDebug { + private static native void nativeGetPagerStats(PagerStats stats); + + /** + * Controls the printing of informational SQL log messages. + * + * Enable using "adb shell setprop log.tag.SQLiteLog VERBOSE". + */ + public static final boolean DEBUG_SQL_LOG = + Log.isLoggable("SQLiteLog", Log.VERBOSE); + + /** + * Controls the printing of SQL statements as they are executed. + * + * Enable using "adb shell setprop log.tag.SQLiteStatements VERBOSE". + */ + public static final boolean DEBUG_SQL_STATEMENTS = + Log.isLoggable("SQLiteStatements", Log.VERBOSE); + + /** + * Controls the printing of wall-clock time taken to execute SQL statements + * as they are executed. + * + * Enable using "adb shell setprop log.tag.SQLiteTime VERBOSE". + */ + public static final boolean DEBUG_SQL_TIME = + Log.isLoggable("SQLiteTime", Log.VERBOSE); + + /** + * True to enable database performance testing instrumentation. + * @hide + */ + public static final boolean DEBUG_LOG_SLOW_QUERIES = false; + + private SQLiteDebug() { + } + + /** + * Determines whether a query should be logged. + * + * Reads the "db.log.slow_query_threshold" system property, which can be changed + * by the user at any time. If the value is zero, then all queries will + * be considered slow. If the value does not exist or is negative, then no queries will + * be considered slow. + * + * This value can be changed dynamically while the system is running. + * For example, "adb shell setprop db.log.slow_query_threshold 200" will + * log all queries that take 200ms or longer to run. + * @hide + */ + public static final boolean shouldLogSlowQuery(long elapsedTimeMillis) { + int slowQueryMillis = Integer.parseInt( + System.getProperty("db.log.slow_query_threshold", "10000") + ); + return slowQueryMillis >= 0 && elapsedTimeMillis >= slowQueryMillis; + } + + /** + * Contains statistics about the active pagers in the current process. + * + * @see #nativeGetPagerStats(PagerStats) + */ + public static class PagerStats { + /** the current amount of memory checked out by sqlite using sqlite3_malloc(). + * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html + */ + public int memoryUsed; + + /** the number of bytes of page cache allocation which could not be sattisfied by the + * SQLITE_CONFIG_PAGECACHE buffer and where forced to overflow to sqlite3_malloc(). + * The returned value includes allocations that overflowed because they where too large + * (they were larger than the "sz" parameter to SQLITE_CONFIG_PAGECACHE) and allocations + * that overflowed because no space was left in the page cache. + * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html + */ + public int pageCacheOverflow; + + /** records the largest memory allocation request handed to sqlite3. + * documented at http://www.sqlite.org/c3ref/c_status_malloc_size.html + */ + public int largestMemAlloc; + + /** a list of {@link DbStats} - one for each main database opened by the applications + * running on the android device + */ + public ArrayList dbStats; + } + + /** + * contains statistics about a database + */ + public static class DbStats { + /** name of the database */ + public String dbName; + + /** the page size for the database */ + public long pageSize; + + /** the database size */ + public long dbSize; + + /** documented here http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html */ + public int lookaside; + + /** statement cache stats: hits/misses/cachesize */ + public String cache; + + public DbStats(String dbName, long pageCount, long pageSize, int lookaside, + int hits, int misses, int cachesize) { + this.dbName = dbName; + this.pageSize = pageSize / 1024; + dbSize = (pageCount * pageSize) / 1024; + this.lookaside = lookaside; + this.cache = hits + "/" + misses + "/" + cachesize; + } + } + + /** + * return all pager and database stats for the current process. + * @return {@link PagerStats} + */ + public static PagerStats getDatabaseInfo() { + PagerStats stats = new PagerStats(); + nativeGetPagerStats(stats); + stats.dbStats = SQLiteDatabase.getDbStats(); + return stats; + } + + /** + * Dumps detailed information about all databases used by the process. + * @param printer The printer for dumping database state. + * @param args Command-line arguments supplied to dumpsys dbinfo + */ + public static void dump(Printer printer, String[] args) { + boolean verbose = false; + for (String arg : args) { + if (arg.equals("-v")) { + verbose = true; + } + } + + SQLiteDatabase.dumpAll(printer, verbose); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDirectCursorDriver.java b/src/api-impl/android/database/sqlite/SQLiteDirectCursorDriver.java index 550e0c2f..476cd9ae 100644 --- a/src/api-impl/android/database/sqlite/SQLiteDirectCursorDriver.java +++ b/src/api-impl/android/database/sqlite/SQLiteDirectCursorDriver.java @@ -52,7 +52,7 @@ public final class SQLiteDirectCursorDriver implements SQLiteCursorDriver { cursor = factory.newCursor(mDatabase, this, mEditTable, query); } } catch (RuntimeException ex) { - // query.close(); + query.close(); throw ex; } diff --git a/src/api-impl/android/database/sqlite/SQLiteDiskIOException.java b/src/api-impl/android/database/sqlite/SQLiteDiskIOException.java new file mode 100644 index 00000000..aa3a94e5 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDiskIOException.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that an IO error occured while accessing the + * SQLite database file. + */ +public class SQLiteDiskIOException extends SQLiteException { + public SQLiteDiskIOException() {} + + public SQLiteDiskIOException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteDoneException.java b/src/api-impl/android/database/sqlite/SQLiteDoneException.java new file mode 100644 index 00000000..d83ad4ed --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteDoneException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that the SQLite program is done. + * Thrown when an operation that expects a row (such as {@link + * SQLiteStatement#simpleQueryForString} or {@link + * SQLiteStatement#simpleQueryForLong}) does not get one. + */ +public class SQLiteDoneException extends SQLiteException { + public SQLiteDoneException() {} + + public SQLiteDoneException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteException.java b/src/api-impl/android/database/sqlite/SQLiteException.java index af3913b7..8f4c8737 100644 --- a/src/api-impl/android/database/sqlite/SQLiteException.java +++ b/src/api-impl/android/database/sqlite/SQLiteException.java @@ -1,4 +1,39 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + package android.database.sqlite; -public class SQLiteException extends Exception { +import android.database.SQLException; + +/** + * A SQLite exception that indicates there was an error with SQL parsing or execution. + */ +public class SQLiteException extends SQLException { + public SQLiteException() { + } + + public SQLiteException(String error) { + super(error); + } + + public SQLiteException(String error, Throwable cause) { + super(error, cause); + } } diff --git a/src/api-impl/android/database/sqlite/SQLiteFullException.java b/src/api-impl/android/database/sqlite/SQLiteFullException.java new file mode 100644 index 00000000..44128c5d --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteFullException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that the SQLite database is full. + */ +public class SQLiteFullException extends SQLiteException { + public SQLiteFullException() {} + + public SQLiteFullException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteGlobal.java b/src/api-impl/android/database/sqlite/SQLiteGlobal.java new file mode 100644 index 00000000..5cfdd039 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteGlobal.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import android.content.Context; +import android.os.StatFs; + +/** + * Provides access to SQLite functions that affect all database connection, + * such as memory management. + * + * The native code associated with SQLiteGlobal is also sets global configuration options + * using sqlite3_config() then calls sqlite3_initialize() to ensure that the SQLite + * library is properly initialized exactly once before any other framework or application + * code has a chance to run. + * + * Verbose SQLite logging is enabled if the "log.tag.SQLiteLog" property is set to "V". + * (per {@link SQLiteDebug#DEBUG_SQL_LOG}). + * + * @hide + */ +public final class SQLiteGlobal { + private static final String TAG = "SQLiteGlobal"; + + private static final Object sLock = new Object(); + private static int sDefaultPageSize; + + private static native int nativeReleaseMemory(); + + private SQLiteGlobal() { + } + + /** + * Attempts to release memory by pruning the SQLite page cache and other + * internal data structures. + * + * @return The number of bytes that were freed. + */ + public static int releaseMemory() { + return nativeReleaseMemory(); + } + + /** + * Gets the default page size to use when creating a database. + */ + public static int getDefaultPageSize() { + synchronized (sLock) { + if (sDefaultPageSize == 0) { + // If there is an issue accessing /data, something is so seriously + // wrong that we just let the IllegalArgumentException propagate. + sDefaultPageSize = new StatFs(Context.this_application.getFilesDir().getPath()).getBlockSize(); + } + return 1024; + } + } + + /** + * Gets the default journal mode when WAL is not in use. + */ + public static String getDefaultJournalMode() { + return "delete"; + } + + /** + * Gets the journal size limit in bytes. + */ + public static int getJournalSizeLimit() { + return 10000; + } + + /** + * Gets the default database synchronization mode when WAL is not in use. + */ + public static String getDefaultSyncMode() { + return "normal"; + } + + /** + * Gets the database synchronization mode when in WAL mode. + */ + public static String getWALSyncMode() { + return "normal"; + } + + /** + * Gets the WAL auto-checkpoint integer in database pages. + */ + public static int getWALAutoCheckpoint() { + int value = 1000; + return Math.max(1, value); + } + + /** + * Gets the connection pool size when in WAL mode. + */ + public static int getWALConnectionPoolSize() { + int value = 10; + return Math.max(2, value); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteMisuseException.java b/src/api-impl/android/database/sqlite/SQLiteMisuseException.java new file mode 100644 index 00000000..9b5d90a5 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteMisuseException.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2008 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * This error can occur if the application creates a SQLiteStatement object and allows multiple + * threads in the application use it at the same time. + * Sqlite returns this error if bind and execute methods on this object occur at the same time + * from multiple threads, like so: + * thread # 1: in execute() method of the SQLiteStatement object + * while thread # 2: is in bind..() on the same object. + *

+ * FIX this by NEVER sharing the same SQLiteStatement object between threads. + * Create a local instance of the SQLiteStatement whenever it is needed, use it and close it ASAP. + * NEVER make it globally available. + */ +public class SQLiteMisuseException extends SQLiteException { + public SQLiteMisuseException() {} + + public SQLiteMisuseException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteOpenHelper.java b/src/api-impl/android/database/sqlite/SQLiteOpenHelper.java index 44b804d9..c776840b 100644 --- a/src/api-impl/android/database/sqlite/SQLiteOpenHelper.java +++ b/src/api-impl/android/database/sqlite/SQLiteOpenHelper.java @@ -1,122 +1,443 @@ +/* + * Copyright (C) 2007 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + + package android.database.sqlite; import android.content.Context; +import android.database.DatabaseErrorHandler; import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.util.Log; +import java.io.File; +/** + * A helper class to manage database creation and version management. + * + *

You create a subclass implementing {@link #onCreate}, {@link #onUpgrade} and + * optionally {@link #onOpen}, and this class takes care of opening the database + * if it exists, creating it if it does not, and upgrading it as necessary. + * Transactions are used to make sure the database is always in a sensible state. + * + *

This class makes it easy for {@link android.content.ContentProvider} + * implementations to defer opening and upgrading the database until first use, + * to avoid blocking application startup with long-running database upgrades. + * + *

For an example, see the NotePadProvider class in the NotePad sample application, + * in the samples/ directory of the SDK.

+ * + *

Note: this class assumes + * monotonically increasing version numbers for upgrades.

+ */ public abstract class SQLiteOpenHelper { + private static final String TAG = SQLiteOpenHelper.class.getSimpleName(); + + // When true, getReadableDatabase returns a read-only database if it is just being opened. + // The database handle is reopened in read/write mode when getWritableDatabase is called. + // We leave this behavior disabled in production because it is inefficient and breaks + // many applications. For debugging purposes it can be useful to turn on strict + // read-only semantics to catch applications that call getReadableDatabase when they really + // wanted getWritableDatabase. + private static final boolean DEBUG_STRICT_READONLY = false; + + private final Context mContext; + private final String mName; + private final CursorFactory mFactory; + private final int mNewVersion; + private final int mMinimumSupportedVersion; + + private SQLiteDatabase mDatabase; + private boolean mIsInitializing; + private boolean mEnableWriteAheadLogging; + private final DatabaseErrorHandler mErrorHandler; + + /** + * Create a helper object to create, open, and/or manage a database. + * This method always returns very quickly. The database is not actually + * created or opened until one of {@link #getWritableDatabase} or + * {@link #getReadableDatabase} is called. + * + * @param context to use to open or create the database + * @param name of the database file, or null for an in-memory database + * @param factory to use for creating cursor objects, or null for the default + * @param version number of the database (starting at 1); if the database is older, + * {@link #onUpgrade} will be used to upgrade the database; if the database is + * newer, {@link #onDowngrade} will be used to downgrade the database + */ public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version) { this(context, name, factory, version, null); } - public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, DatabaseErrorHandler errorHandler) { - if (version < 1) - throw new IllegalArgumentException("Version must be >= 1, was " + version); - - /* mContext = context; - mName = name; - mFactory = factory; - mNewVersion = version; - mErrorHandler = errorHandler;*/ + /** + * Create a helper object to create, open, and/or manage a database. + * The database is not actually created or opened until one of + * {@link #getWritableDatabase} or {@link #getReadableDatabase} is called. + * + *

Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be + * used to handle corruption when sqlite reports database corruption.

+ * + * @param context to use to open or create the database + * @param name of the database file, or null for an in-memory database + * @param factory to use for creating cursor objects, or null for the default + * @param version number of the database (starting at 1); if the database is older, + * {@link #onUpgrade} will be used to upgrade the database; if the database is + * newer, {@link #onDowngrade} will be used to downgrade the database + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption, or null to use the default error handler. + */ + public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, + DatabaseErrorHandler errorHandler) { + this(context, name, factory, version, 0, errorHandler); } - public void close() {} + /** + * Same as {@link #SQLiteOpenHelper(Context, String, CursorFactory, int, DatabaseErrorHandler)} + * but also accepts an integer minimumSupportedVersion as a convenience for upgrading very old + * versions of this database that are no longer supported. If a database with older version that + * minimumSupportedVersion is found, it is simply deleted and a new database is created with the + * given name and version + * + * @param context to use to open or create the database + * @param name the name of the database file, null for a temporary in-memory database + * @param factory to use for creating cursor objects, null for default + * @param version the required version of the database + * @param minimumSupportedVersion the minimum version that is supported to be upgraded to + * {@code version} via {@link #onUpgrade}. If the current database version is lower + * than this, database is simply deleted and recreated with the version passed in + * {@code version}. {@link #onBeforeDelete} is called before deleting the database + * when this happens. This is 0 by default. + * @param errorHandler the {@link DatabaseErrorHandler} to be used when sqlite reports database + * corruption, or null to use the default error handler. + * @see #onBeforeDelete(SQLiteDatabase) + * @see #SQLiteOpenHelper(Context, String, CursorFactory, int, DatabaseErrorHandler) + * @see #onUpgrade(SQLiteDatabase, int, int) + * @hide + */ + public SQLiteOpenHelper(Context context, String name, CursorFactory factory, int version, + int minimumSupportedVersion, DatabaseErrorHandler errorHandler) { + if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version); + mContext = context; + mName = name; + mFactory = factory; + mNewVersion = version; + mErrorHandler = errorHandler; + mMinimumSupportedVersion = Math.max(0, minimumSupportedVersion); + } + + /** + * Return the name of the SQLite database being opened, as given to + * the constructor. + */ + public String getDatabaseName() { + return mName; + } + + /** + * Enables or disables the use of write-ahead logging for the database. + * + * Write-ahead logging cannot be used with read-only databases so the value of + * this flag is ignored if the database is opened read-only. + * + * @param enabled True if write-ahead logging should be enabled, false if it + * should be disabled. + * + * @see SQLiteDatabase#enableWriteAheadLogging() + */ + public void setWriteAheadLoggingEnabled(boolean enabled) { + synchronized (this) { + if (mEnableWriteAheadLogging != enabled) { + if (mDatabase != null && mDatabase.isOpen() && !mDatabase.isReadOnly()) { + if (enabled) { + mDatabase.enableWriteAheadLogging(); + } else { + mDatabase.disableWriteAheadLogging(); + } + } + mEnableWriteAheadLogging = enabled; + } + } + } + + /** + * Create and/or open a database that will be used for reading and writing. + * The first time this is called, the database will be opened and + * {@link #onCreate}, {@link #onUpgrade} and/or {@link #onOpen} will be + * called. + * + *

Once opened successfully, the database is cached, so you can + * call this method every time you need to write to the database. + * (Make sure to call {@link #close} when you no longer need the database.) + * Errors such as bad permissions or a full disk may cause this method + * to fail, but future attempts may succeed if the problem is fixed.

+ * + *

Database upgrade may take a long time, you + * should not call this method from the application main thread, including + * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. + * + * @throws SQLiteException if the database cannot be opened for writing + * @return a read/write database object valid until {@link #close} is called + */ public SQLiteDatabase getWritableDatabase() { synchronized (this) { return getDatabaseLocked(true); } } + /** + * Create and/or open a database. This will be the same object returned by + * {@link #getWritableDatabase} unless some problem, such as a full disk, + * requires the database to be opened read-only. In that case, a read-only + * database object will be returned. If the problem is fixed, a future call + * to {@link #getWritableDatabase} may succeed, in which case the read-only + * database object will be closed and the read/write object will be returned + * in the future. + * + *

Like {@link #getWritableDatabase}, this method may + * take a long time to return, so you should not call it from the + * application main thread, including from + * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. + * + * @throws SQLiteException if the database cannot be opened + * @return a database object valid until {@link #getWritableDatabase} + * or {@link #close} is called. + */ + public SQLiteDatabase getReadableDatabase() { + synchronized (this) { + return getDatabaseLocked(false); + } + } + private SQLiteDatabase getDatabaseLocked(boolean writable) { - return SQLiteDatabase.create(null); // return an empty database, surely the app can handle that - /* - if (mDatabase != null) { - if (!mDatabase.isOpen()) { - // Darn! The user closed the database by calling mDatabase.close(). - mDatabase = null; - } else if (!writable || !mDatabase.isReadOnly()) { - // The database is already open for business. - return mDatabase; - } - } + if (mDatabase != null) { + if (!mDatabase.isOpen()) { + // Darn! The user closed the database by calling mDatabase.close(). + mDatabase = null; + } else if (!writable || !mDatabase.isReadOnly()) { + // The database is already open for business. + return mDatabase; + } + } - if (mIsInitializing) { - throw new IllegalStateException("getDatabase called recursively"); - } + if (mIsInitializing) { + throw new IllegalStateException("getDatabase called recursively"); + } - SQLiteDatabase db = mDatabase; - try { - mIsInitializing = true; + SQLiteDatabase db = mDatabase; + try { + mIsInitializing = true; - if (db != null) { - if (writable && db.isReadOnly()) { - db.reopenReadWrite(); - } - } else if (mName == null) { - db = SQLiteDatabase.create(null); - } else { - try { - if (DEBUG_STRICT_READONLY && !writable) { - final String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, - SQLiteDatabase.OPEN_READONLY, mErrorHandler); - } else { - db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ? - Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0, - mFactory, mErrorHandler); - } - } catch (SQLiteException ex) { - if (writable) { - throw ex; - } - Log.e(TAG, "Couldn't open " + mName - + " for writing (will try read-only):", ex); - final String path = mContext.getDatabasePath(mName).getPath(); - db = SQLiteDatabase.openDatabase(path, mFactory, - SQLiteDatabase.OPEN_READONLY, mErrorHandler); - } - } + if (db != null) { + if (writable && db.isReadOnly()) { + db.reopenReadWrite(); + } + } else if (mName == null) { + db = SQLiteDatabase.create(null); + } else { + String path = mName; + if (!path.startsWith("file:")) { + path = mContext.getDatabasePath(path).getPath(); + } + try { + if (DEBUG_STRICT_READONLY && !writable) { + db = SQLiteDatabase.openDatabase(path, mFactory, + SQLiteDatabase.OPEN_READONLY, mErrorHandler); + } else { + db = SQLiteDatabase.openOrCreateDatabase( + path, mFactory, mErrorHandler + ); + } + } catch (SQLiteException ex) { + if (writable) { + throw ex; + } + Log.e(TAG, "Couldn't open " + mName + + " for writing (will try read-only):", ex); + db = SQLiteDatabase.openDatabase(path, mFactory, + SQLiteDatabase.OPEN_READONLY, mErrorHandler); + } + } - onConfigure(db); + onConfigure(db); - final int version = db.getVersion(); - if (version != mNewVersion) { - if (db.isReadOnly()) { - throw new SQLiteException("Can't upgrade read-only database from version " + - db.getVersion() + " to " + mNewVersion + ": " + mName); - } + final int version = db.getVersion(); + if (version != mNewVersion) { + if (db.isReadOnly()) { + throw new SQLiteException("Can't upgrade read-only database from version " + + db.getVersion() + " to " + mNewVersion + ": " + mName); + } - db.beginTransaction(); - try { - if (version == 0) { - onCreate(db); - } else { - if (version > mNewVersion) { - onDowngrade(db, version, mNewVersion); - } else { - onUpgrade(db, version, mNewVersion); - } - } - db.setVersion(mNewVersion); - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - } + if (version > 0 && version < mMinimumSupportedVersion) { + File databaseFile = new File(db.getPath()); + onBeforeDelete(db); + db.close(); + if (SQLiteDatabase.deleteDatabase(databaseFile)) { + mIsInitializing = false; + return getDatabaseLocked(writable); + } else { + throw new IllegalStateException("Unable to delete obsolete database " + + mName + " with version " + version); + } + } else { + db.beginTransaction(); + try { + if (version == 0) { + onCreate(db); + } else { + if (version > mNewVersion) { + onDowngrade(db, version, mNewVersion); + } else { + onUpgrade(db, version, mNewVersion); + } + } + db.setVersion(mNewVersion); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } - onOpen(db); + onOpen(db); - if (db.isReadOnly()) { - Log.w(TAG, "Opened " + mName + " in read-only mode"); - } + if (db.isReadOnly()) { + Log.w(TAG, "Opened " + mName + " in read-only mode"); + } - mDatabase = db; - return db; - } finally { - mIsInitializing = false; - if (db != null && db != mDatabase) { - db.close(); - } - } - */} + mDatabase = db; + return db; + } finally { + mIsInitializing = false; + if (db != null && db != mDatabase) { + db.close(); + } + } + } + + /** + * Close any open database object. + */ + public synchronized void close() { + if (mIsInitializing) throw new IllegalStateException("Closed during initialization"); + + if (mDatabase != null && mDatabase.isOpen()) { + mDatabase.close(); + mDatabase = null; + } + } + + /** + * Called when the database connection is being configured, to enable features such as + * write-ahead logging or foreign key support. + *

+ * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade}, or + * {@link #onOpen} are called. It should not modify the database except to configure the + * database connection as required. + *

+ *

+ * This method should only call methods that configure the parameters of the database + * connection, such as {@link SQLiteDatabase#enableWriteAheadLogging} + * {@link SQLiteDatabase#setForeignKeyConstraintsEnabled}, {@link SQLiteDatabase#setLocale}, + * {@link SQLiteDatabase#setMaximumSize}, or executing PRAGMA statements. + *

+ * + * @param db The database. + */ + public void onConfigure(SQLiteDatabase db) {} + + /** + * Called before the database is deleted when the version returned by + * {@link SQLiteDatabase#getVersion()} is lower than the minimum supported version passed (if at + * all) while creating this helper. After the database is deleted, a fresh database with the + * given version is created. This will be followed by {@link #onConfigure(SQLiteDatabase)} and + * {@link #onCreate(SQLiteDatabase)} being called with a new SQLiteDatabase object + * + * @param db the database opened with this helper + * @see #SQLiteOpenHelper(Context, String, CursorFactory, int, int, DatabaseErrorHandler) + * @hide + */ + public void onBeforeDelete(SQLiteDatabase db) { + } + + /** + * Called when the database is created for the first time. This is where the + * creation of tables and the initial population of the tables should happen. + * + * @param db The database. + */ + public abstract void onCreate(SQLiteDatabase db); + + /** + * Called when the database needs to be upgraded. The implementation + * should use this method to drop tables, add tables, or do anything else it + * needs to upgrade to the new schema version. + * + *

+ * The SQLite ALTER TABLE documentation can be found + * here. If you add new columns + * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns + * you can use ALTER TABLE to rename the old table, then create the new table and then + * populate the new table with the contents of the old table. + *

+ * This method executes within a transaction. If an exception is thrown, all changes + * will automatically be rolled back. + *

+ * + * @param db The database. + * @param oldVersion The old database version. + * @param newVersion The new database version. + */ + public abstract void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); + + /** + * Called when the database needs to be downgraded. This is strictly similar to + * {@link #onUpgrade} method, but is called whenever current version is newer than requested one. + * However, this method is not abstract, so it is not mandatory for a customer to + * implement it. If not overridden, default implementation will reject downgrade and + * throws SQLiteException + * + *

+ * This method executes within a transaction. If an exception is thrown, all changes + * will automatically be rolled back. + *

+ * + * @param db The database. + * @param oldVersion The old database version. + * @param newVersion The new database version. + */ + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + throw new SQLiteException("Can't downgrade database from version " + + oldVersion + " to " + newVersion); + } + + /** + * Called when the database has been opened. The implementation + * should check {@link SQLiteDatabase#isReadOnly} before updating the + * database. + *

+ * This method is called after the database connection has been configured + * and after the database schema has been created, upgraded or downgraded as necessary. + * If the database connection must be configured in some way before the schema + * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead. + *

+ * + * @param db The database. + */ + public void onOpen(SQLiteDatabase db) {} } diff --git a/src/api-impl/android/database/sqlite/SQLiteOutOfMemoryException.java b/src/api-impl/android/database/sqlite/SQLiteOutOfMemoryException.java new file mode 100644 index 00000000..6c7668e5 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteOutOfMemoryException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteOutOfMemoryException extends SQLiteException { + public SQLiteOutOfMemoryException() {} + + public SQLiteOutOfMemoryException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteProgram.java b/src/api-impl/android/database/sqlite/SQLiteProgram.java index 2601d0c2..4a75ae19 100644 --- a/src/api-impl/android/database/sqlite/SQLiteProgram.java +++ b/src/api-impl/android/database/sqlite/SQLiteProgram.java @@ -13,14 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ package android.database.sqlite; -// import android.database.DatabaseUtils; +import android.database.DatabaseUtils; import android.os.CancellationSignal; -import java.util.Arrays; -class SQLiteSession {} +import java.util.Arrays; /** * A base class for compiled SQLite programs. @@ -28,197 +31,192 @@ class SQLiteSession {} * This class is not thread-safe. *

*/ -public abstract class SQLiteProgram /*extends SQLiteClosable*/ { +public abstract class SQLiteProgram extends SQLiteClosable { private static final String[] EMPTY_STRING_ARRAY = new String[0]; private final SQLiteDatabase mDatabase; private final String mSql; - private final boolean mReadOnly = false; - private final String[] mColumnNames = {"YYY"}; - private final int mNumParameters = -1; - private final Object[] mBindArgs = {}; + private final boolean mReadOnly; + private final String[] mColumnNames; + private final int mNumParameters; + private final Object[] mBindArgs; SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs, - CancellationSignal cancellationSignalForPrepare) { + CancellationSignal cancellationSignalForPrepare) { mDatabase = db; mSql = sql.trim(); -/* - int n = DatabaseUtils.getSqlStatementType(mSql); - switch (n) { - case DatabaseUtils.STATEMENT_BEGIN: - case DatabaseUtils.STATEMENT_COMMIT: - case DatabaseUtils.STATEMENT_ABORT: - mReadOnly = false; - mColumnNames = EMPTY_STRING_ARRAY; - mNumParameters = 0; - break; - default: - boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT); - SQLiteStatementInfo info = new SQLiteStatementInfo(); - db.getThreadSession().prepare(mSql, - db.getThreadDefaultConnectionFlags(assumeReadOnly), - cancellationSignalForPrepare, info); - mReadOnly = info.readOnly; - mColumnNames = info.columnNames; - mNumParameters = info.numParameters; - break; - } + int n = DatabaseUtils.getSqlStatementType(mSql); + switch (n) { + case DatabaseUtils.STATEMENT_BEGIN: + case DatabaseUtils.STATEMENT_COMMIT: + case DatabaseUtils.STATEMENT_ABORT: + mReadOnly = false; + mColumnNames = EMPTY_STRING_ARRAY; + mNumParameters = 0; + break; - if (bindArgs != null && bindArgs.length > mNumParameters) { - throw new IllegalArgumentException("Too many bind arguments. " - + bindArgs.length + " arguments were provided but the statement needs " - + mNumParameters + " arguments."); - } + default: + boolean assumeReadOnly = (n == DatabaseUtils.STATEMENT_SELECT); + SQLiteStatementInfo info = new SQLiteStatementInfo(); + db.getThreadSession().prepare(mSql, + db.getThreadDefaultConnectionFlags(assumeReadOnly), + cancellationSignalForPrepare, info); + mReadOnly = info.readOnly; + mColumnNames = info.columnNames; + mNumParameters = info.numParameters; + break; + } - if (mNumParameters != 0) { - mBindArgs = new Object[mNumParameters]; - if (bindArgs != null) { - System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length); - } - } else { - mBindArgs = null; - } - */} + if (bindArgs != null && bindArgs.length > mNumParameters) { + throw new IllegalArgumentException("Too many bind arguments. " + + bindArgs.length + " arguments were provided but the statement needs " + + mNumParameters + " arguments."); + } -final SQLiteDatabase getDatabase() { - return mDatabase; -} - -final String getSql() { - return mSql; -} - -final Object[] getBindArgs() { - return mBindArgs; -} - -final String[] getColumnNames() { - return mColumnNames; -} - -/** - * @hide - */ -protected final SQLiteSession getSession() { - return null; /*mDatabase.getThreadSession();*/ -} - -/** - * @hide - */ -protected final int getConnectionFlags() { - return -1; /*mDatabase.getThreadDefaultConnectionFlags(mReadOnly);*/ -} - -/** - * @hide - */ -protected final void onCorruption() { - // mDatabase.onCorruption(); -} - -/** - * Unimplemented. - * @deprecated This method is deprecated and must not be used. - */ -@Deprecated -public final int getUniqueId() { - return -1; -} - -/** - * Bind a NULL value to this statement. The value remains bound until - * {@link #clearBindings} is called. - * - * @param index The 1-based index to the parameter to bind null to - */ -public void bindNull(int index) { - bind(index, null); -} - -/** - * Bind a long value to this statement. The value remains bound until - * {@link #clearBindings} is called. - *addToBindArgs - * @param index The 1-based index to the parameter to bind - * @param value The value to bind - */ -public void bindLong(int index, long value) { - bind(index, value); -} - -/** - * Bind a double value to this statement. The value remains bound until - * {@link #clearBindings} is called. - * - * @param index The 1-based index to the parameter to bind - * @param value The value to bind - */ -public void bindDouble(int index, double value) { - bind(index, value); -} - -/** - * Bind a String value to this statement. The value remains bound until - * {@link #clearBindings} is called. - * - * @param index The 1-based index to the parameter to bind - * @param value The value to bind, must not be null - */ -public void bindString(int index, String value) { - if (value == null) { - throw new IllegalArgumentException("the bind value at index " + index + " is null"); - } - bind(index, value); -} - -/** - * Bind a byte array value to this statement. The value remains bound until - * {@link #clearBindings} is called. - * - * @param index The 1-based index to the parameter to bind - * @param value The value to bind, must not be null - */ -public void bindBlob(int index, byte[] value) { - if (value == null) { - throw new IllegalArgumentException("the bind value at index " + index + " is null"); - } - bind(index, value); -} - -/** - * Clears all existing bindings. Unset bindings are treated as NULL. - */ -public void clearBindings() { - if (mBindArgs != null) { - Arrays.fill(mBindArgs, null); - } -} - -/** - * Given an array of String bindArgs, this method binds all of them in one single call. - * - * @param bindArgs the String array of bind args, none of which must be null. - */ -public void bindAllArgsAsStrings(String[] bindArgs) { - if (bindArgs != null) { - for (int i = bindArgs.length; i != 0; i--) { - bindString(i, bindArgs[i - 1]); + if (mNumParameters != 0) { + mBindArgs = new Object[mNumParameters]; + if (bindArgs != null) { + System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length); + } + } else { + mBindArgs = null; } } -} -// @Override -protected void onAllReferencesReleased() { - clearBindings(); -} - -private void bind(int index, Object value) { - if (index < 1 || index > mNumParameters) { - throw new IllegalArgumentException("Cannot bind argument at index " + index + " because the index is out of range. " - + "The statement has " + mNumParameters + " parameters."); + final SQLiteDatabase getDatabase() { + return mDatabase; + } + + final String getSql() { + return mSql; + } + + final Object[] getBindArgs() { + return mBindArgs; + } + + final String[] getColumnNames() { + return mColumnNames; + } + + /** @hide */ + protected final SQLiteSession getSession() { + return mDatabase.getThreadSession(); + } + + /** @hide */ + protected final int getConnectionFlags() { + return mDatabase.getThreadDefaultConnectionFlags(mReadOnly); + } + + /** @hide */ + protected final void onCorruption() { + mDatabase.onCorruption(); + } + + /** + * Unimplemented. + * @deprecated This method is deprecated and must not be used. + */ + @Deprecated + public final int getUniqueId() { + return -1; + } + + /** + * Bind a NULL value to this statement. The value remains bound until + * {@link #clearBindings} is called. + * + * @param index The 1-based index to the parameter to bind null to + */ + public void bindNull(int index) { + bind(index, null); + } + + /** + * Bind a long value to this statement. The value remains bound until + * {@link #clearBindings} is called. + *addToBindArgs + * @param index The 1-based index to the parameter to bind + * @param value The value to bind + */ + public void bindLong(int index, long value) { + bind(index, value); + } + + /** + * Bind a double value to this statement. The value remains bound until + * {@link #clearBindings} is called. + * + * @param index The 1-based index to the parameter to bind + * @param value The value to bind + */ + public void bindDouble(int index, double value) { + bind(index, value); + } + + /** + * Bind a String value to this statement. The value remains bound until + * {@link #clearBindings} is called. + * + * @param index The 1-based index to the parameter to bind + * @param value The value to bind, must not be null + */ + public void bindString(int index, String value) { + if (value == null) { + throw new IllegalArgumentException("the bind value at index " + index + " is null"); + } + bind(index, value); + } + + /** + * Bind a byte array value to this statement. The value remains bound until + * {@link #clearBindings} is called. + * + * @param index The 1-based index to the parameter to bind + * @param value The value to bind, must not be null + */ + public void bindBlob(int index, byte[] value) { + if (value == null) { + throw new IllegalArgumentException("the bind value at index " + index + " is null"); + } + bind(index, value); + } + + /** + * Clears all existing bindings. Unset bindings are treated as NULL. + */ + public void clearBindings() { + if (mBindArgs != null) { + Arrays.fill(mBindArgs, null); + } + } + + /** + * Given an array of String bindArgs, this method binds all of them in one single call. + * + * @param bindArgs the String array of bind args, none of which must be null. + */ + public void bindAllArgsAsStrings(String[] bindArgs) { + if (bindArgs != null) { + for (int i = bindArgs.length; i != 0; i--) { + bindString(i, bindArgs[i - 1]); + } + } + } + + @Override + protected void onAllReferencesReleased() { + clearBindings(); + } + + private void bind(int index, Object value) { + if (index < 1 || index > mNumParameters) { + throw new IllegalArgumentException("Cannot bind argument at index " + + index + " because the index is out of range. " + + "The statement has " + mNumParameters + " parameters."); + } + mBindArgs[index - 1] = value; } - mBindArgs[index - 1] = value; -} } diff --git a/src/api-impl/android/database/sqlite/SQLiteQuery.java b/src/api-impl/android/database/sqlite/SQLiteQuery.java index 626a3f7e..7fa56bc7 100644 --- a/src/api-impl/android/database/sqlite/SQLiteQuery.java +++ b/src/api-impl/android/database/sqlite/SQLiteQuery.java @@ -13,16 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ package android.database.sqlite; -// import android.database.CursorWindow; +import android.database.CursorWindow; import android.os.CancellationSignal; -// import android.os.OperationCanceledException; +import android.os.OperationCanceledException; import android.util.Log; -class CursorWindow {} - /** * Represents a query that reads the resulting rows into a {@link SQLiteQuery}. * This class is used by {@link SQLiteCursor} and isn't useful itself. @@ -56,33 +58,31 @@ public final class SQLiteQuery extends SQLiteProgram { * @throws SQLiteException if an error occurs. * @throws OperationCanceledException if the operation was canceled. */ - int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { /* - acquireReference(); - try { - window.acquireReference(); - try { - int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(), - window, startPos, requiredPos, countAllRows, getConnectionFlags(), - mCancellationSignal); - return numRows; - } catch (SQLiteDatabaseCorruptException ex) { - onCorruption(); - throw ex; - } catch (SQLiteException ex) { - Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); - throw ex; - } finally { - window.releaseReference(); - } - } finally { - releaseReference(); - } - */ - return -1; + int fillWindow(CursorWindow window, int startPos, int requiredPos, boolean countAllRows) { + acquireReference(); + try { + window.acquireReference(); + try { + int numRows = getSession().executeForCursorWindow(getSql(), getBindArgs(), + window, startPos, requiredPos, countAllRows, getConnectionFlags(), + mCancellationSignal); + return numRows; + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } catch (SQLiteException ex) { + Log.e(TAG, "exception: " + ex.getMessage() + "; query: " + getSql()); + throw ex; + } finally { + window.releaseReference(); + } + } finally { + releaseReference(); + } } @Override public String toString() { - return "SQLiteQuery: " /* + getSql()*/; + return "SQLiteQuery: " + getSql(); } } diff --git a/src/api-impl/android/database/sqlite/SQLiteQueryBuilder.java b/src/api-impl/android/database/sqlite/SQLiteQueryBuilder.java new file mode 100644 index 00000000..2f6e2e95 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteQueryBuilder.java @@ -0,0 +1,651 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * This is a convience class that helps build SQL queries to be sent to + * {@link SQLiteDatabase} objects. + */ +public class SQLiteQueryBuilder +{ + private static final String TAG = "SQLiteQueryBuilder"; + private static final Pattern sLimitPattern = + Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?"); + + private Map mProjectionMap = null; + private String mTables = ""; + private StringBuilder mWhereClause = null; // lazily created + private boolean mDistinct; + private SQLiteDatabase.CursorFactory mFactory; + private boolean mStrict; + + public SQLiteQueryBuilder() { + mDistinct = false; + mFactory = null; + } + + /** + * Mark the query as DISTINCT. + * + * @param distinct if true the query is DISTINCT, otherwise it isn't + */ + public void setDistinct(boolean distinct) { + mDistinct = distinct; + } + + /** + * Returns the list of tables being queried + * + * @return the list of tables being queried + */ + public String getTables() { + return mTables; + } + + /** + * Sets the list of tables to query. Multiple tables can be specified to perform a join. + * For example: + * setTables("foo, bar") + * setTables("foo LEFT OUTER JOIN bar ON (foo.id = bar.foo_id)") + * + * @param inTables the list of tables to query on + */ + public void setTables(String inTables) { + mTables = inTables; + } + + /** + * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded + * by parenthesis and ANDed with the selection passed to {@link #query}. The final + * WHERE clause looks like: + * + * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * + * @param inWhere the chunk of text to append to the WHERE clause. + */ + public void appendWhere(CharSequence inWhere) { + if (mWhereClause == null) { + mWhereClause = new StringBuilder(inWhere.length() + 16); + } + if (mWhereClause.length() == 0) { + mWhereClause.append('('); + } + mWhereClause.append(inWhere); + } + + /** + * Append a chunk to the WHERE clause of the query. All chunks appended are surrounded + * by parenthesis and ANDed with the selection passed to {@link #query}. The final + * WHERE clause looks like: + * + * WHERE (<append chunk 1><append chunk2>) AND (<query() selection parameter>) + * + * @param inWhere the chunk of text to append to the WHERE clause. it will be escaped + * to avoid SQL injection attacks + */ + public void appendWhereEscapeString(String inWhere) { + if (mWhereClause == null) { + mWhereClause = new StringBuilder(inWhere.length() + 16); + } + if (mWhereClause.length() == 0) { + mWhereClause.append('('); + } + DatabaseUtils.appendEscapedSQLString(mWhereClause, inWhere); + } + + /** + * Sets the projection map for the query. The projection map maps + * from column names that the caller passes into query to database + * column names. This is useful for renaming columns as well as + * disambiguating column names when doing joins. For example you + * could map "name" to "people.name". If a projection map is set + * it must contain all column names the user may request, even if + * the key and value are the same. + * + * @param columnMap maps from the user column names to the database column names + */ + public void setProjectionMap(Map columnMap) { + mProjectionMap = columnMap; + } + + /** + * Sets the cursor factory to be used for the query. You can use + * one factory for all queries on a database but it is normally + * easier to specify the factory when doing this query. + * + * @param factory the factory to use. + */ + public void setCursorFactory(SQLiteDatabase.CursorFactory factory) { + mFactory = factory; + } + + /** + * When set, the selection is verified against malicious arguments. + * When using this class to create a statement using + * {@link #buildQueryString(boolean, String, String[], String, String, String, String, String)}, + * non-numeric limits will raise an exception. If a projection map is specified, fields + * not in that map will be ignored. + * If this class is used to execute the statement directly using + * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String)} + * or + * {@link #query(SQLiteDatabase, String[], String, String[], String, String, String, String)}, + * additionally also parenthesis escaping selection are caught. + * + * To summarize: To get maximum protection against malicious third party apps (for example + * content provider consumers), make sure to do the following: + *
    + *
  • Set this value to true
  • + *
  • Use a projection map
  • + *
  • Use one of the query overloads instead of getting the statement as a sql string
  • + *
+ * By default, this value is false. + */ + public void setStrict(boolean flag) { + mStrict = flag; + } + + /** + * Build an SQL query string from the given clauses. + * + * @param distinct true if you want each row to be unique, false otherwise. + * @param tables The table names to compile the query against. + * @param columns A list of which columns to return. Passing null will + * return all columns, which is discouraged to prevent reading + * data from storage that isn't going to be used. + * @param where A filter declaring which rows to return, formatted as an SQL + * WHERE clause (excluding the WHERE itself). Passing null will + * return all rows for the given URL. + * @param groupBy A filter declaring how to group rows, formatted as an SQL + * GROUP BY clause (excluding the GROUP BY itself). Passing null + * will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in the cursor, + * if row grouping is being used, formatted as an SQL HAVING + * clause (excluding the HAVING itself). Passing null will cause + * all row groups to be included, and is required when row + * grouping is not being used. + * @param orderBy How to order the rows, formatted as an SQL ORDER BY clause + * (excluding the ORDER BY itself). Passing null will use the + * default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @return the SQL query string + */ + public static String buildQueryString( + boolean distinct, String tables, String[] columns, String where, + String groupBy, String having, String orderBy, String limit) { + if (TextUtils.isEmpty(groupBy) && !TextUtils.isEmpty(having)) { + throw new IllegalArgumentException( + "HAVING clauses are only permitted when using a groupBy clause"); + } + if (!TextUtils.isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) { + throw new IllegalArgumentException("invalid LIMIT clauses:" + limit); + } + + StringBuilder query = new StringBuilder(120); + + query.append("SELECT "); + if (distinct) { + query.append("DISTINCT "); + } + if (columns != null && columns.length != 0) { + appendColumns(query, columns); + } else { + query.append("* "); + } + query.append("FROM "); + query.append(tables); + appendClause(query, " WHERE ", where); + appendClause(query, " GROUP BY ", groupBy); + appendClause(query, " HAVING ", having); + appendClause(query, " ORDER BY ", orderBy); + appendClause(query, " LIMIT ", limit); + + return query.toString(); + } + + private static void appendClause(StringBuilder s, String name, String clause) { + if (!TextUtils.isEmpty(clause)) { + s.append(name); + s.append(clause); + } + } + + /** + * Add the names that are non-null in columns to s, separating + * them with commas. + */ + public static void appendColumns(StringBuilder s, String[] columns) { + int n = columns.length; + + for (int i = 0; i < n; i++) { + String column = columns[i]; + + if (column != null) { + if (i > 0) { + s.append(", "); + } + s.append(column); + } + } + s.append(' '); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY + * itself). Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @return a cursor over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public Cursor query(SQLiteDatabase db, String[] projectionIn, + String selection, String[] selectionArgs, String groupBy, + String having, String sortOrder) { + return query(db, projectionIn, selection, selectionArgs, groupBy, having, sortOrder, + null /* limit */, null /* cancellationSignal */); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY + * itself). Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @return a cursor over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public Cursor query(SQLiteDatabase db, String[] projectionIn, + String selection, String[] selectionArgs, String groupBy, + String having, String sortOrder, String limit) { + return query(db, projectionIn, selection, selectionArgs, + groupBy, having, sortOrder, limit, null); + } + + /** + * Perform a query by combining all current settings and the + * information passed into this method. + * + * @param db the database to query on + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to prevent + * reading data from storage that isn't going to be used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given URL. + * @param selectionArgs You may include ?s in selection, which + * will be replaced by the values from selectionArgs, in order + * that they appear in the selection. The values will be bound + * as Strings. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY + * itself). Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * If the operation is canceled, then {@link OperationCanceledException} will be thrown + * when the query is executed. + * @return a cursor over the result set + * @see android.content.ContentResolver#query(android.net.Uri, String[], + * String, String[], String) + */ + public Cursor query(SQLiteDatabase db, String[] projectionIn, + String selection, String[] selectionArgs, String groupBy, + String having, String sortOrder, String limit, CancellationSignal cancellationSignal) { + if (mTables == null) { + return null; + } + + if (mStrict && selection != null && selection.length() > 0) { + // Validate the user-supplied selection to detect syntactic anomalies + // in the selection string that could indicate a SQL injection attempt. + // The idea is to ensure that the selection clause is a valid SQL expression + // by compiling it twice: once wrapped in parentheses and once as + // originally specified. An attacker cannot create an expression that + // would escape the SQL expression while maintaining balanced parentheses + // in both the wrapped and original forms. + String sqlForValidation = buildQuery(projectionIn, "(" + selection + ")", groupBy, + having, sortOrder, limit); + db.validateSql(sqlForValidation, cancellationSignal); // will throw if query is invalid + } + + String sql = buildQuery( + projectionIn, selection, groupBy, having, + sortOrder, limit); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Performing query: " + sql); + } + return db.rawQueryWithFactory( + mFactory, sql, selectionArgs, + SQLiteDatabase.findEditTable(mTables), + cancellationSignal); // will throw if query is invalid + } + + /** + * Construct a SELECT statement suitable for use in a group of + * SELECT statements that will be joined through UNION operators + * in buildUnionQuery. + * + * @param projectionIn A list of which columns to return. Passing + * null will return all columns, which is discouraged to + * prevent reading data from storage that isn't going to be + * used. + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given + * URL. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY itself). + * Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing null + * will use the default sort order, which may be unordered. + * @param limit Limits the number of rows returned by the query, + * formatted as LIMIT clause. Passing null denotes no LIMIT clause. + * @return the resulting SQL SELECT statement + */ + public String buildQuery( + String[] projectionIn, String selection, String groupBy, + String having, String sortOrder, String limit) { + String[] projection = computeProjection(projectionIn); + + StringBuilder where = new StringBuilder(); + boolean hasBaseWhereClause = mWhereClause != null && mWhereClause.length() > 0; + + if (hasBaseWhereClause) { + where.append(mWhereClause.toString()); + where.append(')'); + } + + // Tack on the user's selection, if present. + if (selection != null && selection.length() > 0) { + if (hasBaseWhereClause) { + where.append(" AND "); + } + + where.append('('); + where.append(selection); + where.append(')'); + } + + return buildQueryString( + mDistinct, mTables, projection, where.toString(), + groupBy, having, sortOrder, limit); + } + + /** + * @deprecated This method's signature is misleading since no SQL parameter + * substitution is carried out. The selection arguments parameter does not get + * used at all. To avoid confusion, call + * {@link #buildQuery(String[], String, String, String, String, String)} instead. + */ + @Deprecated + public String buildQuery( + String[] projectionIn, String selection, String[] selectionArgs, + String groupBy, String having, String sortOrder, String limit) { + return buildQuery(projectionIn, selection, groupBy, having, sortOrder, limit); + } + + /** + * Construct a SELECT statement suitable for use in a group of + * SELECT statements that will be joined through UNION operators + * in buildUnionQuery. + * + * @param typeDiscriminatorColumn the name of the result column + * whose cells will contain the name of the table from which + * each row was drawn. + * @param unionColumns the names of the columns to appear in the + * result. This may include columns that do not appear in the + * table this SELECT is querying (i.e. mTables), but that do + * appear in one of the other tables in the UNION query that we + * are constructing. + * @param columnsPresentInTable a Set of the names of the columns + * that appear in this table (i.e. in the table whose name is + * mTables). Since columns in unionColumns include columns that + * appear only in other tables, we use this array to distinguish + * which ones actually are present. Other columns will have + * NULL values for results from this subquery. + * @param computedColumnsOffset all columns in unionColumns before + * this index are included under the assumption that they're + * computed and therefore won't appear in columnsPresentInTable, + * e.g. "date * 1000 as normalized_date" + * @param typeDiscriminatorValue the value used for the + * type-discriminator column in this subquery + * @param selection A filter declaring which rows to return, + * formatted as an SQL WHERE clause (excluding the WHERE + * itself). Passing null will return all rows for the given + * URL. + * @param groupBy A filter declaring how to group rows, formatted + * as an SQL GROUP BY clause (excluding the GROUP BY itself). + * Passing null will cause the rows to not be grouped. + * @param having A filter declare which row groups to include in + * the cursor, if row grouping is being used, formatted as an + * SQL HAVING clause (excluding the HAVING itself). Passing + * null will cause all row groups to be included, and is + * required when row grouping is not being used. + * @return the resulting SQL SELECT statement + */ + public String buildUnionSubQuery( + String typeDiscriminatorColumn, + String[] unionColumns, + Set columnsPresentInTable, + int computedColumnsOffset, + String typeDiscriminatorValue, + String selection, + String groupBy, + String having) { + int unionColumnsCount = unionColumns.length; + String[] projectionIn = new String[unionColumnsCount]; + + for (int i = 0; i < unionColumnsCount; i++) { + String unionColumn = unionColumns[i]; + + if (unionColumn.equals(typeDiscriminatorColumn)) { + projectionIn[i] = "'" + typeDiscriminatorValue + "' AS " + + typeDiscriminatorColumn; + } else if (i <= computedColumnsOffset + || columnsPresentInTable.contains(unionColumn)) { + projectionIn[i] = unionColumn; + } else { + projectionIn[i] = "NULL AS " + unionColumn; + } + } + return buildQuery( + projectionIn, selection, groupBy, having, + null /* sortOrder */, + null /* limit */); + } + + /** + * @deprecated This method's signature is misleading since no SQL parameter + * substitution is carried out. The selection arguments parameter does not get + * used at all. To avoid confusion, call + * {@link #buildUnionSubQuery} + * instead. + */ + @Deprecated + public String buildUnionSubQuery( + String typeDiscriminatorColumn, + String[] unionColumns, + Set columnsPresentInTable, + int computedColumnsOffset, + String typeDiscriminatorValue, + String selection, + String[] selectionArgs, + String groupBy, + String having) { + return buildUnionSubQuery( + typeDiscriminatorColumn, unionColumns, columnsPresentInTable, + computedColumnsOffset, typeDiscriminatorValue, selection, + groupBy, having); + } + + /** + * Given a set of subqueries, all of which are SELECT statements, + * construct a query that returns the union of what those + * subqueries return. + * @param subQueries an array of SQL SELECT statements, all of + * which must have the same columns as the same positions in + * their results + * @param sortOrder How to order the rows, formatted as an SQL + * ORDER BY clause (excluding the ORDER BY itself). Passing + * null will use the default sort order, which may be unordered. + * @param limit The limit clause, which applies to the entire union result set + * + * @return the resulting SQL SELECT statement + */ + public String buildUnionQuery(String[] subQueries, String sortOrder, String limit) { + StringBuilder query = new StringBuilder(128); + int subQueryCount = subQueries.length; + String unionOperator = mDistinct ? " UNION " : " UNION ALL "; + + for (int i = 0; i < subQueryCount; i++) { + if (i > 0) { + query.append(unionOperator); + } + query.append(subQueries[i]); + } + appendClause(query, " ORDER BY ", sortOrder); + appendClause(query, " LIMIT ", limit); + return query.toString(); + } + + private String[] computeProjection(String[] projectionIn) { + if (projectionIn != null && projectionIn.length > 0) { + if (mProjectionMap != null) { + String[] projection = new String[projectionIn.length]; + int length = projectionIn.length; + + for (int i = 0; i < length; i++) { + String userColumn = projectionIn[i]; + String column = mProjectionMap.get(userColumn); + + if (column != null) { + projection[i] = column; + continue; + } + + if (!mStrict && + ( userColumn.contains(" AS ") || userColumn.contains(" as "))) { + /* A column alias already exist */ + projection[i] = userColumn; + continue; + } + + throw new IllegalArgumentException("Invalid column " + + projectionIn[i]); + } + return projection; + } else { + return projectionIn; + } + } else if (mProjectionMap != null) { + // Return all columns in projection map. + Set> entrySet = mProjectionMap.entrySet(); + String[] projection = new String[entrySet.size()]; + Iterator> entryIter = entrySet.iterator(); + int i = 0; + + while (entryIter.hasNext()) { + Entry entry = entryIter.next(); + + // Don't include the _count column when people ask for no projection. + if (entry.getKey().equals(/*BaseColumns._COUNT*/"_count")) { + continue; + } + projection[i++] = entry.getValue(); + } + return projection; + } + return null; + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteReadOnlyDatabaseException.java b/src/api-impl/android/database/sqlite/SQLiteReadOnlyDatabaseException.java new file mode 100644 index 00000000..de87023d --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteReadOnlyDatabaseException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteReadOnlyDatabaseException extends SQLiteException { + public SQLiteReadOnlyDatabaseException() {} + + public SQLiteReadOnlyDatabaseException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteSession.java b/src/api-impl/android/database/sqlite/SQLiteSession.java new file mode 100644 index 00000000..60a4eb14 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteSession.java @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import android.database.CursorWindow; +import android.database.DatabaseUtils; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.os.ParcelFileDescriptor; + +/** + * Provides a single client the ability to use a database. + * + *

About database sessions

+ *

+ * Database access is always performed using a session. The session + * manages the lifecycle of transactions and database connections. + *

+ * Sessions can be used to perform both read-only and read-write operations. + * There is some advantage to knowing when a session is being used for + * read-only purposes because the connection pool can optimize the use + * of the available connections to permit multiple read-only operations + * to execute in parallel whereas read-write operations may need to be serialized. + *

+ * When Write Ahead Logging (WAL) is enabled, the database can + * execute simultaneous read-only and read-write transactions, provided that + * at most one read-write transaction is performed at a time. When WAL is not + * enabled, read-only transactions can execute in parallel but read-write + * transactions are mutually exclusive. + *

+ * + *

Ownership and concurrency guarantees

+ *

+ * Session objects are not thread-safe. In fact, session objects are thread-bound. + * The {@link SQLiteDatabase} uses a thread-local variable to associate a session + * with each thread for the use of that thread alone. Consequently, each thread + * has its own session object and therefore its own transaction state independent + * of other threads. + *

+ * A thread has at most one session per database. This constraint ensures that + * a thread can never use more than one database connection at a time for a + * given database. As the number of available database connections is limited, + * if a single thread tried to acquire multiple connections for the same database + * at the same time, it might deadlock. Therefore we allow there to be only + * one session (so, at most one connection) per thread per database. + *

+ * + *

Transactions

+ *

+ * There are two kinds of transaction: implicit transactions and explicit + * transactions. + *

+ * An implicit transaction is created whenever a database operation is requested + * and there is no explicit transaction currently in progress. An implicit transaction + * only lasts for the duration of the database operation in question and then it + * is ended. If the database operation was successful, then its changes are committed. + *

+ * An explicit transaction is started by calling {@link #beginTransaction} and + * specifying the desired transaction mode. Once an explicit transaction has begun, + * all subsequent database operations will be performed as part of that transaction. + * To end an explicit transaction, first call {@link #setTransactionSuccessful} if the + * transaction was successful, then call {@link #end}. If the transaction was + * marked successful, its changes will be committed, otherwise they will be rolled back. + *

+ * Explicit transactions can also be nested. A nested explicit transaction is + * started with {@link #beginTransaction}, marked successful with + * {@link #setTransactionSuccessful}and ended with {@link #endTransaction}. + * If any nested transaction is not marked successful, then the entire transaction + * including all of its nested transactions will be rolled back + * when the outermost transaction is ended. + *

+ * To improve concurrency, an explicit transaction can be yielded by calling + * {@link #yieldTransaction}. If there is contention for use of the database, + * then yielding ends the current transaction, commits its changes, releases the + * database connection for use by another session for a little while, and starts a + * new transaction with the same properties as the original one. + * Changes committed by {@link #yieldTransaction} cannot be rolled back. + *

+ * When a transaction is started, the client can provide a {@link SQLiteTransactionListener} + * to listen for notifications of transaction-related events. + *

+ * Recommended usage: + *

+ * // First, begin the transaction.
+ * session.beginTransaction(SQLiteSession.TRANSACTION_MODE_DEFERRED, 0);
+ * try {
+ *     // Then do stuff...
+ *     session.execute("INSERT INTO ...", null, 0);
+ *
+ *     // As the very last step before ending the transaction, mark it successful.
+ *     session.setTransactionSuccessful();
+ * } finally {
+ *     // Finally, end the transaction.
+ *     // This statement will commit the transaction if it was marked successful or
+ *     // roll it back otherwise.
+ *     session.endTransaction();
+ * }
+ * 
+ *

+ * + *

Database connections

+ *

+ * A {@link SQLiteDatabase} can have multiple active sessions at the same + * time. Each session acquires and releases connections to the database + * as needed to perform each requested database transaction. If all connections + * are in use, then database transactions on some sessions will block until a + * connection becomes available. + *

+ * The session acquires a single database connection only for the duration + * of a single (implicit or explicit) database transaction, then releases it. + * This characteristic allows a small pool of database connections to be shared + * efficiently by multiple sessions as long as they are not all trying to perform + * database transactions at the same time. + *

+ * + *

Responsiveness

+ *

+ * Because there are a limited number of database connections and the session holds + * a database connection for the entire duration of a database transaction, + * it is important to keep transactions short. This is especially important + * for read-write transactions since they may block other transactions + * from executing. Consider calling {@link #yieldTransaction} periodically + * during long-running transactions. + *

+ * Another important consideration is that transactions that take too long to + * run may cause the application UI to become unresponsive. Even if the transaction + * is executed in a background thread, the user will get bored and + * frustrated if the application shows no data for several seconds while + * a transaction runs. + *

+ * Guidelines: + *

    + *
  • Do not perform database transactions on the UI thread.
  • + *
  • Keep database transactions as short as possible.
  • + *
  • Simple queries often run faster than complex queries.
  • + *
  • Measure the performance of your database transactions.
  • + *
  • Consider what will happen when the size of the data set grows. + * A query that works well on 100 rows may struggle with 10,000.
  • + *
+ * + *

Reentrance

+ *

+ * This class must tolerate reentrant execution of SQLite operations because + * triggers may call custom SQLite functions that perform additional queries. + *

+ * + * @hide + */ +public final class SQLiteSession { + private final SQLiteConnectionPool mConnectionPool; + + private SQLiteConnection mConnection; + private int mConnectionFlags; + private int mConnectionUseCount; + private Transaction mTransactionPool; + private Transaction mTransactionStack; + + /** + * Transaction mode: Deferred. + *

+ * In a deferred transaction, no locks are acquired on the database + * until the first operation is performed. If the first operation is + * read-only, then a SHARED lock is acquired, otherwise + * a RESERVED lock is acquired. + *

+ * While holding a SHARED lock, this session is only allowed to + * read but other sessions are allowed to read or write. + * While holding a RESERVED lock, this session is allowed to read + * or write but other sessions are only allowed to read. + *

+ * Because the lock is only acquired when needed in a deferred transaction, + * it is possible for another session to write to the database first before + * this session has a chance to do anything. + *

+ * Corresponds to the SQLite BEGIN DEFERRED transaction mode. + *

+ */ + public static final int TRANSACTION_MODE_DEFERRED = 0; + + /** + * Transaction mode: Immediate. + *

+ * When an immediate transaction begins, the session acquires a + * RESERVED lock. + *

+ * While holding a RESERVED lock, this session is allowed to read + * or write but other sessions are only allowed to read. + *

+ * Corresponds to the SQLite BEGIN IMMEDIATE transaction mode. + *

+ */ + public static final int TRANSACTION_MODE_IMMEDIATE = 1; + + /** + * Transaction mode: Exclusive. + *

+ * When an exclusive transaction begins, the session acquires an + * EXCLUSIVE lock. + *

+ * While holding an EXCLUSIVE lock, this session is allowed to read + * or write but no other sessions are allowed to access the database. + *

+ * Corresponds to the SQLite BEGIN EXCLUSIVE transaction mode. + *

+ */ + public static final int TRANSACTION_MODE_EXCLUSIVE = 2; + + /** + * Creates a session bound to the specified connection pool. + * + * @param connectionPool The connection pool. + */ + public SQLiteSession(SQLiteConnectionPool connectionPool) { + if (connectionPool == null) { + throw new IllegalArgumentException("connectionPool must not be null"); + } + + mConnectionPool = connectionPool; + } + + /** + * Returns true if the session has a transaction in progress. + * + * @return True if the session has a transaction in progress. + */ + public boolean hasTransaction() { + return mTransactionStack != null; + } + + /** + * Returns true if the session has a nested transaction in progress. + * + * @return True if the session has a nested transaction in progress. + */ + public boolean hasNestedTransaction() { + return mTransactionStack != null && mTransactionStack.mParent != null; + } + + /** + * Returns true if the session has an active database connection. + * + * @return True if the session has an active database connection. + */ + public boolean hasConnection() { + return mConnection != null; + } + + /** + * Begins a transaction. + *

+ * Transactions may nest. If the transaction is not in progress, + * then a database connection is obtained and a new transaction is started. + * Otherwise, a nested transaction is started. + *

+ * Each call to {@link #beginTransaction} must be matched exactly by a call + * to {@link #endTransaction}. To mark a transaction as successful, + * call {@link #setTransactionSuccessful} before calling {@link #endTransaction}. + * If the transaction is not successful, or if any of its nested + * transactions were not successful, then the entire transaction will + * be rolled back when the outermost transaction is ended. + *

+ * + * @param transactionMode The transaction mode. One of: {@link #TRANSACTION_MODE_DEFERRED}, + * {@link #TRANSACTION_MODE_IMMEDIATE}, or {@link #TRANSACTION_MODE_EXCLUSIVE}. + * Ignored when creating a nested transaction. + * @param transactionListener The transaction listener, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws IllegalStateException if {@link #setTransactionSuccessful} has already been + * called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #setTransactionSuccessful + * @see #yieldTransaction + * @see #endTransaction + */ + public void beginTransaction(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags, + CancellationSignal cancellationSignal) { + throwIfTransactionMarkedSuccessful(); + beginTransactionUnchecked(transactionMode, transactionListener, connectionFlags, + cancellationSignal); + } + + private void beginTransactionUnchecked(int transactionMode, + SQLiteTransactionListener transactionListener, int connectionFlags, + CancellationSignal cancellationSignal) { + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + if (mTransactionStack == null) { + acquireConnection(null, connectionFlags, cancellationSignal); // might throw + } + try { + // Set up the transaction such that we can back out safely + // in case we fail part way. + if (mTransactionStack == null) { + // Execute SQL might throw a runtime exception. + switch (transactionMode) { + case TRANSACTION_MODE_IMMEDIATE: + mConnection.execute("BEGIN IMMEDIATE;", null, + cancellationSignal); // might throw + break; + case TRANSACTION_MODE_EXCLUSIVE: + mConnection.execute("BEGIN EXCLUSIVE;", null, + cancellationSignal); // might throw + break; + default: + mConnection.execute("BEGIN;", null, cancellationSignal); // might throw + break; + } + } + + // Listener might throw a runtime exception. + if (transactionListener != null) { + try { + transactionListener.onBegin(); // might throw + } catch (RuntimeException ex) { + if (mTransactionStack == null) { + mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw + } + throw ex; + } + } + + // Bookkeeping can't throw, except an OOM, which is just too bad... + Transaction transaction = obtainTransaction(transactionMode, transactionListener); + transaction.mParent = mTransactionStack; + mTransactionStack = transaction; + } finally { + if (mTransactionStack == null) { + releaseConnection(); // might throw + } + } + } + + /** + * Marks the current transaction as having completed successfully. + *

+ * This method can be called at most once between {@link #beginTransaction} and + * {@link #endTransaction} to indicate that the changes made by the transaction should be + * committed. If this method is not called, the changes will be rolled back + * when the transaction is ended. + *

+ * + * @throws IllegalStateException if there is no current transaction, or if + * {@link #setTransactionSuccessful} has already been called for the current transaction. + * + * @see #beginTransaction + * @see #endTransaction + */ + public void setTransactionSuccessful() { + throwIfNoTransaction(); + throwIfTransactionMarkedSuccessful(); + + mTransactionStack.mMarkedSuccessful = true; + } + + /** + * Ends the current transaction and commits or rolls back changes. + *

+ * If this is the outermost transaction (not nested within any other + * transaction), then the changes are committed if {@link #setTransactionSuccessful} + * was called or rolled back otherwise. + *

+ * This method must be called exactly once for each call to {@link #beginTransaction}. + *

+ * + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws IllegalStateException if there is no current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #beginTransaction + * @see #setTransactionSuccessful + * @see #yieldTransaction + */ + public void endTransaction(CancellationSignal cancellationSignal) { + throwIfNoTransaction(); + assert mConnection != null; + + endTransactionUnchecked(cancellationSignal, false); + } + + private void endTransactionUnchecked(CancellationSignal cancellationSignal, boolean yielding) { + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + final Transaction top = mTransactionStack; + boolean successful = (top.mMarkedSuccessful || yielding) && !top.mChildFailed; + + RuntimeException listenerException = null; + final SQLiteTransactionListener listener = top.mListener; + if (listener != null) { + try { + if (successful) { + listener.onCommit(); // might throw + } else { + listener.onRollback(); // might throw + } + } catch (RuntimeException ex) { + listenerException = ex; + successful = false; + } + } + + mTransactionStack = top.mParent; + recycleTransaction(top); + + if (mTransactionStack != null) { + if (!successful) { + mTransactionStack.mChildFailed = true; + } + } else { + try { + if (successful) { + mConnection.execute("COMMIT;", null, cancellationSignal); // might throw + } else { + mConnection.execute("ROLLBACK;", null, cancellationSignal); // might throw + } + } finally { + releaseConnection(); // might throw + } + } + + if (listenerException != null) { + throw listenerException; + } + } + + /** + * Temporarily ends a transaction to let other threads have use of + * the database. Begins a new transaction after a specified delay. + *

+ * If there are other threads waiting to acquire connections, + * then the current transaction is committed and the database + * connection is released. After a short delay, a new transaction + * is started. + *

+ * The transaction is assumed to be successful so far. Do not call + * {@link #setTransactionSuccessful()} before calling this method. + * This method will fail if the transaction has already been marked + * successful. + *

+ * The changes that were committed by a yield cannot be rolled back later. + *

+ * Before this method was called, there must already have been + * a transaction in progress. When this method returns, there will + * still be a transaction in progress, either the same one as before + * or a new one if the transaction was actually yielded. + *

+ * This method should not be called when there is a nested transaction + * in progress because it is not possible to yield a nested transaction. + * If throwIfNested is true, then attempting to yield + * a nested transaction will throw {@link IllegalStateException}, otherwise + * the method will return false in that case. + *

+ * If there is no nested transaction in progress but a previous nested + * transaction failed, then the transaction is not yielded (because it + * must be rolled back) and this method returns false. + *

+ * + * @param sleepAfterYieldDelayMillis A delay time to wait after yielding + * the database connection to allow other threads some time to run. + * If the value is less than or equal to zero, there will be no additional + * delay beyond the time it will take to begin a new transaction. + * @param throwIfUnsafe If true, then instead of returning false when no + * transaction is in progress, a nested transaction is in progress, or when + * the transaction has already been marked successful, throws {@link IllegalStateException}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return True if the transaction was actually yielded. + * + * @throws IllegalStateException if throwIfNested is true and + * there is no current transaction, there is a nested transaction in progress or + * if {@link #setTransactionSuccessful} has already been called for the current transaction. + * @throws SQLiteException if an error occurs. + * @throws OperationCanceledException if the operation was canceled. + * + * @see #beginTransaction + * @see #endTransaction + */ + public boolean yieldTransaction(long sleepAfterYieldDelayMillis, boolean throwIfUnsafe, + CancellationSignal cancellationSignal) { + if (throwIfUnsafe) { + throwIfNoTransaction(); + throwIfTransactionMarkedSuccessful(); + throwIfNestedTransaction(); + } else { + if (mTransactionStack == null || mTransactionStack.mMarkedSuccessful + || mTransactionStack.mParent != null) { + return false; + } + } + assert mConnection != null; + + if (mTransactionStack.mChildFailed) { + return false; + } + + return yieldTransactionUnchecked(sleepAfterYieldDelayMillis, + cancellationSignal); // might throw + } + + private boolean yieldTransactionUnchecked(long sleepAfterYieldDelayMillis, + CancellationSignal cancellationSignal) { + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + if (!mConnectionPool.shouldYieldConnection(mConnection, mConnectionFlags)) { + return false; + } + + final int transactionMode = mTransactionStack.mMode; + final SQLiteTransactionListener listener = mTransactionStack.mListener; + final int connectionFlags = mConnectionFlags; + endTransactionUnchecked(cancellationSignal, true); // might throw + + if (sleepAfterYieldDelayMillis > 0) { + try { + Thread.sleep(sleepAfterYieldDelayMillis); + } catch (InterruptedException ex) { + // we have been interrupted, that's all we need to do + } + } + + beginTransactionUnchecked(transactionMode, listener, connectionFlags, + cancellationSignal); // might throw + return true; + } + + /** + * Prepares a statement for execution but does not bind its parameters or execute it. + *

+ * This method can be used to check for syntax errors during compilation + * prior to execution of the statement. If the {@code outStatementInfo} argument + * is not null, the provided {@link SQLiteStatementInfo} object is populated + * with information about the statement. + *

+ * A prepared statement makes no reference to the arguments that may eventually + * be bound to it, consequently it it possible to cache certain prepared statements + * such as SELECT or INSERT/UPDATE statements. If the statement is cacheable, + * then it will be stored in the cache for later and reused if possible. + *

+ * + * @param sql The SQL statement to prepare. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @param outStatementInfo The {@link SQLiteStatementInfo} object to populate + * with information about the statement, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error. + * @throws OperationCanceledException if the operation was canceled. + */ + public void prepare(String sql, int connectionFlags, CancellationSignal cancellationSignal, + SQLiteStatementInfo outStatementInfo) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + mConnection.prepare(sql, outStatementInfo); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that does not return a result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public void execute(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + mConnection.execute(sql, bindArgs, cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single long result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a long, or zero if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLong(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForLong(sql, bindArgs, cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single {@link String} result. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The value of the first column in the first row of the result set + * as a String, or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public String executeForString(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return null; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForString(sql, bindArgs, cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a single BLOB result as a + * file descriptor to a shared memory region. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The file descriptor for a shared memory region that contains + * the value of the first column in the first row of the result set as a BLOB, + * or null if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bindArgs, + int connectionFlags, CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return null; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForBlobFileDescriptor(sql, bindArgs, + cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns a count of the number of rows + * that were changed. Use for UPDATE or DELETE SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were changed. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForChangedRowCount(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForChangedRowCount(sql, bindArgs, + cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement that returns the row id of the last row inserted + * by the statement. Use for INSERT SQL statements. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The row id of the last row that was inserted, or 0 if none. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public long executeForLastInsertedRowId(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + return 0; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForLastInsertedRowId(sql, bindArgs, + cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Executes a statement and populates the specified {@link CursorWindow} + * with a range of results. Returns the number of rows that were counted + * during query execution. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param window The cursor window to clear and fill. + * @param startPos The start position for filling the window. + * @param requiredPos The position of a row that MUST be in the window. + * If it won't fit, then the query should discard part of what it filled + * so that it does. Must be greater than or equal to startPos. + * @param countAllRows True to count all rows that the query would return + * regagless of whether they fit in the window. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return The number of rows that were counted during query execution. Might + * not be all rows in the result set unless countAllRows is true. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + public int executeForCursorWindow(String sql, Object[] bindArgs, + CursorWindow window, int startPos, int requiredPos, boolean countAllRows, + int connectionFlags, CancellationSignal cancellationSignal) { + if (sql == null) { + throw new IllegalArgumentException("sql must not be null."); + } + if (window == null) { + throw new IllegalArgumentException("window must not be null."); + } + + if (executeSpecial(sql, bindArgs, connectionFlags, cancellationSignal)) { + window.clear(); + return 0; + } + + acquireConnection(sql, connectionFlags, cancellationSignal); // might throw + try { + return mConnection.executeForCursorWindow(sql, bindArgs, + window, startPos, requiredPos, countAllRows, + cancellationSignal); // might throw + } finally { + releaseConnection(); // might throw + } + } + + /** + * Performs special reinterpretation of certain SQL statements such as "BEGIN", + * "COMMIT" and "ROLLBACK" to ensure that transaction state invariants are + * maintained. + * + * This function is mainly used to support legacy apps that perform their + * own transactions by executing raw SQL rather than calling {@link #beginTransaction} + * and the like. + * + * @param sql The SQL statement to execute. + * @param bindArgs The arguments to bind, or null if none. + * @param connectionFlags The connection flags to use if a connection must be + * acquired by this operation. Refer to {@link SQLiteConnectionPool}. + * @param cancellationSignal A signal to cancel the operation in progress, or null if none. + * @return True if the statement was of a special form that was handled here, + * false otherwise. + * + * @throws SQLiteException if an error occurs, such as a syntax error + * or invalid number of bind arguments. + * @throws OperationCanceledException if the operation was canceled. + */ + private boolean executeSpecial(String sql, Object[] bindArgs, int connectionFlags, + CancellationSignal cancellationSignal) { + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + final int type = DatabaseUtils.getSqlStatementType(sql); + switch (type) { + case DatabaseUtils.STATEMENT_BEGIN: + beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags, + cancellationSignal); + return true; + + case DatabaseUtils.STATEMENT_COMMIT: + setTransactionSuccessful(); + endTransaction(cancellationSignal); + return true; + + case DatabaseUtils.STATEMENT_ABORT: + endTransaction(cancellationSignal); + return true; + } + return false; + } + + private void acquireConnection(String sql, int connectionFlags, + CancellationSignal cancellationSignal) { + if (mConnection == null) { + assert mConnectionUseCount == 0; + mConnection = mConnectionPool.acquireConnection(sql, connectionFlags, + cancellationSignal); // might throw + mConnectionFlags = connectionFlags; + } + mConnectionUseCount += 1; + } + + private void releaseConnection() { + assert mConnection != null; + assert mConnectionUseCount > 0; + if (--mConnectionUseCount == 0) { + try { + mConnectionPool.releaseConnection(mConnection); // might throw + } finally { + mConnection = null; + } + } + } + + private void throwIfNoTransaction() { + if (mTransactionStack == null) { + throw new IllegalStateException("Cannot perform this operation because " + + "there is no current transaction."); + } + } + + private void throwIfTransactionMarkedSuccessful() { + if (mTransactionStack != null && mTransactionStack.mMarkedSuccessful) { + throw new IllegalStateException("Cannot perform this operation because " + + "the transaction has already been marked successful. The only " + + "thing you can do now is call endTransaction()."); + } + } + + private void throwIfNestedTransaction() { + if (hasNestedTransaction()) { + throw new IllegalStateException("Cannot perform this operation because " + + "a nested transaction is in progress."); + } + } + + private Transaction obtainTransaction(int mode, SQLiteTransactionListener listener) { + Transaction transaction = mTransactionPool; + if (transaction != null) { + mTransactionPool = transaction.mParent; + transaction.mParent = null; + transaction.mMarkedSuccessful = false; + transaction.mChildFailed = false; + } else { + transaction = new Transaction(); + } + transaction.mMode = mode; + transaction.mListener = listener; + return transaction; + } + + private void recycleTransaction(Transaction transaction) { + transaction.mParent = mTransactionPool; + transaction.mListener = null; + mTransactionPool = transaction; + } + + private static final class Transaction { + public Transaction mParent; + public int mMode; + public SQLiteTransactionListener mListener; + public boolean mMarkedSuccessful; + public boolean mChildFailed; + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteStatement.java b/src/api-impl/android/database/sqlite/SQLiteStatement.java new file mode 100644 index 00000000..73c99be2 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteStatement.java @@ -0,0 +1,167 @@ +/* + * 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import android.os.ParcelFileDescriptor; + +/** + * Represents a statement that can be executed against a database. The statement + * cannot return multiple rows or columns, but single value (1 x 1) result sets + * are supported. + *

+ * This class is not thread-safe. + *

+ */ +public final class SQLiteStatement extends SQLiteProgram { + SQLiteStatement(SQLiteDatabase db, String sql, Object[] bindArgs) { + super(db, sql, bindArgs, null); + } + + /** + * Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example + * CREATE / DROP table, view, trigger, index etc. + * + * @throws android.database.SQLException If the SQL string is invalid for + * some reason + */ + public void execute() { + acquireReference(); + try { + getSession().execute(getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + /** + * Execute this SQL statement, if the the number of rows affected by execution of this SQL + * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements. + * + * @return the number of rows affected by this SQL statement execution. + * @throws android.database.SQLException If the SQL string is invalid for + * some reason + */ + public int executeUpdateDelete() { + acquireReference(); + try { + return getSession().executeForChangedRowCount( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + /** + * Execute this SQL statement and return the ID of the row inserted due to this call. + * The SQL statement should be an INSERT for this to be a useful call. + * + * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise. + * + * @throws android.database.SQLException If the SQL string is invalid for + * some reason + */ + public long executeInsert() { + acquireReference(); + try { + return getSession().executeForLastInsertedRowId( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + /** + * Execute a statement that returns a 1 by 1 table with a numeric value. + * For example, SELECT COUNT(*) FROM table; + * + * @return The result of the query. + * + * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows + */ + public long simpleQueryForLong() { + acquireReference(); + try { + return getSession().executeForLong( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + /** + * Execute a statement that returns a 1 by 1 table with a text value. + * For example, SELECT COUNT(*) FROM table; + * + * @return The result of the query. + * + * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows + */ + public String simpleQueryForString() { + acquireReference(); + try { + return getSession().executeForString( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + /** + * Executes a statement that returns a 1 by 1 table with a blob value. + * + * @return A read-only file descriptor for a copy of the blob value, or {@code null} + * if the value is null or could not be read for some reason. + * + * @throws android.database.sqlite.SQLiteDoneException if the query returns zero rows + */ + public ParcelFileDescriptor simpleQueryForBlobFileDescriptor() { + acquireReference(); + try { + return getSession().executeForBlobFileDescriptor( + getSql(), getBindArgs(), getConnectionFlags(), null); + } catch (SQLiteDatabaseCorruptException ex) { + onCorruption(); + throw ex; + } finally { + releaseReference(); + } + } + + @Override + public String toString() { + return "SQLiteProgram: " + getSql(); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteStatementInfo.java b/src/api-impl/android/database/sqlite/SQLiteStatementInfo.java new file mode 100644 index 00000000..c70d5b5b --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteStatementInfo.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2011 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * Describes a SQLite statement. + * + * @hide + */ +public final class SQLiteStatementInfo { + /** + * The number of parameters that the statement has. + */ + public int numParameters; + + /** + * The names of all columns in the result set of the statement. + */ + public String[] columnNames; + + /** + * True if the statement is read-only. + */ + public boolean readOnly; +} diff --git a/src/api-impl/android/database/sqlite/SQLiteTableLockedException.java b/src/api-impl/android/database/sqlite/SQLiteTableLockedException.java new file mode 100644 index 00000000..e2402a06 --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteTableLockedException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2010 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +public class SQLiteTableLockedException extends SQLiteException { + public SQLiteTableLockedException() {} + + public SQLiteTableLockedException(String error) { + super(error); + } +} diff --git a/src/api-impl/android/database/sqlite/SQLiteTransactionListener.java b/src/api-impl/android/database/sqlite/SQLiteTransactionListener.java new file mode 100644 index 00000000..d0b12b2e --- /dev/null +++ b/src/api-impl/android/database/sqlite/SQLiteTransactionListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2009 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. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * A listener for transaction events. + */ +public interface SQLiteTransactionListener { + /** + * Called immediately after the transaction begins. + */ + void onBegin(); + + /** + * Called immediately before commiting the transaction. + */ + void onCommit(); + + /** + * Called if the transaction is about to be rolled back. + */ + void onRollback(); +} diff --git a/src/api-impl/android/os/CancellationSignal.java b/src/api-impl/android/os/CancellationSignal.java index 43b9f00f..b12b3726 100644 --- a/src/api-impl/android/os/CancellationSignal.java +++ b/src/api-impl/android/os/CancellationSignal.java @@ -1,6 +1,124 @@ +/* + * Copyright (C) 2012 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.os; -// import android.os.ICancellationSignal; - +/** + * Provides the ability to cancel an operation in progress. + */ public final class CancellationSignal { + private boolean mIsCanceled; + private OnCancelListener mOnCancelListener; + private boolean mCancelInProgress; + + /** + * Returns true if the operation has been canceled. + * + * @return True if the operation has been canceled. + */ + public boolean isCanceled() { + synchronized (this) { + return mIsCanceled; + } + } + + /** + * Throws {@link OperationCanceledException} if the operation has been canceled. + * + * @throws OperationCanceledException if the operation has been canceled. + */ + public void throwIfCanceled() { + if (isCanceled()) { + throw new OperationCanceledException(); + } + } + + /** + * Cancels the operation and signals the cancellation listener. + * If the operation has not yet started, then it will be canceled as soon as it does. + */ + public void cancel() { + final OnCancelListener listener; + synchronized (this) { + if (mIsCanceled) { + return; + } + mIsCanceled = true; + mCancelInProgress = true; + listener = mOnCancelListener; + } + try { + if (listener != null) { + listener.onCancel(); + } + } finally { + synchronized (this) { + mCancelInProgress = false; + notifyAll(); + } + } + } + + /** + * Sets the cancellation listener to be called when canceled. + * + * This method is intended to be used by the recipient of a cancellation signal + * such as a database or a content provider to handle cancellation requests + * while performing a long-running operation. This method is not intended to be + * used by applications themselves. + * + * If {@link CancellationSignal#cancel} has already been called, then the provided + * listener is invoked immediately. + * + * This method is guaranteed that the listener will not be called after it + * has been removed. + * + * @param listener The cancellation listener, or null to remove the current listener. + */ + public void setOnCancelListener(OnCancelListener listener) { + synchronized (this) { + waitForCancelFinishedLocked(); + if (mOnCancelListener == listener) { + return; + } + mOnCancelListener = listener; + if (!mIsCanceled || listener == null) { + return; + } + } + listener.onCancel(); + } + + private void waitForCancelFinishedLocked() { + while (mCancelInProgress) { + try { + wait(); + } catch (InterruptedException ex) { + } + } + } + + /** + * Listens for cancellation. + */ + public interface OnCancelListener { + /** + * Called when {@link CancellationSignal#cancel} is invoked. + */ + void onCancel(); + } + } diff --git a/src/api-impl/android/os/OperationCanceledException.java b/src/api-impl/android/os/OperationCanceledException.java new file mode 100644 index 00000000..a965e3cb --- /dev/null +++ b/src/api-impl/android/os/OperationCanceledException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 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.os; + +/** + * An exception type that is thrown when an operation in progress is canceled. + * + * @see CancellationSignal + */ +public class OperationCanceledException extends RuntimeException { + + public OperationCanceledException() { + this(null); + } + + public OperationCanceledException(String message) { + super(message != null ? message : "The operation has been canceled."); + } +} \ No newline at end of file diff --git a/src/api-impl/android/util/LruCache.java b/src/api-impl/android/util/LruCache.java new file mode 100644 index 00000000..b187e05e --- /dev/null +++ b/src/api-impl/android/util/LruCache.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2011 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.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A cache that holds strong references to a limited number of values. Each time + * a value is accessed, it is moved to the head of a queue. When a value is + * added to a full cache, the value at the end of that queue is evicted and may + * become eligible for garbage collection. + * + *

If your cached values hold resources that need to be explicitly released, + * override {@link #entryRemoved}. + * + *

If a cache miss should be computed on demand for the corresponding keys, + * override {@link #create}. This simplifies the calling code, allowing it to + * assume a value will always be returned, even when there's a cache miss. + * + *

By default, the cache size is measured in the number of entries. Override + * {@link #sizeOf} to size the cache in different units. For example, this cache + * is limited to 4MiB of bitmaps: + *

   {@code
+ *   int cacheSize = 4 * 1024 * 1024; // 4MiB
+ *   LruCache bitmapCache = new LruCache(cacheSize) {
+ *       protected int sizeOf(String key, Bitmap value) {
+ *           return value.getByteCount();
+ *       }
+ *   }}
+ * + *

This class is thread-safe. Perform multiple cache operations atomically by + * synchronizing on the cache:

   {@code
+ *   synchronized (cache) {
+ *     if (cache.get(key) == null) {
+ *         cache.put(key, value);
+ *     }
+ *   }}
+ * + *

This class does not allow null to be used as a key or value. A return + * value of null from {@link #get}, {@link #put} or {@link #remove} is + * unambiguous: the key was not in the cache. + * + *

This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of Android's + * Support Package for earlier releases. + */ +public class LruCache { + private final LinkedHashMap map; + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * + * @param maxSize The new maximum size. + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + V createdValue = create(key); + if (createdValue == null) { + return null; + } + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + if (previous != null) { + entryRemoved(false, key, previous, value); + } + trimToSize(maxSize); + return previous; + } + + /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + public void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + if (size <= maxSize) { + break; + } + Map.Entry toEvict = map.eldest(); + if (toEvict == null) { + break; + } + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + if (previous != null) { + entryRemoved(false, key, previous, null); + } + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + *

The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + *

If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + *

An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} \ No newline at end of file diff --git a/src/api-impl/android/util/Pair.java b/src/api-impl/android/util/Pair.java new file mode 100644 index 00000000..910b129e --- /dev/null +++ b/src/api-impl/android/util/Pair.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2009 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.util; + +import java.util.Objects; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } + Pair p = (Pair) o; + return Objects.equals(p.first, first) && Objects.equals(p.second, second); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override + public int hashCode() { + return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + } + + @Override + public String toString() { + return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}"; + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/src/api-impl/meson.build b/src/api-impl/meson.build index 3c141194..a12bcb67 100644 --- a/src/api-impl/meson.build +++ b/src/api-impl/meson.build @@ -85,21 +85,58 @@ hax_jar = jar('hax', [ 'android/content/res/XmlResourceParser.java', 'android/content/ServiceConnection.java', 'android/content/SharedPreferences.java', - 'android/database/Cursor.java', + 'android/database/AbstractCursor.java', + 'android/database/AbstractWindowedCursor.java', + 'android/database/ContentObservable.java', 'android/database/ContentObserver.java', + 'android/database/CrossProcessCursor.java', + 'android/database/Cursor.java', + 'android/database/CursorWindow.java', 'android/database/DatabaseErrorHandler.java', + 'android/database/DatabaseUtils.java', 'android/database/DataSetObservable.java', 'android/database/DataSetObserver.java', + 'android/database/DefaultDatabaseErrorHandler.java', 'android/database/Observable.java', + 'android/database/SQLException.java', 'android/database/sqlite/DatabaseErrorHandler.java', + 'android/database/sqlite/DatabaseObjectNotClosedException.java', + 'android/database/sqlite/SQLiteAbortException.java', + 'android/database/sqlite/SQLiteAccessPermException.java', + 'android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java', + 'android/database/sqlite/SQLiteBlobTooBigException.java', + 'android/database/sqlite/SQLiteCantOpenDatabaseException.java', + 'android/database/sqlite/SQLiteClosable.java', + 'android/database/sqlite/SQLiteConnection.java', + 'android/database/sqlite/SQLiteConnectionPool.java', + 'android/database/sqlite/SQLiteConstraintException.java', 'android/database/sqlite/SQLiteCursorDriver.java', 'android/database/sqlite/SQLiteCursor.java', + 'android/database/sqlite/SQLiteCustomFunction.java', + 'android/database/sqlite/SQLiteDatabaseConfiguration.java', + 'android/database/sqlite/SQLiteDatabaseCorruptException.java', 'android/database/sqlite/SQLiteDatabase.java', + 'android/database/sqlite/SQLiteDatabaseLockedException.java', + 'android/database/sqlite/SQLiteDatatypeMismatchException.java', + 'android/database/sqlite/SQLiteDebug.java', 'android/database/sqlite/SQLiteDirectCursorDriver.java', + 'android/database/sqlite/SQLiteDiskIOException.java', + 'android/database/sqlite/SQLiteDoneException.java', 'android/database/sqlite/SQLiteException.java', + 'android/database/sqlite/SQLiteFullException.java', + 'android/database/sqlite/SQLiteGlobal.java', + 'android/database/sqlite/SQLiteMisuseException.java', 'android/database/sqlite/SQLiteOpenHelper.java', + 'android/database/sqlite/SQLiteOutOfMemoryException.java', 'android/database/sqlite/SQLiteProgram.java', + 'android/database/sqlite/SQLiteQueryBuilder.java', 'android/database/sqlite/SQLiteQuery.java', + 'android/database/sqlite/SQLiteReadOnlyDatabaseException.java', + 'android/database/sqlite/SQLiteSession.java', + 'android/database/sqlite/SQLiteStatementInfo.java', + 'android/database/sqlite/SQLiteStatement.java', + 'android/database/sqlite/SQLiteTableLockedException.java', + 'android/database/sqlite/SQLiteTransactionListener.java', 'android/gesture/GestureOverlayView.java', 'android/gesture/GestureStore.java', 'android/graphics/BitmapFactory.java', @@ -174,6 +211,7 @@ hax_jar = jar('hax', [ 'android/os/Message.java', 'android/os/MessageQueue.java', 'android/os/Messenger.java', + 'android/os/OperationCanceledException.java', 'android/os/Parcelable.java', 'android/os/ParcelFileDescriptor.java', 'android/os/PatternMatcher.java', @@ -230,7 +268,9 @@ hax_jar = jar('hax', [ 'android/util/LayoutDirection.java', 'android/util/Log.java', 'android/util/LongSparseArray.java', + 'android/util/LruCache.java', 'android/util/MapCollections.java', + 'android/util/Pair.java', 'android/util/PrefixPrinter.java', 'android/util/Printer.java', 'android/util/Slog.java',