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.
+ *
+ *
{@link #STATEMENT_SELECT}
+ *
{@link #STATEMENT_UPDATE}
+ *
{@link #STATEMENT_ATTACH}
+ *
{@link #STATEMENT_BEGIN}
+ *
{@link #STATEMENT_COMMIT}
+ *
{@link #STATEMENT_ABORT}
+ *
{@link #STATEMENT_OTHER}
+ *
+ * @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:
+ *
+ * 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
+ *
+ * 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.
+ *
+ *
+ * @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}.
+ *
+ *
+ * @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:
+ *
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.
+ *
+ *