From f40dde7c830a923410994f8549679ef9b91442ff Mon Sep 17 00:00:00 2001
From: nbransby
Date: Fri, 5 Jan 2024 01:24:19 +1300
Subject: [PATCH 1/7] add getDatabasePath to FirebasePlatform
---
build.gradle.kts | 3 ++-
src/main/java/android/content/Context.kt | 2 ++
src/main/java/com/google/firebase/FirebasePlatform.kt | 4 ++++
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 0d032df..10c7790 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -27,7 +27,7 @@ version = project.property("version") as String
java {
withSourcesJar()
- withJavadocJar()
+// withJavadocJar()
sourceCompatibility = JavaVersion.VERSION_11
}
@@ -149,6 +149,7 @@ dependencies {
compileOnly("org.robolectric:android-all:12.1-robolectric-8229987")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.0")
+ testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.0")
// firebase aars
aar("com.google.firebase:firebase-firestore:24.10.0")
aar("com.google.firebase:firebase-functions:20.4.0")
diff --git a/src/main/java/android/content/Context.kt b/src/main/java/android/content/Context.kt
index 5032137..abf685f 100644
--- a/src/main/java/android/content/Context.kt
+++ b/src/main/java/android/content/Context.kt
@@ -132,6 +132,8 @@ open class Context {
return File(System.getProperty("java.io.tmpdir"))
}
+ fun getDatabasePath(name: String): File = FirebasePlatform.firebasePlatform.getDatabasePath(name)
+
companion object {
@JvmStatic
val CONNECTIVITY_SERVICE = "connectivity"
diff --git a/src/main/java/com/google/firebase/FirebasePlatform.kt b/src/main/java/com/google/firebase/FirebasePlatform.kt
index 20ac5ea..8fe28d5 100644
--- a/src/main/java/com/google/firebase/FirebasePlatform.kt
+++ b/src/main/java/com/google/firebase/FirebasePlatform.kt
@@ -1,5 +1,7 @@
package com.google.firebase
+import java.io.File
+
abstract class FirebasePlatform {
companion object {
@@ -20,4 +22,6 @@ abstract class FirebasePlatform {
abstract fun clear(key: String)
abstract fun log(msg: String)
+
+ open fun getDatabasePath(name: String): File = File(System.getProperty("java.io.tmpdir"))
}
From 75e1131e318257dbeef84cbdbd759d24ed2f070a Mon Sep 17 00:00:00 2001
From: nbransby
Date: Fri, 5 Jan 2024 01:25:01 +1300
Subject: [PATCH 2/7] add FirestoreTest
---
src/test/kotlin/FirestoreTest.kt | 37 ++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
create mode 100644 src/test/kotlin/FirestoreTest.kt
diff --git a/src/test/kotlin/FirestoreTest.kt b/src/test/kotlin/FirestoreTest.kt
new file mode 100644
index 0000000..6bc0dcb
--- /dev/null
+++ b/src/test/kotlin/FirestoreTest.kt
@@ -0,0 +1,37 @@
+import android.app.Application
+import com.google.firebase.Firebase
+import com.google.firebase.FirebaseOptions
+import com.google.firebase.FirebasePlatform
+import com.google.firebase.firestore.firestore
+import com.google.firebase.initialize
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.tasks.await
+import org.junit.Before
+import org.junit.Test
+
+
+class FirestoreTest {
+ @Before
+ fun initialize() {
+ FirebasePlatform.initializeFirebasePlatform(object : FirebasePlatform() {
+ val storage = mutableMapOf()
+ override fun store(key: String, value: String) = storage.set(key, value)
+ override fun retrieve(key: String) = storage[key]
+ override fun clear(key: String) { storage.remove(key) }
+ override fun log(msg: String) = println(msg)
+ })
+ val options = FirebaseOptions.Builder()
+ .setProjectId("my-firebase-project")
+ .setApplicationId("1:27992087142:android:ce3b6448250083d1")
+ .setApiKey("AIzaSyADUe90ULnQDuGShD9W23RDP0xmeDc6Mvw")
+ // setDatabaseURL(...)
+ // setStorageBucket(...)
+ .build()
+ Firebase.initialize(Application(), options)
+ }
+
+ @Test
+ fun testFirestore(): Unit = runBlocking {
+ Firebase.firestore.document("sally/jim").get().await()
+ }
+}
\ No newline at end of file
From b746a6a23934e1e2afa74434290bacd55252d11a Mon Sep 17 00:00:00 2001
From: nbransby
Date: Fri, 5 Jan 2024 01:25:51 +1300
Subject: [PATCH 3/7] add android os and util classes used by android.database
---
.../java/android/os/CancellationSignal.java | 192 ++++++++++
.../android/os/OperationCanceledException.kt | 3 +
src/main/java/android/util/LruCache.java | 337 ++++++++++++++++++
3 files changed, 532 insertions(+)
create mode 100644 src/main/java/android/os/CancellationSignal.java
create mode 100644 src/main/java/android/os/OperationCanceledException.kt
create mode 100644 src/main/java/android/util/LruCache.java
diff --git a/src/main/java/android/os/CancellationSignal.java b/src/main/java/android/os/CancellationSignal.java
new file mode 100644
index 0000000..f094bf3
--- /dev/null
+++ b/src/main/java/android/os/CancellationSignal.java
@@ -0,0 +1,192 @@
+/*
+ * 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;
+
+/**
+ * Provides the ability to cancel an operation in progress.
+ */
+public final class CancellationSignal {
+ private boolean mIsCanceled;
+ private OnCancelListener mOnCancelListener;
+ private ICancellationSignal mRemote;
+ private boolean mCancelInProgress;
+ /**
+ * Creates a cancellation signal, initially not canceled.
+ */
+ public CancellationSignal() {
+ }
+ /**
+ * 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;
+ final ICancellationSignal remote;
+ synchronized (this) {
+ if (mIsCanceled) {
+ return;
+ }
+ mIsCanceled = true;
+ mCancelInProgress = true;
+ listener = mOnCancelListener;
+ remote = mRemote;
+ }
+ try {
+ if (listener != null) {
+ listener.onCancel();
+ }
+ if (remote != null) {
+ try {
+ remote.cancel();
+ } catch (RemoteException ex) {
+ }
+ }
+ } 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();
+ }
+ /**
+ * Sets the remote transport.
+ *
+ * If {@link CancellationSignal#cancel} has already been called, then the provided
+ * remote transport is canceled immediately.
+ *
+ * This method is guaranteed that the remote transport will not be called after it
+ * has been removed.
+ *
+ * @param remote The remote transport, or null to remove.
+ *
+ * @hide
+ */
+ public void setRemote(ICancellationSignal remote) {
+ synchronized (this) {
+ waitForCancelFinishedLocked();
+ if (mRemote == remote) {
+ return;
+ }
+ mRemote = remote;
+ if (!mIsCanceled || remote == null) {
+ return;
+ }
+ }
+ try {
+ remote.cancel();
+ } catch (RemoteException ex) {
+ }
+ }
+
+ private void waitForCancelFinishedLocked() {
+ while (mCancelInProgress) {
+ try {
+ wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ /**
+ * Creates a transport that can be returned back to the caller of
+ * a Binder function and subsequently used to dispatch a cancellation signal.
+ *
+ * @return The new cancellation signal transport.
+ *
+ * @hide
+ */
+ public static ICancellationSignal createTransport() {
+ return new Transport();
+ }
+ /**
+ * Given a locally created transport, returns its associated cancellation signal.
+ *
+ * @param transport The locally created transport, or null if none.
+ * @return The associated cancellation signal, or null if none.
+ *
+ * @hide
+ */
+ public static CancellationSignal fromTransport(ICancellationSignal transport) {
+ if (transport instanceof Transport) {
+ return ((Transport)transport).mCancellationSignal;
+ }
+ return null;
+ }
+ /**
+ * Listens for cancellation.
+ */
+ public interface OnCancelListener {
+ /**
+ * Called when {@link CancellationSignal#cancel} is invoked.
+ */
+ void onCancel();
+ }
+ private static final class Transport extends ICancellationSignal.Stub {
+ final CancellationSignal mCancellationSignal = new CancellationSignal();
+ @Override
+ public void cancel() throws RemoteException {
+ mCancellationSignal.cancel();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/os/OperationCanceledException.kt b/src/main/java/android/os/OperationCanceledException.kt
new file mode 100644
index 0000000..0ed2022
--- /dev/null
+++ b/src/main/java/android/os/OperationCanceledException.kt
@@ -0,0 +1,3 @@
+package android.os
+
+class OperationCanceledException : RuntimeException()
\ No newline at end of file
diff --git a/src/main/java/android/util/LruCache.java b/src/main/java/android/util/LruCache.java
new file mode 100644
index 0000000..52a9516
--- /dev/null
+++ b/src/main/java/android/util/LruCache.java
@@ -0,0 +1,337 @@
+/*
+ * 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.Iterator;
+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) {
+ Iterator> iterator = map.entrySet().iterator();
+ while (iterator.hasNext()) {
+ 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 = iterator.next();
+ key = toEvict.getKey();
+ value = toEvict.getValue();
+ iterator.remove();
+ size -= safeSizeOf(key, value);
+ evictionCount++;
+ }
+ entryRemoved(true, key, value, null);
+ }
+ }
+ /**
+ * Removes the entry for {@code key} if it exists.
+ *
+ * @return the previous value mapped by {@code key}.
+ */
+ public final V remove(K key) {
+ if (key == null) {
+ throw new NullPointerException("key == null");
+ }
+ V previous;
+ synchronized (this) {
+ previous = map.remove(key);
+ if (previous != null) {
+ size -= safeSizeOf(key, previous);
+ }
+ }
+ if (previous != null) {
+ entryRemoved(false, key, previous, null);
+ }
+ return previous;
+ }
+ /**
+ * Called for entries that have been evicted or removed. This method is
+ * invoked when a value is evicted to make space, removed by a call to
+ * {@link #remove}, or replaced by a call to {@link #put}. The default
+ * implementation does nothing.
+ *
+ *
The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ * @param evicted true if the entry is being removed to make space, false
+ * if the removal was caused by a {@link #put} or {@link #remove}.
+ * @param newValue the new value for {@code key}, if it exists. If non-null,
+ * this removal was caused by a {@link #put} or a {@link #get}. Otherwise it was caused by
+ * an eviction or a {@link #remove}.
+ */
+ protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
+ /**
+ * Called after a cache miss to compute a value for the corresponding key.
+ * Returns the computed value or null if no value can be computed. The
+ * default implementation returns null.
+ *
+ *
The method is called without synchronization: other threads may
+ * access the cache while this method is executing.
+ *
+ *
If a value for {@code key} exists in the cache when this method
+ * returns, the created value will be released with {@link #entryRemoved}
+ * and discarded. This can occur when multiple threads request the same key
+ * at the same time (causing multiple values to be created), or when one
+ * thread calls {@link #put} while another is creating a value for the same
+ * key.
+ */
+ protected V create(K key) {
+ return null;
+ }
+ private int safeSizeOf(K key, V value) {
+ int result = sizeOf(key, value);
+ if (result < 0) {
+ throw new IllegalStateException("Negative size: " + key + "=" + value);
+ }
+ return result;
+ }
+ /**
+ * Returns the size of the entry for {@code key} and {@code value} in
+ * user-defined units. The default implementation returns 1 so that size
+ * is the number of entries and max size is the maximum number of entries.
+ *
+ *
An entry's size must not change while it is in the cache.
+ */
+ protected int sizeOf(K key, V value) {
+ return 1;
+ }
+ /**
+ * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+ */
+ public final void evictAll() {
+ trimToSize(-1); // -1 will evict 0-sized elements
+ }
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the number
+ * of entries in the cache. For all other caches, this returns the sum of
+ * the sizes of the entries in this cache.
+ */
+ public synchronized final int size() {
+ return size;
+ }
+ /**
+ * For caches that do not override {@link #sizeOf}, this returns the maximum
+ * number of entries in the cache. For all other caches, this returns the
+ * maximum sum of the sizes of the entries in this cache.
+ */
+ public synchronized final int maxSize() {
+ return maxSize;
+ }
+ /**
+ * Returns the number of times {@link #get} returned a value that was
+ * already present in the cache.
+ */
+ public synchronized final int hitCount() {
+ return hitCount;
+ }
+ /**
+ * Returns the number of times {@link #get} returned null or required a new
+ * value to be created.
+ */
+ public synchronized final int missCount() {
+ return missCount;
+ }
+ /**
+ * Returns the number of times {@link #create(Object)} returned a value.
+ */
+ public synchronized final int createCount() {
+ return createCount;
+ }
+ /**
+ * Returns the number of times {@link #put} was called.
+ */
+ public synchronized final int putCount() {
+ return putCount;
+ }
+ /**
+ * Returns the number of values that have been evicted.
+ */
+ public synchronized final int evictionCount() {
+ return evictionCount;
+ }
+ /**
+ * Returns a copy of the current contents of the cache, ordered from least
+ * recently accessed to most recently accessed.
+ */
+ public synchronized final Map snapshot() {
+ return new LinkedHashMap(map);
+ }
+ @Override public synchronized final String toString() {
+ int accesses = hitCount + missCount;
+ int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
+ return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
+ maxSize, hitCount, missCount, hitPercent);
+ }
+}
\ No newline at end of file
From 168d65dfa68f95d89ab84fa40a368af38ae17dd6 Mon Sep 17 00:00:00 2001
From: nbransby
Date: Fri, 5 Jan 2024 01:26:57 +1300
Subject: [PATCH 4/7] add source for android.database from
https://www.sqlite.org/android/doc/trunk/www/install.wiki
---
.../database/DatabaseErrorHandler.java | 37 +
.../java/android/database/DatabaseUtils.java | 1461 +++++++++++
.../database/DefaultDatabaseErrorHandler.java | 117 +
.../java/android/database/SQLException.java | 37 +
.../android/database/sqlite/CloseGuard.java | 235 ++
.../DatabaseObjectNotClosedException.java | 35 +
.../database/sqlite/SQLiteAbortException.java | 34 +
.../sqlite/SQLiteAccessPermException.java | 33 +
...eBindOrColumnIndexOutOfRangeException.java | 32 +
.../sqlite/SQLiteBlobTooBigException.java | 29 +
.../SQLiteCantOpenDatabaseException.java | 29 +
.../database/sqlite/SQLiteClosable.java | 112 +
.../database/sqlite/SQLiteConnection.java | 1526 +++++++++++
.../database/sqlite/SQLiteConnectionPool.java | 1086 ++++++++
.../sqlite/SQLiteConstraintException.java | 32 +
.../android/database/sqlite/SQLiteCursor.java | 284 +++
.../database/sqlite/SQLiteCursorDriver.java | 60 +
.../database/sqlite/SQLiteCustomFunction.java | 57 +
.../database/sqlite/SQLiteDatabase.java | 2223 +++++++++++++++++
.../sqlite/SQLiteDatabaseConfiguration.java | 177 ++
.../SQLiteDatabaseCorruptException.java | 32 +
.../sqlite/SQLiteDatabaseLockedException.java | 37 +
.../SQLiteDatatypeMismatchException.java | 29 +
.../android/database/sqlite/SQLiteDebug.java | 178 ++
.../sqlite/SQLiteDirectCursorDriver.java | 87 +
.../sqlite/SQLiteDiskIOException.java | 33 +
.../database/sqlite/SQLiteDoneException.java | 35 +
.../database/sqlite/SQLiteException.java | 39 +
.../database/sqlite/SQLiteFullException.java | 32 +
.../android/database/sqlite/SQLiteGlobal.java | 118 +
.../sqlite/SQLiteMisuseException.java | 41 +
.../database/sqlite/SQLiteOpenHelper.java | 443 ++++
.../sqlite/SQLiteOutOfMemoryException.java | 29 +
.../database/sqlite/SQLiteProgram.java | 222 ++
.../android/database/sqlite/SQLiteQuery.java | 88 +
.../database/sqlite/SQLiteQueryBuilder.java | 652 +++++
.../SQLiteReadOnlyDatabaseException.java | 29 +
.../database/sqlite/SQLiteSession.java | 967 +++++++
.../database/sqlite/SQLiteStatement.java | 167 ++
.../database/sqlite/SQLiteStatementInfo.java | 43 +
.../sqlite/SQLiteTableLockedException.java | 29 +
.../sqlite/SQLiteTransactionListener.java | 41 +
42 files changed, 11007 insertions(+)
create mode 100644 src/main/java/android/database/DatabaseErrorHandler.java
create mode 100644 src/main/java/android/database/DatabaseUtils.java
create mode 100644 src/main/java/android/database/DefaultDatabaseErrorHandler.java
create mode 100644 src/main/java/android/database/SQLException.java
create mode 100644 src/main/java/android/database/sqlite/CloseGuard.java
create mode 100644 src/main/java/android/database/sqlite/DatabaseObjectNotClosedException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteAbortException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteAccessPermException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteBlobTooBigException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteClosable.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteConnection.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteConnectionPool.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteConstraintException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteCursor.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteCursorDriver.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteCustomFunction.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDatabase.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDatabaseLockedException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDatatypeMismatchException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDebug.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDirectCursorDriver.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDiskIOException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteDoneException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteFullException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteGlobal.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteMisuseException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteOpenHelper.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteOutOfMemoryException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteProgram.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteQuery.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteQueryBuilder.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteSession.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteStatement.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteStatementInfo.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteTableLockedException.java
create mode 100644 src/main/java/android/database/sqlite/SQLiteTransactionListener.java
diff --git a/src/main/java/android/database/DatabaseErrorHandler.java b/src/main/java/android/database/DatabaseErrorHandler.java
new file mode 100644
index 0000000..d0aca74
--- /dev/null
+++ b/src/main/java/android/database/DatabaseErrorHandler.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;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * An interface to let apps define an action to take when database corruption is detected.
+ */
+public interface DatabaseErrorHandler {
+
+ /**
+ * The method invoked when database corruption is detected.
+ * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+ * is detected.
+ */
+ void onCorruption(SQLiteDatabase dbObj);
+}
diff --git a/src/main/java/android/database/DatabaseUtils.java b/src/main/java/android/database/DatabaseUtils.java
new file mode 100644
index 0000000..3d40798
--- /dev/null
+++ b/src/main/java/android/database/DatabaseUtils.java
@@ -0,0 +1,1461 @@
+/*
+ * 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.content.OperationApplicationException;
+import android.database.sqlite.SQLiteAbortException;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.database.sqlite.SQLiteProgram;
+import android.database.sqlite.SQLiteStatement;
+
+import android.database.CursorWindow;
+import android.os.OperationCanceledException;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+
+import android.database.Cursor;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+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;
+
+ /**
+ * Special function for writing an exception result at the header of
+ * a parcel, to be used when returning an exception from a transaction.
+ * exception will be re-thrown by the function in another process
+ * @param reply Parcel to write to
+ * @param e The Exception to be written.
+ * @see Parcel#writeNoException
+ * @see Parcel#writeException
+ */
+ public static final void writeExceptionToParcel(Parcel reply, Exception e) {
+ int code = 0;
+ boolean logException = true;
+ if (e instanceof FileNotFoundException) {
+ code = 1;
+ logException = false;
+ } else if (e instanceof IllegalArgumentException) {
+ code = 2;
+ } else if (e instanceof UnsupportedOperationException) {
+ code = 3;
+ } else if (e instanceof SQLiteAbortException) {
+ code = 4;
+ } else if (e instanceof SQLiteConstraintException) {
+ code = 5;
+ } else if (e instanceof SQLiteDatabaseCorruptException) {
+ code = 6;
+ } else if (e instanceof SQLiteFullException) {
+ code = 7;
+ } else if (e instanceof SQLiteDiskIOException) {
+ code = 8;
+ } else if (e instanceof SQLiteException) {
+ code = 9;
+ } else if (e instanceof OperationApplicationException) {
+ code = 10;
+ } else if (e instanceof OperationCanceledException) {
+ code = 11;
+ logException = false;
+ } else {
+ reply.writeException(e);
+ Log.e(TAG, "Writing exception to parcel", e);
+ return;
+ }
+ reply.writeInt(code);
+ reply.writeString(e.getMessage());
+
+ if (logException) {
+ Log.e(TAG, "Writing exception to parcel", e);
+ }
+ }
+
+ /**
+ * Special function for reading an exception result from the header of
+ * a parcel, to be used after receiving the result of a transaction. This
+ * will throw the exception for you if it had been written to the Parcel,
+ * otherwise return and let you read the normal result data from the Parcel.
+ * @param reply Parcel to read from
+ * @see Parcel#writeNoException
+ * @see Parcel#readException
+ */
+// public static final void readExceptionFromParcel(Parcel reply) {
+// int code = reply.readExceptionCode();
+// if (code == 0) return;
+// String msg = reply.readString();
+// DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+// }
+//
+// public static void readExceptionWithFileNotFoundExceptionFromParcel(
+// Parcel reply) throws FileNotFoundException {
+// int code = reply.readExceptionCode();
+// if (code == 0) return;
+// String msg = reply.readString();
+// if (code == 1) {
+// throw new FileNotFoundException(msg);
+// } else {
+// DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+// }
+// }
+//
+// public static void readExceptionWithOperationApplicationExceptionFromParcel(
+// Parcel reply) throws OperationApplicationException {
+// int code = reply.readExceptionCode();
+// if (code == 0) return;
+// String msg = reply.readString();
+// if (code == 10) {
+// throw new OperationApplicationException(msg);
+// } else {
+// DatabaseUtils.readExceptionFromParcel(reply, msg, code);
+// }
+// }
+
+ private static final void readExceptionFromParcel(Parcel reply, String msg, int code) {
+ switch (code) {
+ case 2:
+ throw new IllegalArgumentException(msg);
+ case 3:
+ throw new UnsupportedOperationException(msg);
+ case 4:
+ throw new SQLiteAbortException(msg);
+ case 5:
+ throw new SQLiteConstraintException(msg);
+ case 6:
+ throw new SQLiteDatabaseCorruptException(msg);
+ case 7:
+ throw new SQLiteFullException(msg);
+ case 8:
+ throw new SQLiteDiskIOException(msg);
+ case 9:
+ throw new SQLiteException(msg);
+ case 11:
+ throw new OperationCanceledException(msg);
+ default:
+ reply.readException(code, msg);
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteDatabase db,
+ String query, String[] selectionArgs) {
+ SQLiteStatement prog = db.compileStatement(query);
+ try {
+ return blobFileDescriptorForQuery(prog, selectionArgs);
+ } finally {
+ prog.close();
+ }
+ }
+
+ /**
+ * Utility method to run the pre-compiled query 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.
+ */
+ public static ParcelFileDescriptor blobFileDescriptorForQuery(SQLiteStatement prog,
+ String[] selectionArgs) {
+ prog.bindAllArgsAsStrings(selectionArgs);
+ return prog.simpleQueryForBlobFileDescriptor();
+ }
+
+ /**
+ * 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/main/java/android/database/DefaultDatabaseErrorHandler.java b/src/main/java/android/database/DefaultDatabaseErrorHandler.java
new file mode 100644
index 0000000..ce8d731
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/SQLException.java b/src/main/java/android/database/SQLException.java
new file mode 100644
index 0000000..c680c42
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/CloseGuard.java b/src/main/java/android/database/sqlite/CloseGuard.java
new file mode 100644
index 0000000..7e235bc
--- /dev/null
+++ b/src/main/java/android/database/sqlite/CloseGuard.java
@@ -0,0 +1,235 @@
+/*
+ * 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;
+import android.util.Log;
+
+/**
+ * CloseGuard is a mechanism for flagging implicit finalizer cleanup of
+ * resources that should have been cleaned up by explicit close
+ * methods (aka "explicit termination methods" in Effective Java).
+ *
+ *
+ * In usage where the resource to be explicitly cleaned up are
+ * allocated after object construction, CloseGuard protection can
+ * be deferred. For example:
+ *
+ * When used in a constructor calls to {@code open} should occur at
+ * the end of the constructor since an exception that would cause
+ * abrupt termination of the constructor will mean that the user will
+ * not have a reference to the object to cleanup explicitly. When used
+ * in a method, the call to {@code open} should occur just after
+ * resource acquisition.
+ *
+ *
+ *
+ * Note that the null check on {@code guard} in the finalizer is to
+ * cover cases where a constructor throws an exception causing the
+ * {@code guard} to be uninitialized.
+ *
+ * @hide
+ */
+public final class CloseGuard {
+
+ /**
+ * Instance used when CloseGuard is disabled to avoid allocation.
+ */
+ private static final CloseGuard NOOP = new CloseGuard();
+
+ /**
+ * Enabled by default so we can catch issues early in VM startup.
+ * Note, however, that Android disables this early in its startup,
+ * but enables it with DropBoxing for system apps on debug builds.
+ */
+ private static volatile boolean ENABLED = true;
+
+ /**
+ * Hook for customizing how CloseGuard issues are reported.
+ */
+ private static volatile Reporter REPORTER = new DefaultReporter();
+
+ /**
+ * Returns a CloseGuard instance. If CloseGuard is enabled, {@code
+ * #open(String)} can be used to set up the instance to warn on
+ * failure to close. If CloseGuard is disabled, a non-null no-op
+ * instance is returned.
+ */
+ public static CloseGuard get() {
+ if (!ENABLED) {
+ return NOOP;
+ }
+ return new CloseGuard();
+ }
+
+ /**
+ * Used to enable or disable CloseGuard. Note that CloseGuard only
+ * warns if it is enabled for both allocation and finalization.
+ */
+ public static void setEnabled(boolean enabled) {
+ ENABLED = enabled;
+ }
+
+ /**
+ * Used to replace default Reporter used to warn of CloseGuard
+ * violations. Must be non-null.
+ */
+ public static void setReporter(Reporter reporter) {
+ if (reporter == null) {
+ throw new NullPointerException("reporter == null");
+ }
+ REPORTER = reporter;
+ }
+
+ /**
+ * Returns non-null CloseGuard.Reporter.
+ */
+ public static Reporter getReporter() {
+ return REPORTER;
+ }
+
+ private CloseGuard() {}
+
+ /**
+ * If CloseGuard is enabled, {@code open} initializes the instance
+ * with a warning that the caller should have explicitly called the
+ * {@code closer} method instead of relying on finalization.
+ *
+ * @param closer non-null name of explicit termination method
+ * @throws NullPointerException if closer is null, regardless of
+ * whether or not CloseGuard is enabled
+ */
+ public void open(String closer) {
+ // always perform the check for valid API usage...
+ if (closer == null) {
+ throw new NullPointerException("closer == null");
+ }
+ // ...but avoid allocating an allocationSite if disabled
+ if (this == NOOP || !ENABLED) {
+ return;
+ }
+ String message = "Explicit termination method '" + closer + "' not called";
+ allocationSite = new Throwable(message);
+ }
+
+ private Throwable allocationSite;
+
+ /**
+ * Marks this CloseGuard instance as closed to avoid warnings on
+ * finalization.
+ */
+ public void close() {
+ allocationSite = null;
+ }
+
+ /**
+ * If CloseGuard is enabled, logs a warning if the caller did not
+ * properly cleanup by calling an explicit close method
+ * before finalization. If CloseGuard is disabled, no action is
+ * performed.
+ */
+ public void warnIfOpen() {
+ if (allocationSite == null || !ENABLED) {
+ return;
+ }
+
+ String message =
+ ("A resource was acquired at attached stack trace but never released. "
+ + "See java.io.Closeable for information on avoiding resource leaks.");
+
+ REPORTER.report(message, allocationSite);
+ }
+
+ /**
+ * Interface to allow customization of reporting behavior.
+ */
+ public static interface Reporter {
+ public void report (String message, Throwable allocationSite);
+ }
+
+ /**
+ * Default Reporter which reports CloseGuard violations to the log.
+ */
+ private static final class DefaultReporter implements Reporter {
+ @Override public void report (String message, Throwable allocationSite) {
+ Log.w(message, allocationSite);
+ }
+ }
+}
diff --git a/src/main/java/android/database/sqlite/DatabaseObjectNotClosedException.java b/src/main/java/android/database/sqlite/DatabaseObjectNotClosedException.java
new file mode 100644
index 0000000..a1c47cc
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteAbortException.java b/src/main/java/android/database/sqlite/SQLiteAbortException.java
new file mode 100644
index 0000000..7e1f543
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteAccessPermException.java b/src/main/java/android/database/sqlite/SQLiteAccessPermException.java
new file mode 100644
index 0000000..be3a1c1
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java b/src/main/java/android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException.java
new file mode 100644
index 0000000..20fd944
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteBlobTooBigException.java b/src/main/java/android/database/sqlite/SQLiteBlobTooBigException.java
new file mode 100644
index 0000000..54d42ce
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java b/src/main/java/android/database/sqlite/SQLiteCantOpenDatabaseException.java
new file mode 100644
index 0000000..8b153f1
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteClosable.java b/src/main/java/android/database/sqlite/SQLiteClosable.java
new file mode 100644
index 0000000..f215b2c
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteConnection.java b/src/main/java/android/database/sqlite/SQLiteConnection.java
new file mode 100644
index 0000000..b3167e1
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteConnection.java
@@ -0,0 +1,1526 @@
+/*
+ * 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.sqlite.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.OperationCanceledException;
+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);
+ }
+
+ /**
+ * 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);
+ }
+
+ public static boolean hasCodec() {
+ return SQLiteConnection.hasCodec();
+ }
+
+ public void enableLocalizedCollators() {
+ mConnectionPoolLocked.enableLocalizedCollators();
+ }
+}
diff --git a/src/main/java/android/database/sqlite/SQLiteDatabaseConfiguration.java b/src/main/java/android/database/sqlite/SQLiteDatabaseConfiguration.java
new file mode 100644
index 0000000..7a351a5
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteDatabaseCorruptException.java b/src/main/java/android/database/sqlite/SQLiteDatabaseCorruptException.java
new file mode 100644
index 0000000..b6da9c1
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteDatabaseLockedException.java b/src/main/java/android/database/sqlite/SQLiteDatabaseLockedException.java
new file mode 100644
index 0000000..a05a43d
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteDatatypeMismatchException.java b/src/main/java/android/database/sqlite/SQLiteDatatypeMismatchException.java
new file mode 100644
index 0000000..08d23b7
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteDebug.java b/src/main/java/android/database/sqlite/SQLiteDebug.java
new file mode 100644
index 0000000..85264f4
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteDebug.java
@@ -0,0 +1,178 @@
+/*
+ * 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.os.Build;
+/* import android.os.SystemProperties; */
+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/main/java/android/database/sqlite/SQLiteDirectCursorDriver.java b/src/main/java/android/database/sqlite/SQLiteDirectCursorDriver.java
new file mode 100644
index 0000000..2223d39
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -0,0 +1,87 @@
+/*
+ * 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.database.Cursor;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.os.CancellationSignal;
+
+/**
+ * A cursor driver that uses the given query directly.
+ *
+ * @hide
+ */
+public final class SQLiteDirectCursorDriver implements SQLiteCursorDriver {
+ private final SQLiteDatabase mDatabase;
+ private final String mEditTable;
+ private final String mSql;
+ private final CancellationSignal mCancellationSignal;
+ private SQLiteQuery mQuery;
+
+ public SQLiteDirectCursorDriver(SQLiteDatabase db, String sql, String editTable,
+ CancellationSignal cancellationSignal) {
+ mDatabase = db;
+ mEditTable = editTable;
+ mSql = sql;
+ mCancellationSignal = cancellationSignal;
+ }
+
+ public Cursor query(CursorFactory factory, String[] selectionArgs) {
+ final SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, mCancellationSignal);
+ final Cursor cursor;
+ try {
+ query.bindAllArgsAsStrings(selectionArgs);
+
+ if (factory == null) {
+ cursor = new SQLiteCursor(this, mEditTable, query);
+ } else {
+ cursor = factory.newCursor(mDatabase, this, mEditTable, query);
+ }
+ } catch (RuntimeException ex) {
+ query.close();
+ throw ex;
+ }
+
+ mQuery = query;
+ return cursor;
+ }
+
+ public void cursorClosed() {
+ // Do nothing
+ }
+
+ public void setBindArguments(String[] bindArgs) {
+ mQuery.bindAllArgsAsStrings(bindArgs);
+ }
+
+ public void cursorDeactivated() {
+ // Do nothing
+ }
+
+ public void cursorRequeried(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteDirectCursorDriver: " + mSql;
+ }
+}
diff --git a/src/main/java/android/database/sqlite/SQLiteDiskIOException.java b/src/main/java/android/database/sqlite/SQLiteDiskIOException.java
new file mode 100644
index 0000000..651cc8d
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteDoneException.java b/src/main/java/android/database/sqlite/SQLiteDoneException.java
new file mode 100644
index 0000000..6322928
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteException.java b/src/main/java/android/database/sqlite/SQLiteException.java
new file mode 100644
index 0000000..949dcc4
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteException.java
@@ -0,0 +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;
+
+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/main/java/android/database/sqlite/SQLiteFullException.java b/src/main/java/android/database/sqlite/SQLiteFullException.java
new file mode 100644
index 0000000..31cba18
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteGlobal.java b/src/main/java/android/database/sqlite/SQLiteGlobal.java
new file mode 100644
index 0000000..5250d5a
--- /dev/null
+++ b/src/main/java/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.res.Resources;
+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("/data").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/main/java/android/database/sqlite/SQLiteMisuseException.java b/src/main/java/android/database/sqlite/SQLiteMisuseException.java
new file mode 100644
index 0000000..208233f
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteOpenHelper.java b/src/main/java/android/database/sqlite/SQLiteOpenHelper.java
new file mode 100644
index 0000000..c7ec3b4
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteOpenHelper.java
@@ -0,0 +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);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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) {
+ 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");
+ }
+
+ SQLiteDatabase db = mDatabase;
+ try {
+ mIsInitializing = true;
+
+ 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);
+
+ 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);
+ }
+
+ 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);
+
+ 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();
+ }
+ }
+ }
+
+ /**
+ * 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/main/java/android/database/sqlite/SQLiteOutOfMemoryException.java b/src/main/java/android/database/sqlite/SQLiteOutOfMemoryException.java
new file mode 100644
index 0000000..9e8c48e
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteProgram.java b/src/main/java/android/database/sqlite/SQLiteProgram.java
new file mode 100644
index 0000000..fea0d2a
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteProgram.java
@@ -0,0 +1,222 @@
+/*
+ * 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.DatabaseUtils;
+import android.os.CancellationSignal;
+
+import java.util.Arrays;
+
+/**
+ * A base class for compiled SQLite programs.
+ *
+ * This class is not thread-safe.
+ *
+ */
+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;
+ private final String[] mColumnNames;
+ private final int mNumParameters;
+ private final Object[] mBindArgs;
+
+ SQLiteProgram(SQLiteDatabase db, String sql, Object[] bindArgs,
+ 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;
+ }
+
+ if (bindArgs != null && bindArgs.length > mNumParameters) {
+ throw new IllegalArgumentException("Too many bind arguments. "
+ + bindArgs.length + " arguments were provided but the statement needs "
+ + mNumParameters + " arguments.");
+ }
+
+ if (mNumParameters != 0) {
+ mBindArgs = new Object[mNumParameters];
+ if (bindArgs != null) {
+ System.arraycopy(bindArgs, 0, mBindArgs, 0, bindArgs.length);
+ }
+ } else {
+ mBindArgs = null;
+ }
+ }
+
+ 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;
+ }
+}
diff --git a/src/main/java/android/database/sqlite/SQLiteQuery.java b/src/main/java/android/database/sqlite/SQLiteQuery.java
new file mode 100644
index 0000000..f2bcd60
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteQuery.java
@@ -0,0 +1,88 @@
+/*
+ * 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.CursorWindow;
+import android.os.CancellationSignal;
+import android.os.OperationCanceledException;
+import android.util.Log;
+
+/**
+ * Represents a query that reads the resulting rows into a {@link SQLiteQuery}.
+ * This class is used by {@link SQLiteCursor} and isn't useful itself.
+ *
+ * This class is not thread-safe.
+ *
+ */
+public final class SQLiteQuery extends SQLiteProgram {
+ private static final String TAG = "SQLiteQuery";
+
+ private final CancellationSignal mCancellationSignal;
+
+ SQLiteQuery(SQLiteDatabase db, String query, CancellationSignal cancellationSignal) {
+ super(db, query, null, cancellationSignal);
+
+ mCancellationSignal = cancellationSignal;
+ }
+
+ /**
+ * Reads rows into a buffer.
+ *
+ * @param window The window to fill into
+ * @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.
+ * @param countAllRows True to count all rows that the query would
+ * return regardless of whether they fit in the window.
+ * @return Number of rows that were enumerated. Might not be all rows
+ * unless countAllRows is true.
+ *
+ * @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();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteQuery: " + getSql();
+ }
+}
diff --git a/src/main/java/android/database/sqlite/SQLiteQueryBuilder.java b/src/main/java/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 0000000..bb35907
--- /dev/null
+++ b/src/main/java/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,652 @@
+/*
+ * 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.provider.BaseColumns;
+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)) {
+ continue;
+ }
+ projection[i++] = entry.getValue();
+ }
+ return projection;
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java b/src/main/java/android/database/sqlite/SQLiteReadOnlyDatabaseException.java
new file mode 100644
index 0000000..8063554
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteSession.java b/src/main/java/android/database/sqlite/SQLiteSession.java
new file mode 100644
index 0000000..21ea950
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteStatement.java b/src/main/java/android/database/sqlite/SQLiteStatement.java
new file mode 100644
index 0000000..16be5c3
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteStatementInfo.java b/src/main/java/android/database/sqlite/SQLiteStatementInfo.java
new file mode 100644
index 0000000..33cb3b5
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteTableLockedException.java b/src/main/java/android/database/sqlite/SQLiteTableLockedException.java
new file mode 100644
index 0000000..a1a41fe
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/sqlite/SQLiteTransactionListener.java b/src/main/java/android/database/sqlite/SQLiteTransactionListener.java
new file mode 100644
index 0000000..864546a
--- /dev/null
+++ b/src/main/java/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();
+}
From 0a707e03797870c9324a9b1917e7a2133388277f Mon Sep 17 00:00:00 2001
From: nbransby
Date: Thu, 11 Jan 2024 01:25:06 +1100
Subject: [PATCH 5/7] replace native functions in SQLiteConnection with calls
to native wrapper around org.xerial:sqlite-jdbc's NativeDB
---
build.gradle.kts | 1 +
.../database/sqlite/SQLiteConnection.java | 126 ++++-------
.../com/google/firebase/FirebasePlatform.kt | 2 +-
.../com/google/firebase/auth/FirebaseAuth.kt | 2 +-
src/main/java/org/sqlite/core/native.kt | 198 ++++++++++++++++++
src/test/kotlin/FirestoreTest.kt | 11 +-
6 files changed, 253 insertions(+), 87 deletions(-)
create mode 100644 src/main/java/org/sqlite/core/native.kt
diff --git a/build.gradle.kts b/build.gradle.kts
index 10c7790..91b2eba 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -164,6 +164,7 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
implementation("com.squareup.okhttp:okhttp:2.7.5")
+ implementation("org.xerial:sqlite-jdbc:3.44.1.0")
// firebase dependencies
implementation("android.arch.lifecycle:common:1.1.1")
implementation("io.grpc:grpc-protobuf-lite:1.52.1")
diff --git a/src/main/java/android/database/sqlite/SQLiteConnection.java b/src/main/java/android/database/sqlite/SQLiteConnection.java
index b3167e1..5866ea3 100644
--- a/src/main/java/android/database/sqlite/SQLiteConnection.java
+++ b/src/main/java/android/database/sqlite/SQLiteConnection.java
@@ -20,8 +20,6 @@
package android.database.sqlite;
-import android.database.sqlite.CloseGuard;
-
import android.database.Cursor;
import android.database.CursorWindow;
import android.database.DatabaseUtils;
@@ -33,12 +31,13 @@
import android.util.Log;
import android.util.LruCache;
import android.util.Printer;
+import org.sqlite.core.NativeDB;
+import org.sqlite.core.NativeKt;
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.
@@ -109,7 +108,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
private final OperationLog mRecentOperations = new OperationLog();
// The native SQLiteConnection pointer. (FOR INTERNAL USE ONLY)
- private long mConnectionPtr;
+ private NativeDB mConnectionPtr;
private boolean mOnlyAllowReadOnlyOperations;
@@ -118,49 +117,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
// 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(); }
+
+ public static boolean hasCodec(){ return NativeKt.HasCodec(); }
private SQLiteConnection(SQLiteConnectionPool pool,
SQLiteDatabaseConfiguration configuration,
@@ -178,7 +136,7 @@ private SQLiteConnection(SQLiteConnectionPool pool,
@Override
protected void finalize() throws Throwable {
try {
- if (mPool != null && mConnectionPtr != 0) {
+ if (mPool != null && mConnectionPtr != null) {
mPool.onConnectionLeaked();
}
@@ -211,7 +169,7 @@ void close() {
}
private void open() {
- mConnectionPtr = nativeOpen(mConfiguration.path, mConfiguration.openFlags,
+ mConnectionPtr = NativeKt.Open(mConfiguration.path, mConfiguration.openFlags,
mConfiguration.label,
SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME);
@@ -219,7 +177,7 @@ private void open() {
setForeignKeyModeFromConfiguration();
setJournalSizeLimit();
setAutoCheckpointInterval();
- if( !nativeHasCodec() ){
+ if(!NativeKt.HasCodec() ){
setWalModeFromConfiguration();
setLocaleFromConfiguration();
}
@@ -227,7 +185,7 @@ private void open() {
final int functionCount = mConfiguration.customFunctions.size();
for (int i = 0; i < functionCount; i++) {
SQLiteCustomFunction function = mConfiguration.customFunctions.get(i);
- nativeRegisterCustomFunction(mConnectionPtr, function);
+ NativeKt.RegisterCustomFunction(mConnectionPtr, function);
}
}
@@ -239,12 +197,12 @@ private void dispose(boolean finalized) {
mCloseGuard.close();
}
- if (mConnectionPtr != 0) {
+ if (mConnectionPtr != null) {
final int cookie = mRecentOperations.beginOperation("close", null, null);
try {
mPreparedStatementCache.evictAll();
- nativeClose(mConnectionPtr);
- mConnectionPtr = 0;
+ NativeKt.Close(mConnectionPtr);
+ mConnectionPtr = null;
} finally {
mRecentOperations.endOperation(cookie);
}
@@ -364,7 +322,7 @@ private void setLocaleFromConfiguration() {
// Register the localized collators.
final String newLocale = mConfiguration.locale.toString();
- nativeRegisterLocalizedCollators(mConnectionPtr, newLocale);
+ NativeKt.RegisterLocalizedCollators(mConnectionPtr, newLocale);
// If the database is read-only, we cannot modify the android metadata table
// or existing indexes.
@@ -390,7 +348,7 @@ private void setLocaleFromConfiguration() {
execute("DELETE FROM android_metadata", null, null);
execute("INSERT INTO android_metadata (locale) VALUES(?)",
new Object[] { newLocale }, null);
- execute("REINDEX LOCALIZED", null, null);
+// execute("REINDEX LOCALIZED", null, null);
success = true;
} finally {
execute(success ? "COMMIT" : "ROLLBACK", null, null);
@@ -402,7 +360,7 @@ private void setLocaleFromConfiguration() {
}
public void enableLocalizedCollators(){
- if( nativeHasCodec() ){
+ if( NativeKt.HasCodec() ){
setLocaleFromConfiguration();
}
}
@@ -416,7 +374,7 @@ void reconfigure(SQLiteDatabaseConfiguration configuration) {
for (int i = 0; i < functionCount; i++) {
SQLiteCustomFunction function = configuration.customFunctions.get(i);
if (!mConfiguration.customFunctions.contains(function)) {
- nativeRegisterCustomFunction(mConnectionPtr, function);
+ NativeKt.RegisterCustomFunction(mConnectionPtr, function);
}
}
@@ -516,14 +474,14 @@ public void prepare(String sql, SQLiteStatementInfo outStatementInfo) {
outStatementInfo.numParameters = statement.mNumParameters;
outStatementInfo.readOnly = statement.mReadOnly;
- final int columnCount = nativeGetColumnCount(
+ final int columnCount = NativeKt.GetColumnCount(
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(
+ outStatementInfo.columnNames[i] = NativeKt.GetColumnName(
mConnectionPtr, statement.mStatementPtr, i);
}
}
@@ -565,7 +523,7 @@ public void execute(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- nativeExecute(mConnectionPtr, statement.mStatementPtr);
+ NativeKt.Execute(mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
@@ -608,7 +566,7 @@ public long executeForLong(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- return nativeExecuteForLong(mConnectionPtr, statement.mStatementPtr);
+ return NativeKt.ExecuteForLong(mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
@@ -651,7 +609,7 @@ public String executeForString(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- return nativeExecuteForString(mConnectionPtr, statement.mStatementPtr);
+ return NativeKt.ExecuteForString(mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
}
@@ -697,7 +655,7 @@ public ParcelFileDescriptor executeForBlobFileDescriptor(String sql, Object[] bi
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- int fd = nativeExecuteForBlobFileDescriptor(
+ int fd = NativeKt.ExecuteForBlobFileDescriptor(
mConnectionPtr, statement.mStatementPtr);
return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
} finally {
@@ -744,7 +702,7 @@ public int executeForChangedRowCount(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- changedRows = nativeExecuteForChangedRowCount(
+ changedRows = NativeKt.ExecuteForChangedRowCount(
mConnectionPtr, statement.mStatementPtr);
return changedRows;
} finally {
@@ -792,7 +750,7 @@ public long executeForLastInsertedRowId(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- return nativeExecuteForLastInsertedRowId(
+ return NativeKt.ExecuteForLastInsertedRowId(
mConnectionPtr, statement.mStatementPtr);
} finally {
detachCancellationSignal(cancellationSignal);
@@ -855,7 +813,7 @@ public int executeForCursorWindow(String sql, Object[] bindArgs,
applyBlockGuardPolicy(statement);
attachCancellationSignal(cancellationSignal);
try {
- final long result = nativeExecuteForCursorWindow(
+ final long result = NativeKt.ExecuteForCursorWindow(
mConnectionPtr, statement.mStatementPtr, window,
startPos, requiredPos, countAllRows);
actualPos = (int)(result >> 32);
@@ -899,11 +857,11 @@ private PreparedStatement acquirePreparedStatement(String sql) {
skipCache = true;
}
- final long statementPtr = nativePrepareStatement(mConnectionPtr, sql);
+ final long statementPtr = NativeKt.PrepareStatement(mConnectionPtr, sql);
try {
- final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
+ final int numParameters = NativeKt.GetParameterCount(mConnectionPtr, statementPtr);
final int type = DatabaseUtils.getSqlStatementType(sql);
- final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
+ final boolean readOnly = NativeKt.IsReadOnly(mConnectionPtr, statementPtr);
statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly);
if (!skipCache && isCacheable(type)) {
mPreparedStatementCache.put(sql, statement);
@@ -913,7 +871,7 @@ private PreparedStatement acquirePreparedStatement(String sql) {
// 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);
+ NativeKt.FinalizeStatement(mConnectionPtr, statementPtr);
}
throw ex;
}
@@ -925,7 +883,7 @@ private void releasePreparedStatement(PreparedStatement statement) {
statement.mInUse = false;
if (statement.mInCache) {
try {
- nativeResetStatementAndClearBindings(mConnectionPtr, statement.mStatementPtr);
+ NativeKt.ResetStatementAndClearBindings(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,
@@ -945,7 +903,7 @@ private void releasePreparedStatement(PreparedStatement statement) {
}
private void finalizePreparedStatement(PreparedStatement statement) {
- nativeFinalizeStatement(mConnectionPtr, statement.mStatementPtr);
+ NativeKt.FinalizeStatement(mConnectionPtr, statement.mStatementPtr);
recyclePreparedStatement(statement);
}
@@ -956,7 +914,7 @@ private void attachCancellationSignal(CancellationSignal cancellationSignal) {
mCancellationSignalAttachCount += 1;
if (mCancellationSignalAttachCount == 1) {
// Reset cancellation flag before executing the statement.
- nativeResetCancel(mConnectionPtr, true /*cancelable*/);
+ NativeKt.ResetCancel(mConnectionPtr, true /*cancelable*/);
// After this point, onCancel() may be called concurrently.
cancellationSignal.setOnCancelListener(this);
@@ -974,7 +932,7 @@ private void detachCancellationSignal(CancellationSignal cancellationSignal) {
cancellationSignal.setOnCancelListener(null);
// Reset cancellation flag after executing the statement.
- nativeResetCancel(mConnectionPtr, false /*cancelable*/);
+ NativeKt.ResetCancel(mConnectionPtr, false /*cancelable*/);
}
}
}
@@ -986,7 +944,7 @@ private void detachCancellationSignal(CancellationSignal cancellationSignal) {
// that the SQLite connection is still alive.
@Override
public void onCancel() {
- nativeCancel(mConnectionPtr);
+ NativeKt.Cancel(mConnectionPtr);
}
private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
@@ -1005,28 +963,28 @@ private void bindArguments(PreparedStatement statement, Object[] bindArgs) {
final Object arg = bindArgs[i];
switch (DatabaseUtils.getTypeOfObject(arg)) {
case Cursor.FIELD_TYPE_NULL:
- nativeBindNull(mConnectionPtr, statementPtr, i + 1);
+ NativeKt.BindNull(mConnectionPtr, statementPtr, i + 1);
break;
case Cursor.FIELD_TYPE_INTEGER:
- nativeBindLong(mConnectionPtr, statementPtr, i + 1,
+ NativeKt.BindLong(mConnectionPtr, statementPtr, i + 1,
((Number)arg).longValue());
break;
case Cursor.FIELD_TYPE_FLOAT:
- nativeBindDouble(mConnectionPtr, statementPtr, i + 1,
+ NativeKt.BindDouble(mConnectionPtr, statementPtr, i + 1,
((Number)arg).doubleValue());
break;
case Cursor.FIELD_TYPE_BLOB:
- nativeBindBlob(mConnectionPtr, statementPtr, i + 1, (byte[])arg);
+ NativeKt.BindBlob(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,
+ NativeKt.BindLong(mConnectionPtr, statementPtr, i + 1,
((Boolean)arg).booleanValue() ? 1 : 0);
} else {
- nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
+ NativeKt.BindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
}
break;
}
@@ -1078,7 +1036,7 @@ public void dump(Printer printer, boolean verbose) {
void dumpUnsafe(Printer printer, boolean verbose) {
printer.println("Connection #" + mConnectionId + ":");
if (verbose) {
- printer.println(" connectionPtr: 0x" + Long.toHexString(mConnectionPtr));
+ printer.println(" connectionPtr: 0x" + Long.toHexString(mConnectionPtr.hashCode()));
}
printer.println(" isPrimaryConnection: " + mIsPrimaryConnection);
printer.println(" onlyAllowReadOnlyOperations: " + mOnlyAllowReadOnlyOperations);
@@ -1115,7 +1073,7 @@ String describeCurrentOperationUnsafe() {
*/
void collectDbStats(ArrayList dbStatsList) {
// Get information about the main database.
- int lookaside = nativeGetDbLookaside(mConnectionPtr);
+ int lookaside = NativeKt.GetDbLookaside(mConnectionPtr);
long pageCount = 0;
long pageSize = 0;
try {
diff --git a/src/main/java/com/google/firebase/FirebasePlatform.kt b/src/main/java/com/google/firebase/FirebasePlatform.kt
index 8fe28d5..4db9c84 100644
--- a/src/main/java/com/google/firebase/FirebasePlatform.kt
+++ b/src/main/java/com/google/firebase/FirebasePlatform.kt
@@ -23,5 +23,5 @@ abstract class FirebasePlatform {
abstract fun log(msg: String)
- open fun getDatabasePath(name: String): File = File(System.getProperty("java.io.tmpdir"))
+ open fun getDatabasePath(name: String): File = File("${System.getProperty("java.io.tmpdir")}${File.separatorChar}$name")
}
diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt
index f6df082..88e4ac4 100644
--- a/src/main/java/com/google/firebase/auth/FirebaseAuth.kt
+++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.kt
@@ -273,7 +273,6 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
}
override fun getAccessToken(forceRefresh: Boolean): Task {
- val source = TaskCompletionSource()
val user = user ?: return Tasks.forException(FirebaseNoSignedInUserException("Please sign in before trying to get a token."))
if (!forceRefresh && user.createdAt + user.expiresIn*1000 - 5*60*1000 > System.currentTimeMillis() ) {
@@ -281,6 +280,7 @@ class FirebaseAuth constructor(val app: FirebaseApp) : InternalAuthProvider {
return Tasks.forResult(GetTokenResult(user.idToken, user.claims))
}
// Log.i("FirebaseAuth", "Refreshing access token forceRefresh=$forceRefresh createdAt=${user.createdAt} expiresIn=${user.expiresIn}")
+ val source = TaskCompletionSource()
refreshToken(user, source) { GetTokenResult(it.idToken, user.claims) }
return source.task
}
diff --git a/src/main/java/org/sqlite/core/native.kt b/src/main/java/org/sqlite/core/native.kt
new file mode 100644
index 0000000..71b682b
--- /dev/null
+++ b/src/main/java/org/sqlite/core/native.kt
@@ -0,0 +1,198 @@
+package org.sqlite.core
+
+import android.database.CursorWindow
+import android.database.sqlite.SQLiteCustomFunction
+import android.database.sqlite.SQLiteDatabase
+import org.sqlite.Collation
+import org.sqlite.Function
+import org.sqlite.SQLiteConfig
+import org.sqlite.SQLiteOpenMode
+import java.text.Collator
+import java.util.*
+
+fun Open(path: String, openFlags: Int, label: String, enableTrace: Boolean, enableProfile: Boolean): NativeDB {
+ NativeDB.load()
+ val db = NativeDB(null, path, SQLiteConfig())
+ val flags = (0 .. 31).asSequence()
+ .map { 1 shl it }
+ .filter { it and openFlags > 0 }
+ .map {
+ when(it) {
+ SQLiteDatabase.CREATE_IF_NECESSARY -> SQLiteOpenMode.READWRITE.flag or SQLiteOpenMode.CREATE.flag
+ SQLiteDatabase.OPEN_READONLY -> {
+ db.config.isExplicitReadOnly = true
+ SQLiteOpenMode.READONLY.flag
+ }
+ SQLiteDatabase.OPEN_READWRITE -> SQLiteOpenMode.READWRITE.flag
+ else -> TODO("Unknown openFlag ${it.toString(16)}")
+ }
+ }
+ .reduce { acc, flag -> acc or flag }
+ db.open(path, flags)
+ return db
+}
+
+fun Close(connectionPtr: NativeDB) = connectionPtr._close()
+
+fun RegisterCustomFunction(
+ connectionPtr: NativeDB,
+ function: SQLiteCustomFunction
+) {
+ val callback = object : Function() {
+ override fun xFunc() {
+ val args = arrayOfNulls(args())
+ args.indices.forEach { args[it] = value_text(it) }
+// function.callback.callback(args)
+ }
+
+ }
+ connectionPtr.create_function(function.name, callback, function.numArgs, 0)
+}
+
+fun RegisterLocalizedCollators(connectionPtr: NativeDB, locale: String) {
+ val collator = Collator.getInstance(Locale.forLanguageTag(locale))
+ connectionPtr.create_collation(locale, object : Collation() {
+ override fun xCompare(str1: String?, str2: String?) = collator.compare(str1, str2)
+ })
+}
+
+fun PrepareStatement(connectionPtr: NativeDB, sql: String) =
+ connectionPtr.prepare_utf8(NativeDB.stringToUtf8ByteArray(sql))
+
+fun FinalizeStatement(connectionPtr: NativeDB, statementPtr: Long) = connectionPtr.finalize(statementPtr)
+
+fun GetParameterCount(connectionPtr: NativeDB, statementPtr: Long): Int = connectionPtr.bind_parameter_count(statementPtr)
+
+fun IsReadOnly(connectionPtr: NativeDB, statementPtr: Long): Boolean = connectionPtr.config.isExplicitReadOnly
+
+fun GetColumnCount(connectionPtr: NativeDB, statementPtr: Long): Int = connectionPtr.column_count(statementPtr)
+
+fun GetColumnName(connectionPtr: NativeDB, statementPtr: Long, index: Int): String? =
+ connectionPtr.column_name(statementPtr, index)
+
+fun BindNull(connectionPtr: NativeDB, statementPtr: Long, index: Int) = connectionPtr.bind_null(statementPtr, index)
+
+fun BindLong(connectionPtr: NativeDB, statementPtr: Long, index: Int, value: Long) =
+ connectionPtr.bind_long(statementPtr, index, value)
+
+fun BindDouble(connectionPtr: NativeDB, statementPtr: Long, index: Int, value: Double) =
+ connectionPtr.bind_double(statementPtr, index, value)
+
+fun BindString(connectionPtr: NativeDB, statementPtr: Long, index: Int, value: String) =
+ connectionPtr.bind_text(statementPtr, index, value)
+
+fun BindBlob(connectionPtr: NativeDB, statementPtr: Long, index: Int, value: ByteArray) =
+ connectionPtr.bind_blob(statementPtr, index, value)
+
+fun ResetStatementAndClearBindings(connectionPtr: NativeDB, statementPtr: Long) {
+ connectionPtr.reset(statementPtr)
+ connectionPtr.clear_bindings(statementPtr)
+}
+
+fun Execute(connectionPtr: NativeDB, statementPtr: Long) = connectionPtr.step(statementPtr)
+
+fun ExecuteForLong(connectionPtr: NativeDB, statementPtr: Long): Long {
+ connectionPtr.step(statementPtr)
+ return connectionPtr.column_long(statementPtr, 0)
+}
+
+fun ExecuteForString(connectionPtr: NativeDB, statementPtr: Long): String? {
+ connectionPtr.step(statementPtr)
+ return connectionPtr.column_text(statementPtr, 0)
+}
+
+fun GetDbLookaside(connectionPtr: NativeDB): Int = 0
+
+fun Cancel(connectionPtr: NativeDB): Nothing = TODO()
+
+fun ResetCancel(connectionPtr: NativeDB, cancelable: Boolean): Nothing = TODO()
+
+fun HasCodec(): Boolean = false
+
+fun ExecuteForBlobFileDescriptor(connectionPtr: NativeDB, statementPtr: Long): Int = TODO()
+
+fun ExecuteForChangedRowCount(connectionPtr: NativeDB, statementPtr: Long): Int {
+ connectionPtr.step(statementPtr)
+ return connectionPtr.changes().toInt()
+}
+
+fun ExecuteForLastInsertedRowId(connectionPtr: NativeDB, statementPtr: Long): Long {
+ connectionPtr.step(statementPtr)
+ return connectionPtr.column_long(statementPtr, 0)
+}
+
+fun ExecuteForCursorWindow(connectionPtr: NativeDB, statementPtr: Long, win: CursorWindow, startPos: Int, iRowRequired: Int, countAllRows: Boolean): Long {
+
+ /* Set the number of columns in the window */
+ if(!win.setNumColumns(connectionPtr.column_count(statementPtr))) return 0
+
+ var nRow = 0;
+ var iStart = startPos;
+ var bOk = true;
+
+ while(connectionPtr.step(statementPtr) == Codes.SQLITE_ROW) {
+ /* Only copy in rows that occur at or after row index iStart. */
+ if((nRow >= iStart) && bOk){
+ bOk = copyRowToWindow(connectionPtr, win, (nRow - iStart), statementPtr);
+ if(!bOk){
+ /* The CursorWindow object ran out of memory. If row iRowRequired was
+ ** not successfully added before this happened, clear the CursorWindow
+ ** and try to add the current row again. */
+ if( nRow<=iRowRequired ){
+ bOk = win.setNumColumns(connectionPtr.column_count(statementPtr));
+ if(!bOk){
+ connectionPtr.reset(statementPtr);
+ return 0;
+ }
+ iStart = nRow;
+ bOk = copyRowToWindow(connectionPtr, win, (nRow - iStart), statementPtr);
+ }
+
+ /* If the CursorWindow is still full and the countAllRows flag is not
+ ** set, break out of the loop here. If countAllRows is set, continue
+ ** so as to set variable nRow correctly. */
+ if( !bOk && !countAllRows ) break;
+ }
+ }
+
+ nRow++;
+ }
+
+ /* Finalize the statement. If this indicates an error occurred, throw an
+ ** SQLiteException exception. */
+ val rc = connectionPtr.reset(statementPtr);
+ if( rc!= Codes.SQLITE_OK ){
+ NativeDB.throwex(rc, connectionPtr.errmsg())
+ return 0;
+ }
+
+ return iStart.toLong() shl 32 or nRow.toLong();
+}
+
+/*
+** Append the contents of the row that SQL statement pStmt currently points to
+** to the CursorWindow object passed as the second argument. The CursorWindow
+** currently contains iRow rows. Return true on success or false if an error
+** occurs.
+*/
+fun copyRowToWindow(connectionPtr: NativeDB, win: CursorWindow, iRow: Int, statementPtr: Long): Boolean {
+ val nCol = connectionPtr.column_count(statementPtr);
+ val i = 0
+ var bOk = false
+
+ bOk = win.allocRow()
+ for(i in 0 until nCol){
+ when(val type = connectionPtr.column_type(statementPtr, i)) {
+ Codes.SQLITE_NULL -> win.putNull(iRow, i)
+ Codes.SQLITE_INTEGER -> win.putLong(connectionPtr.column_long(statementPtr, i), iRow, i)
+ Codes.SQLITE_FLOAT -> win.putDouble(connectionPtr.column_double(statementPtr, i), iRow, i)
+ Codes.SQLITE_TEXT -> win.putString(connectionPtr.column_text(statementPtr, i), iRow, i)
+ Codes.SQLITE_BLOB -> win.putBlob(connectionPtr.column_blob(statementPtr, i), iRow, i)
+ else -> TODO("Unknown column type: $type")
+ }
+
+ if(!bOk) win.freeLastRow()
+ }
+
+ return bOk;
+}
diff --git a/src/test/kotlin/FirestoreTest.kt b/src/test/kotlin/FirestoreTest.kt
index 6bc0dcb..971777a 100644
--- a/src/test/kotlin/FirestoreTest.kt
+++ b/src/test/kotlin/FirestoreTest.kt
@@ -6,8 +6,10 @@ import com.google.firebase.firestore.firestore
import com.google.firebase.initialize
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.tasks.await
+import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
+import java.io.File
class FirestoreTest {
@@ -19,6 +21,7 @@ class FirestoreTest {
override fun retrieve(key: String) = storage[key]
override fun clear(key: String) { storage.remove(key) }
override fun log(msg: String) = println(msg)
+ override fun getDatabasePath(name: String) = File("./build/$name")
})
val options = FirebaseOptions.Builder()
.setProjectId("my-firebase-project")
@@ -28,10 +31,16 @@ class FirestoreTest {
// setStorageBucket(...)
.build()
Firebase.initialize(Application(), options)
+ Firebase.firestore.disableNetwork()
}
@Test
fun testFirestore(): Unit = runBlocking {
- Firebase.firestore.document("sally/jim").get().await()
+ val data = Data("jim")
+ val doc = Firebase.firestore.document("sally/jim")
+ doc.set(data)
+ assertEquals(data, doc.get().await().toObject(Data::class.java))
}
+
+ data class Data(val name: String = "none")
}
\ No newline at end of file
From 34d7d4e2ad38fcbf0dd69e75f2f96c9539006041 Mon Sep 17 00:00:00 2001
From: nbransby
Date: Thu, 11 Jan 2024 01:26:05 +1100
Subject: [PATCH 6/7] add classes to android emulation layer for
android.database package
---
.../java/android/content/ContentResolver.java | 1 +
.../java/android/content/ContentValues.java | 602 +++++++++
.../java/android/database/AbstractCursor.java | 563 +++++++++
.../database/AbstractWindowedCursor.java | 204 +++
.../android/database/ContentObservable.java | 91 ++
.../android/database/CrossProcessCursor.java | 78 ++
src/main/java/android/database/Cursor.java | 524 ++++++++
.../CursorIndexOutOfBoundsException.java | 31 +
.../java/android/database/CursorWindow.java | 713 +++++++++++
.../android/database/DataSetObservable.java | 54 +
.../java/android/database/Observable.java | 83 ++
.../android/database/StaleDataException.java | 34 +
src/main/java/android/os/StatFs.java | 22 +
src/main/java/android/util/ArrayMap.java | 1094 +++++++++++++++++
.../java/android/util/MapCollections.java | 558 +++++++++
.../com/android/internal/util/ArrayUtils.java | 320 ++++-
src/main/java/libcore/util/EmptyArray.java | 85 ++
17 files changed, 5051 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/android/content/ContentValues.java
create mode 100644 src/main/java/android/database/AbstractCursor.java
create mode 100644 src/main/java/android/database/AbstractWindowedCursor.java
create mode 100644 src/main/java/android/database/ContentObservable.java
create mode 100644 src/main/java/android/database/CrossProcessCursor.java
create mode 100644 src/main/java/android/database/Cursor.java
create mode 100644 src/main/java/android/database/CursorIndexOutOfBoundsException.java
create mode 100644 src/main/java/android/database/CursorWindow.java
create mode 100644 src/main/java/android/database/DataSetObservable.java
create mode 100644 src/main/java/android/database/Observable.java
create mode 100644 src/main/java/android/database/StaleDataException.java
create mode 100644 src/main/java/android/os/StatFs.java
create mode 100644 src/main/java/android/util/ArrayMap.java
create mode 100644 src/main/java/android/util/MapCollections.java
create mode 100644 src/main/java/libcore/util/EmptyArray.java
diff --git a/src/main/java/android/content/ContentResolver.java b/src/main/java/android/content/ContentResolver.java
index 43ef088..aee556b 100644
--- a/src/main/java/android/content/ContentResolver.java
+++ b/src/main/java/android/content/ContentResolver.java
@@ -1,4 +1,5 @@
package android.content;
public class ContentResolver {
+
}
diff --git a/src/main/java/android/content/ContentValues.java b/src/main/java/android/content/ContentValues.java
new file mode 100644
index 0000000..725b992
--- /dev/null
+++ b/src/main/java/android/content/ContentValues.java
@@ -0,0 +1,602 @@
+/*
+ * 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.content;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.internal.util.Preconditions;
+
+import java.util.*;
+
+/**
+ * This class is used to store a set of values that the {@link ContentResolver}
+ * can process.
+ */
+public final class ContentValues implements Parcelable {
+ public static final String TAG = "ContentValues";
+
+ /**
+ * @hide
+ * @deprecated kept around for lame people doing reflection
+ */
+ @Deprecated
+ private HashMap mValues;
+
+ private final ArrayMap mMap;
+
+ /**
+ * Creates an empty set of values using the default initial size
+ */
+ public ContentValues() {
+ mMap = new ArrayMap<>();
+ }
+
+ /**
+ * Creates an empty set of values using the given initial size
+ *
+ * @param size the initial size of the set of values
+ */
+ public ContentValues(int size) {
+ Preconditions.checkArgumentNonnegative(size);
+ mMap = new ArrayMap<>(size);
+ }
+
+ /**
+ * Creates a set of values copied from the given set
+ *
+ * @param from the values to copy
+ */
+ public ContentValues(ContentValues from) {
+ Objects.requireNonNull(from);
+ mMap = new ArrayMap<>(from.mMap);
+ }
+
+ /**
+ * @hide
+ * @deprecated kept around for lame people doing reflection
+ */
+ @Deprecated
+ private ContentValues(HashMap from) {
+ mMap = new ArrayMap<>();
+ mMap.putAll(from);
+ }
+
+ /** {@hide} */
+ private ContentValues(Parcel in) {
+ mMap = new ArrayMap<>(in.readInt());
+ in.readArrayMap(mMap, null);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (!(object instanceof ContentValues)) {
+ return false;
+ }
+ return mMap.equals(((ContentValues) object).mMap);
+ }
+
+ /** {@hide} */
+ public ArrayMap getValues() {
+ return mMap;
+ }
+
+ @Override
+ public int hashCode() {
+ return mMap.hashCode();
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, String value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds all values from the passed in ContentValues.
+ *
+ * @param other the ContentValues from which to copy
+ */
+ public void putAll(ContentValues other) {
+ mMap.putAll(other.mMap);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Byte value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Short value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Integer value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Long value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Float value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Double value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, Boolean value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a value to the set.
+ *
+ * @param key the name of the value to put
+ * @param value the data for the value to put
+ */
+ public void put(String key, byte[] value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Adds a null value to the set.
+ *
+ * @param key the name of the value to make null
+ */
+ public void putNull(String key) {
+ mMap.put(key, null);
+ }
+
+ /** {@hide} */
+ public void putObject(@Nullable String key, @Nullable Object value) {
+ if (value == null) {
+ putNull(key);
+ } else if (value instanceof String) {
+ put(key, (String) value);
+ } else if (value instanceof Byte) {
+ put(key, (Byte) value);
+ } else if (value instanceof Short) {
+ put(key, (Short) value);
+ } else if (value instanceof Integer) {
+ put(key, (Integer) value);
+ } else if (value instanceof Long) {
+ put(key, (Long) value);
+ } else if (value instanceof Float) {
+ put(key, (Float) value);
+ } else if (value instanceof Double) {
+ put(key, (Double) value);
+ } else if (value instanceof Boolean) {
+ put(key, (Boolean) value);
+ } else if (value instanceof byte[]) {
+ put(key, (byte[]) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + value.getClass());
+ }
+ }
+
+ /**
+ * Returns the number of values.
+ *
+ * @return the number of values
+ */
+ public int size() {
+ return mMap.size();
+ }
+
+ /**
+ * Indicates whether this collection is empty.
+ *
+ * @return true iff size == 0
+ */
+ public boolean isEmpty() {
+ return mMap.isEmpty();
+ }
+
+ /**
+ * Remove a single value.
+ *
+ * @param key the name of the value to remove
+ */
+ public void remove(String key) {
+ mMap.remove(key);
+ }
+
+ /**
+ * Removes all values.
+ */
+ public void clear() {
+ mMap.clear();
+ }
+
+ /**
+ * Returns true if this object has the named value.
+ *
+ * @param key the value to check for
+ * @return {@code true} if the value is present, {@code false} otherwise
+ */
+ public boolean containsKey(String key) {
+ return mMap.containsKey(key);
+ }
+
+ /**
+ * Gets a value. Valid value types are {@link String}, {@link Boolean},
+ * {@link Number}, and {@code byte[]} implementations.
+ *
+ * @param key the value to get
+ * @return the data for the value, or {@code null} if the value is missing or if {@code null}
+ * was previously added with the given {@code key}
+ */
+ public Object get(String key) {
+ return mMap.get(key);
+ }
+
+ /**
+ * Gets a value and converts it to a String.
+ *
+ * @param key the value to get
+ * @return the String for the value
+ */
+ public String getAsString(String key) {
+ Object value = mMap.get(key);
+ return value != null ? value.toString() : null;
+ }
+
+ /**
+ * Gets a value and converts it to a Long.
+ *
+ * @param key the value to get
+ * @return the Long value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Long getAsLong(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).longValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Long.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Long: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to an Integer.
+ *
+ * @param key the value to get
+ * @return the Integer value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Integer getAsInteger(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).intValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Integer.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Integer: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Short.
+ *
+ * @param key the value to get
+ * @return the Short value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Short getAsShort(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).shortValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Short.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Short value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Short: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Byte.
+ *
+ * @param key the value to get
+ * @return the Byte value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Byte getAsByte(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).byteValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Byte.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Byte value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Byte: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Double.
+ *
+ * @param key the value to get
+ * @return the Double value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Double getAsDouble(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).doubleValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Double.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Double value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Double: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Float.
+ *
+ * @param key the value to get
+ * @return the Float value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Float getAsFloat(String key) {
+ Object value = mMap.get(key);
+ try {
+ return value != null ? ((Number) value).floatValue() : null;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ try {
+ return Float.valueOf(value.toString());
+ } catch (NumberFormatException e2) {
+ Log.e(TAG, "Cannot parse Float value for " + value + " at key " + key);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Float: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value and converts it to a Boolean.
+ *
+ * @param key the value to get
+ * @return the Boolean value, or {@code null} if the value is missing or cannot be converted
+ */
+ public Boolean getAsBoolean(String key) {
+ Object value = mMap.get(key);
+ try {
+ return (Boolean) value;
+ } catch (ClassCastException e) {
+ if (value instanceof CharSequence) {
+ // Note that we also check against 1 here because SQLite's internal representation
+ // for booleans is an integer with a value of 0 or 1. Without this check, boolean
+ // values obtained via DatabaseUtils#cursorRowToContentValues will always return
+ // false.
+ return Boolean.valueOf(value.toString()) || "1".equals(value);
+ } else if (value instanceof Number) {
+ return ((Number) value).intValue() != 0;
+ } else {
+ Log.e(TAG, "Cannot cast value for " + key + " to a Boolean: " + value, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Gets a value that is a byte array. Note that this method will not convert
+ * any other types to byte arrays.
+ *
+ * @param key the value to get
+ * @return the {@code byte[]} value, or {@code null} is the value is missing or not a
+ * {@code byte[]}
+ */
+ public byte[] getAsByteArray(String key) {
+ Object value = mMap.get(key);
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a set of all of the keys and values
+ *
+ * @return a set of all of the keys and values
+ */
+ public Set> valueSet() {
+ return mMap.entrySet();
+ }
+
+ /**
+ * Returns a set of all of the keys
+ *
+ * @return a set of all of the keys
+ */
+ public Set keySet() {
+ return mMap.keySet();
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ @Override
+ public ContentValues createFromParcel(Parcel in) {
+ return new ContentValues(in);
+ }
+
+ @Override
+ public ContentValues[] newArray(int size) {
+ return new ContentValues[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeInt(mMap.size());
+ parcel.writeArrayMap(mMap);
+ }
+
+ /**
+ * Unsupported, here until we get proper bulk insert APIs.
+ * {@hide}
+ */
+ @Deprecated
+ public void putStringArrayList(String key, ArrayList value) {
+ mMap.put(key, value);
+ }
+
+ /**
+ * Unsupported, here until we get proper bulk insert APIs.
+ * {@hide}
+ */
+ @SuppressWarnings("unchecked")
+ @Deprecated
+ public ArrayList getStringArrayList(String key) {
+ return (ArrayList) mMap.get(key);
+ }
+
+ /**
+ * Returns a string containing a concise, human-readable description of this object.
+ * @return a printable representation of this object.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (String name : mMap.keySet()) {
+ String value = getAsString(name);
+ if (sb.length() > 0) sb.append(" ");
+ sb.append(name + "=" + value);
+ }
+ return sb.toString();
+ }
+
+ /** {@hide} */
+ public static boolean isSupportedValue(Object value) {
+ if (value == null) {
+ return true;
+ } else if (value instanceof String) {
+ return true;
+ } else if (value instanceof Byte) {
+ return true;
+ } else if (value instanceof Short) {
+ return true;
+ } else if (value instanceof Integer) {
+ return true;
+ } else if (value instanceof Long) {
+ return true;
+ } else if (value instanceof Float) {
+ return true;
+ } else if (value instanceof Double) {
+ return true;
+ } else if (value instanceof Boolean) {
+ return true;
+ } else if (value instanceof byte[]) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/database/AbstractCursor.java b/src/main/java/android/database/AbstractCursor.java
new file mode 100644
index 0000000..dad233c
--- /dev/null
+++ b/src/main/java/android/database/AbstractCursor.java
@@ -0,0 +1,563 @@
+/*
+ * 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.annotation.NonNull;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import kotlin.NotImplementedError;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 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 = null;//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();
+ throw new NotImplementedError();
+
+ }
+
+ @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;
+ throw new NotImplementedError();
+ }
+
+ @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);
+// }
+// }
+// }
+ throw new NotImplementedError();
+ }
+
+ /**
+ * 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(@NonNull ContentResolver cr, @NonNull List notifyUris) {
+// Objects.requireNonNull(cr);
+// Objects.requireNonNull(notifyUris);
+//
+// setNotificationUris(cr, notifyUris, cr.getUserId(), true);
+ throw new NotImplementedError();
+ }
+
+ /**
+ * 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;
+// }
+// }
+ throw new NotImplementedError();
+
+ }
+
+ @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;
+ throw new NotImplementedError();
+ }
+
+ @Override
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public Bundle respond(Bundle extras) {
+// return Bundle.EMPTY;
+ throw new NotImplementedError();
+ }
+
+ /**
+ * @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());
+ }
+ }
+
+ @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/main/java/android/database/AbstractWindowedCursor.java b/src/main/java/android/database/AbstractWindowedCursor.java
new file mode 100644
index 0000000..d306bf2
--- /dev/null
+++ b/src/main/java/android/database/AbstractWindowedCursor.java
@@ -0,0 +1,204 @@
+/*
+ * 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 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("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/main/java/android/database/ContentObservable.java b/src/main/java/android/database/ContentObservable.java
new file mode 100644
index 0000000..a57ec78
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/CrossProcessCursor.java b/src/main/java/android/database/CrossProcessCursor.java
new file mode 100644
index 0000000..de8a7b7
--- /dev/null
+++ b/src/main/java/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/main/java/android/database/Cursor.java b/src/main/java/android/database/Cursor.java
new file mode 100644
index 0000000..0e73189
--- /dev/null
+++ b/src/main/java/android/database/Cursor.java
@@ -0,0 +1,524 @@
+/*
+ * 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.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+
+import java.io.Closeable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This interface provides random read-write access to the result set returned
+ * by a database query.
+ *
+ * Cursor implementations are not required to be synchronized so code using a Cursor from multiple
+ * threads should perform its own synchronization when using the Cursor.
+ *
+ * Implementations should subclass {@link AbstractCursor}.
+ *
+ */
+public interface Cursor extends Closeable {
+ /*
+ * Values returned by {@link #getType(int)}.
+ * These should be consistent with the corresponding types defined in CursorWindow.h
+ */
+ /** Value returned by {@link #getType(int)} if the specified column is null */
+ static final int FIELD_TYPE_NULL = 0;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is integer */
+ static final int FIELD_TYPE_INTEGER = 1;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is float */
+ static final int FIELD_TYPE_FLOAT = 2;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is string */
+ static final int FIELD_TYPE_STRING = 3;
+
+ /** Value returned by {@link #getType(int)} if the specified column type is blob */
+ static final int FIELD_TYPE_BLOB = 4;
+
+ /** @hide */
+ @IntDef(prefix = { "FIELD_TYPE_" }, value = {
+ FIELD_TYPE_NULL,
+ FIELD_TYPE_INTEGER,
+ FIELD_TYPE_FLOAT,
+ FIELD_TYPE_STRING,
+ FIELD_TYPE_BLOB,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface FieldType {}
+
+ /**
+ * Returns the numbers of rows in the cursor.
+ *
+ * @return the number of rows in the cursor.
+ */
+ @IntRange(from = 0) int getCount();
+
+ /**
+ * Returns the current position of the cursor in the row set.
+ * The value is zero-based. When the row set is first returned the cursor
+ * will be at positon -1, which is before the first row. After the
+ * last row is returned another call to next() will leave the cursor past
+ * the last entry, at a position of count().
+ *
+ * @return the current cursor position.
+ */
+ @IntRange(from = -1) int getPosition();
+
+ /**
+ * Move the cursor by a relative amount, forward or backward, from the
+ * current position. Positive offsets move forwards, negative offsets move
+ * backwards. If the final position is outside of the bounds of the result
+ * set then the resultant position will be pinned to -1 or count() depending
+ * on whether the value is off the front or end of the set, respectively.
+ *
+ *
This method will return true if the requested destination was
+ * reachable, otherwise, it returns false. For example, if the cursor is at
+ * currently on the second entry in the result set and move(-5) is called,
+ * the position will be pinned at -1, and false will be returned.
+ *
+ * @param offset the offset to be applied from the current position.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean move(int offset);
+
+ /**
+ * Move the cursor to an absolute position. The valid
+ * range of values is -1 <= position <= count.
+ *
+ *
This method will return true if the request destination was reachable,
+ * otherwise, it returns false.
+ *
+ * @param position the zero-based position to move to.
+ * @return whether the requested move fully succeeded.
+ */
+ boolean moveToPosition(@IntRange(from = -1) int position);
+
+ /**
+ * Move the cursor to the first row.
+ *
+ *
This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToFirst();
+
+ /**
+ * Move the cursor to the last row.
+ *
+ *
This method will return false if the cursor is empty.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToLast();
+
+ /**
+ * Move the cursor to the next row.
+ *
+ *
This method will return false if the cursor is already past the
+ * last entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToNext();
+
+ /**
+ * Move the cursor to the previous row.
+ *
+ *
This method will return false if the cursor is already before the
+ * first entry in the result set.
+ *
+ * @return whether the move succeeded.
+ */
+ boolean moveToPrevious();
+
+ /**
+ * Returns whether the cursor is pointing to the first row.
+ *
+ * @return whether the cursor is pointing at the first entry.
+ */
+ boolean isFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the last row.
+ *
+ * @return whether the cursor is pointing at the last entry.
+ */
+ boolean isLast();
+
+ /**
+ * Returns whether the cursor is pointing to the position before the first
+ * row.
+ *
+ * @return whether the cursor is before the first result.
+ */
+ boolean isBeforeFirst();
+
+ /**
+ * Returns whether the cursor is pointing to the position after the last
+ * row.
+ *
+ * @return whether the cursor is after the last result.
+ */
+ boolean isAfterLast();
+
+ /**
+ * Returns the zero-based index for the given column name, or -1 if the column doesn't exist.
+ * If you expect the column to exist use {@link #getColumnIndexOrThrow(String)} instead, which
+ * will make the error more clear.
+ *
+ * @param columnName the name of the target column.
+ * @return the zero-based column index for the given column name, or -1 if
+ * the column name does not exist.
+ * @see #getColumnIndexOrThrow(String)
+ */
+ @IntRange(from = -1) int getColumnIndex(String columnName);
+
+ /**
+ * Returns the zero-based index for the given column name, or throws
+ * {@link IllegalArgumentException} if the column doesn't exist. If you're not sure if
+ * a column will exist or not use {@link #getColumnIndex(String)} and check for -1, which
+ * is more efficient than catching the exceptions.
+ *
+ * @param columnName the name of the target column.
+ * @return the zero-based column index for the given column name
+ * @see #getColumnIndex(String)
+ * @throws IllegalArgumentException if the column does not exist
+ */
+ @IntRange(from = 0) int getColumnIndexOrThrow(String columnName)
+ throws IllegalArgumentException;
+
+ /**
+ * Returns the column name at the given zero-based column index.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the column name for the given column index.
+ */
+ String getColumnName(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns a string array holding the names of all of the columns in the
+ * result set in the order in which they were listed in the result.
+ *
+ * @return the names of the columns returned in this query.
+ */
+ String[] getColumnNames();
+
+ /**
+ * Return total number of columns
+ * @return number of columns
+ */
+ @IntRange(from = 0) int getColumnCount();
+
+ /**
+ * Returns the value of the requested column as a byte array.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null or the column type is not a blob type is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a byte array.
+ */
+ byte[] getBlob(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a String.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null or the column type is not a string type is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a String.
+ */
+ String getString(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Retrieves the requested column text and stores it in the buffer provided.
+ * If the buffer size is not sufficient, a new char buffer will be allocated
+ * and assigned to CharArrayBuffer.data
+ * @param columnIndex the zero-based index of the target column.
+ * if the target column is null, return buffer
+ * @param buffer the buffer to copy the text into.
+ */
+ void copyStringToBuffer(@IntRange(from = 0) int columnIndex, CharArrayBuffer buffer);
+
+ /**
+ * Returns the value of the requested column as a short.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [Short.MIN_VALUE,
+ * Short.MAX_VALUE] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a short.
+ */
+ short getShort(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns the value of the requested column as an int.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [Integer.MIN_VALUE,
+ * Integer.MAX_VALUE] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as an int.
+ */
+ int getInt(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a long.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null, the column type is not an integral type, or the
+ * integer value is outside the range [Long.MIN_VALUE,
+ * Long.MAX_VALUE] is implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a long.
+ */
+ long getLong(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a float.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null, the column type is not a floating-point type, or the
+ * floating-point value is not representable as a float value is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a float.
+ */
+ float getFloat(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns the value of the requested column as a double.
+ *
+ *
The result and whether this method throws an exception when the
+ * column value is null, the column type is not a floating-point type, or the
+ * floating-point value is not representable as a double value is
+ * implementation-defined.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return the value of that column as a double.
+ */
+ double getDouble(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns data type of the given column's value.
+ * The preferred type of the column is returned but the data may be converted to other types
+ * as documented in the get-type methods such as {@link #getInt(int)}, {@link #getFloat(int)}
+ * etc.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return column value type
+ */
+ @FieldType int getType(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Returns true if the value in the indicated column is null.
+ *
+ * @param columnIndex the zero-based index of the target column.
+ * @return whether the column value is null.
+ */
+ boolean isNull(@IntRange(from = 0) int columnIndex);
+
+ /**
+ * Deactivates the Cursor, making all calls on it fail until {@link #requery} is called.
+ * Inactive Cursors use fewer resources than active Cursors.
+ * Calling {@link #requery} will make the cursor active again.
+ * @deprecated Since {@link #requery()} is deprecated, so too is this.
+ */
+ @Deprecated
+ void deactivate();
+
+ /**
+ * Performs the query that created the cursor again, refreshing its
+ * contents. This may be done at any time, including after a call to {@link
+ * #deactivate}.
+ *
+ * Since this method could execute a query on the database and potentially take
+ * a while, it could cause ANR if it is called on Main (UI) thread.
+ * A warning is printed if this method is being executed on Main thread.
+ *
+ * @return true if the requery succeeded, false if not, in which case the
+ * cursor becomes invalid.
+ * @deprecated Don't use this. Just request a new cursor, so you can do this
+ * asynchronously and update your list view once the new cursor comes back.
+ */
+ @Deprecated
+ boolean requery();
+
+ /**
+ * Closes the Cursor, releasing all of its resources and making it completely invalid.
+ * Unlike {@link #deactivate()} a call to {@link #requery()} will not make the Cursor valid
+ * again.
+ */
+ void close();
+
+ /**
+ * return true if the cursor is closed
+ * @return true if the cursor is closed.
+ */
+ boolean isClosed();
+
+ /**
+ * Register an observer that is called when changes happen to the content backing this cursor.
+ * Typically the data set won't change until {@link #requery()} is called.
+ *
+ * @param observer the object that gets notified when the content backing the cursor changes.
+ * @see #unregisterContentObserver(ContentObserver)
+ */
+ void registerContentObserver(ContentObserver observer);
+
+ /**
+ * Unregister an observer that has previously been registered with this
+ * cursor via {@link #registerContentObserver}.
+ *
+ * @param observer the object to unregister.
+ * @see #registerContentObserver(ContentObserver)
+ */
+ void unregisterContentObserver(ContentObserver observer);
+
+ /**
+ * Register an observer that is called when changes happen to the contents
+ * of the this cursors data set, for example, when the data set is changed via
+ * {@link #requery()}, {@link #deactivate()}, or {@link #close()}.
+ *
+ * @param observer the object that gets notified when the cursors data set changes.
+ * @see #unregisterDataSetObserver(DataSetObserver)
+ */
+ void registerDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Unregister an observer that has previously been registered with this
+ * cursor via {@link #registerContentObserver}.
+ *
+ * @param observer the object to unregister.
+ * @see #registerDataSetObserver(DataSetObserver)
+ */
+ void unregisterDataSetObserver(DataSetObserver observer);
+
+ /**
+ * Register to watch a content URI for changes. This can be the URI of a specific data row (for
+ * example, "content://my_provider_type/23"), or a a generic URI for a content type.
+ *
+ *
Calling this overrides any previous call to
+ * {@link #setNotificationUris(ContentResolver, List)}.
+ *
+ * @param cr The content resolver from the caller's context. The listener attached to
+ * this resolver will be notified.
+ * @param uri The content URI to watch.
+ */
+ void setNotificationUri(ContentResolver cr, Uri uri);
+
+ /**
+ * Similar to {@link #setNotificationUri(ContentResolver, Uri)}, except this version allows
+ * to watch multiple content URIs for changes.
+ *
+ *
If this is not implemented, this is equivalent to calling
+ * {@link #setNotificationUri(ContentResolver, Uri)} with the first URI in {@code uris}.
+ *
+ *
Calling this overrides any previous call to
+ * {@link #setNotificationUri(ContentResolver, Uri)}.
+ *
+ * @param cr The content resolver from the caller's context. The listener attached to
+ * this resolver will be notified.
+ * @param uris The content URIs to watch.
+ */
+ default void setNotificationUris(@NonNull ContentResolver cr, @NonNull List uris) {
+ setNotificationUri(cr, uris.get(0));
+ }
+
+ /**
+ * Return the URI at which notifications of changes in this Cursor's data
+ * will be delivered, as previously set by {@link #setNotificationUri}.
+ * @return Returns a URI that can be used with
+ * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
+ * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
+ * data. May be null if no notification URI has been set.
+ */
+ Uri getNotificationUri();
+
+ /**
+ * Return the URIs at which notifications of changes in this Cursor's data
+ * will be delivered, as previously set by {@link #setNotificationUris}.
+ *
+ *
If this is not implemented, this is equivalent to calling {@link #getNotificationUri()}.
+ *
+ * @return Returns URIs that can be used with
+ * {@link ContentResolver#registerContentObserver(android.net.Uri, boolean, ContentObserver)
+ * ContentResolver.registerContentObserver} to find out about changes to this Cursor's
+ * data. May be null if no notification URI has been set.
+ */
+ default @Nullable List getNotificationUris() {
+ final Uri notifyUri = getNotificationUri();
+ return notifyUri == null ? null : Arrays.asList(notifyUri);
+ }
+
+ /**
+ * 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();
+
+ /**
+ * Sets a {@link Bundle} that will be returned by {@link #getExtras()}.
+ *
+ * @param extras {@link Bundle} to set, or null to set an empty bundle.
+ */
+ void setExtras(Bundle extras);
+
+ /**
+ * Returns a bundle of extra values. This is an optional way for cursors to provide out-of-band
+ * metadata to their users. One use of this is for reporting on the progress of network requests
+ * that are required to fetch data for the cursor.
+ *
+ *
These values may only change when requery is called.
+ * @return cursor-defined values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY} if there
+ * are no values. Never null.
+ */
+ Bundle getExtras();
+
+ /**
+ * This is an out-of-band way for the the user of a cursor to communicate with the cursor. The
+ * structure of each bundle is entirely defined by the cursor.
+ *
+ *
One use of this is to tell a cursor that it should retry its network request after it
+ * reported an error.
+ * @param extras extra values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY}.
+ * Never null.
+ * @return extra values, or {@link android.os.Bundle#EMPTY Bundle.EMPTY}.
+ * Never null.
+ */
+ Bundle respond(Bundle extras);
+}
\ No newline at end of file
diff --git a/src/main/java/android/database/CursorIndexOutOfBoundsException.java b/src/main/java/android/database/CursorIndexOutOfBoundsException.java
new file mode 100644
index 0000000..dc0c413
--- /dev/null
+++ b/src/main/java/android/database/CursorIndexOutOfBoundsException.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+/**
+ * An exception indicating that a cursor is out of bounds.
+ */
+public class CursorIndexOutOfBoundsException extends IndexOutOfBoundsException {
+
+ public CursorIndexOutOfBoundsException(int index, int size) {
+ super("Index " + index + " requested, with a size of " + size);
+ }
+
+ public CursorIndexOutOfBoundsException(String message) {
+ super(message);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/database/CursorWindow.java b/src/main/java/android/database/CursorWindow.java
new file mode 100644
index 0000000..ee31de5
--- /dev/null
+++ b/src/main/java/android/database/CursorWindow.java
@@ -0,0 +1,713 @@
+/*
+ * 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.annotation.BytesLong;
+import android.annotation.IntRange;
+import android.database.sqlite.SQLiteClosable;
+import android.database.sqlite.SQLiteException;
+import android.os.Parcel;
+import com.google.common.base.Preconditions;
+import org.sqlite.core.Codes;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * 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 {
+
+ private final List rows;
+ private int numColumns;
+
+ private static final String STATS_TAG = "CursorWindowStats";
+
+ // This static member will be evaluated when first used.
+ private static int sCursorWindowSize = -1;
+
+ private int mStartPos;
+ private final String mName;
+
+ /**
+ * 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, @BytesLong long windowSizeBytes) {
+ if (windowSizeBytes < 0) {
+ throw new IllegalArgumentException("Window size cannot be less than 0");
+ }
+ mStartPos = 0;
+ mName = name != null && !name.isEmpty() ? name : "";
+ this.rows = new ArrayList();
+ }
+
+ /**
+ * 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);
+ }
+
+// private CursorWindow(Parcel source) {
+// mStartPos = source.readInt();
+// mWindowPtr = nativeCreateFromParcel(source);
+// if (mWindowPtr == 0) {
+// throw new AssertionError(); // Not possible, the native code won't return it.
+// }
+// mName = nativeGetName(mWindowPtr);
+// mCloseGuard.open("CursorWindow.close");
+// }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void dispose() {
+ rows.clear();
+ }
+
+ /**
+ * 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() {
+ rows.clear();
+ }
+
+ /**
+ * 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 @IntRange(from = 0) 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(@IntRange(from = 0) int pos) {
+ mStartPos = pos;
+ }
+
+ /**
+ * Gets the number of rows in this window.
+ *
+ * @return The number of rows in this cursor window.
+ */
+ public @IntRange(from = 0) int getNumRows() {
+ return rows.size();
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int columnNum) {
+ this.numColumns = columnNum;
+ return true;
+ }
+
+ /**
+ * 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() {
+ rows.add(new Row(numColumns));
+ return true;
+ }
+
+ /**
+ * Frees the last row in this cursor window.
+ */
+ public void freeLastRow(){
+ rows.remove(rows.size()-1);
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ int type = getType(row, column);
+ return type == Cursor.FIELD_TYPE_STRING || type == Cursor.FIELD_TYPE_NULL;
+ }
+
+ private Value value(int rowN, int colN) {
+ Row row = rows.get(rowN);
+ if (row == null) {
+ throw new IllegalArgumentException("Bad row number: " + rowN + ", count: " + rows.size());
+ }
+ return row.get(colN);
+ }
+
+ private boolean putValue(Value value, int rowN, int colN) {
+ return rows.get(rowN).set(colN, value);
+ }
+
+ /**
+ * 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 @Cursor.FieldType int getType(@IntRange(from = 0) int row,
+ @IntRange(from = 0) int column) {
+ return value(row - mStartPos, column).type;
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ Value value = value(row - mStartPos, column);
+
+ switch (value.type) {
+ case Cursor.FIELD_TYPE_NULL:
+ return null;
+ case Cursor.FIELD_TYPE_BLOB:
+ // This matches Android's behavior, which does not match the SQLite spec
+ byte[] blob = (byte[])value.value;
+ return blob == null ? new byte[]{} : blob;
+ case Cursor.FIELD_TYPE_STRING:
+ // Matches the Android behavior to contain a zero-byte at the end
+ byte[] stringBytes = ((String) value.value).getBytes(UTF_8);
+ return Arrays.copyOf(stringBytes, stringBytes.length + 1);
+ default:
+ throw new android.database.sqlite.SQLiteException(
+ "Getting blob when column is non-blob. Row " + row + ", col " + column);
+ } }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ Value val = value(row - mStartPos, column);
+ if (val.type == Cursor.FIELD_TYPE_BLOB) {
+ throw new android.database.sqlite.SQLiteException(
+ "Getting string when column is blob. Row " + row + ", col " + column);
+ }
+ Object value = val.value;
+ return value == null ? null : String.valueOf(value);
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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();
+// }
+// }
+
+ private Number getNumber(int row, int column) {
+ Value value = value(row, column);
+ switch (value.type) {
+ case Cursor.FIELD_TYPE_NULL:
+ case Codes.SQLITE_NULL:
+ return 0;
+ case Cursor.FIELD_TYPE_INTEGER:
+ case Cursor.FIELD_TYPE_FLOAT:
+ return (Number) value.value;
+ case Cursor.FIELD_TYPE_STRING: {
+ try {
+ return Double.parseDouble((String) value.value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+ case Cursor.FIELD_TYPE_BLOB:
+ throw new android.database.sqlite.SQLiteException("could not convert "+value);
+ default:
+ throw new android.database.sqlite.SQLiteException("unknown type: "+value.type);
+ }
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ return getNumber(row - mStartPos, column).longValue();
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ return getNumber(row - mStartPos, column).doubleValue();
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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(@IntRange(from = 0) int row, @IntRange(from = 0) 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,
+ @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ // Real Android will crash in native code if putString is called with a null value.
+ Preconditions.checkNotNull(value);
+ return putValue(new Value(value, Cursor.FIELD_TYPE_BLOB), row, column);
+ }
+
+ /**
+ * 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,
+ @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ // Real Android will crash in native code if putString is called with a null value.
+ Preconditions.checkNotNull(value);
+ return putValue(new Value(value, Cursor.FIELD_TYPE_STRING), row, column);
+ }
+
+ /**
+ * 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,
+ @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ return putValue(new Value(value, Cursor.FIELD_TYPE_INTEGER), row, column);
+ }
+
+ /**
+ * 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,
+ @IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ return putValue(new Value(value, Cursor.FIELD_TYPE_FLOAT), row, column);
+ }
+
+ /**
+ * 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(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
+ return putValue(new Value(null, Cursor.FIELD_TYPE_NULL), row, column);
+ }
+
+// public static final @android.annotation.NonNull Parcelable.Creator CREATOR
+// = new Parcelable.Creator() {
+// public CursorWindow createFromParcel(Parcel source) {
+// return new CursorWindow(source);
+// }
+//
+// public CursorWindow[] newArray(int size) {
+// return new CursorWindow[size];
+// }
+// };
+
+// public static CursorWindow newFromParcel(Parcel p) {
+// return CREATOR.createFromParcel(p);
+// }
+
+ public int describeContents() {
+ return 0;
+ }
+
+// public void writeToParcel(Parcel dest, int flags) {
+// acquireReference();
+// try {
+// dest.writeInt(mStartPos);
+// nativeWriteToParcel(mWindowPtr, dest);
+// } finally {
+// releaseReference();
+// }
+//
+// if ((flags & Parcelable.PARCELABLE_WRITE_RETURN_VALUE) != 0) {
+// releaseReference();
+// }
+// }
+
+ @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 = 2024 * 1024;
+ }
+ return sCursorWindowSize;
+ }
+
+ @Override
+ public String toString() {
+ return getName();
+ }
+
+ private static class Row {
+ private final List values;
+
+ public Row(int length) {
+ values = new ArrayList(length);
+ for (int i=0; i {
+ /**
+ * Invokes {@link DataSetObserver#onChanged} on each observer.
+ * Called when the contents of the data set have changed. The recipient
+ * will obtain the new contents the next time it queries the data set.
+ */
+ public void notifyChanged() {
+ synchronized(mObservers) {
+ // since onChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
+ }
+ }
+ }
+
+ /**
+ * Invokes {@link DataSetObserver#onInvalidated} on each observer.
+ * Called when the data set is no longer valid and cannot be queried again,
+ * such as when the data set has been closed.
+ */
+ public void notifyInvalidated() {
+ synchronized (mObservers) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onInvalidated();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/database/Observable.java b/src/main/java/android/database/Observable.java
new file mode 100644
index 0000000..bee8a7a
--- /dev/null
+++ b/src/main/java/android/database/Observable.java
@@ -0,0 +1,83 @@
+/*
+ * 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 java.util.ArrayList;
+
+/**
+ * Provides methods for registering or unregistering arbitrary observers in an {@link ArrayList}.
+ *
+ * This abstract class is intended to be subclassed and specialized to maintain
+ * a registry of observers of specific types and dispatch notifications to them.
+ *
+ * @param T The observer type.
+ */
+public abstract class Observable {
+ /**
+ * The list of observers. An observer can be in the list at most
+ * once and will never be null.
+ */
+ protected final ArrayList mObservers = new ArrayList();
+
+ /**
+ * Adds an observer to the list. The observer cannot be null and it must not already
+ * be registered.
+ * @param observer the observer to register
+ * @throws IllegalArgumentException the observer is null
+ * @throws IllegalStateException the observer is already registered
+ */
+ public void registerObserver(T observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("The observer is null.");
+ }
+ synchronized(mObservers) {
+ if (mObservers.contains(observer)) {
+ throw new IllegalStateException("Observer " + observer + " is already registered.");
+ }
+ mObservers.add(observer);
+ }
+ }
+
+ /**
+ * Removes a previously registered observer. The observer must not be null and it
+ * must already have been registered.
+ * @param observer the observer to unregister
+ * @throws IllegalArgumentException the observer is null
+ * @throws IllegalStateException the observer is not yet registered
+ */
+ public void unregisterObserver(T observer) {
+ if (observer == null) {
+ throw new IllegalArgumentException("The observer is null.");
+ }
+ synchronized(mObservers) {
+ int index = mObservers.indexOf(observer);
+ if (index == -1) {
+ throw new IllegalStateException("Observer " + observer + " was not registered.");
+ }
+ mObservers.remove(index);
+ }
+ }
+
+ /**
+ * Remove all registered observers.
+ */
+ public void unregisterAll() {
+ synchronized(mObservers) {
+ mObservers.clear();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/database/StaleDataException.java b/src/main/java/android/database/StaleDataException.java
new file mode 100644
index 0000000..cf233b6
--- /dev/null
+++ b/src/main/java/android/database/StaleDataException.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * This exception is thrown when a Cursor contains stale data and must be
+ * requeried before being used again.
+ */
+public class StaleDataException extends java.lang.RuntimeException
+{
+ public StaleDataException()
+ {
+ super();
+ }
+
+ public StaleDataException(String description)
+ {
+ super(description);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/os/StatFs.java b/src/main/java/android/os/StatFs.java
new file mode 100644
index 0000000..fefa42a
--- /dev/null
+++ b/src/main/java/android/os/StatFs.java
@@ -0,0 +1,22 @@
+package android.os;
+
+import java.io.File;
+import java.io.IOException;
+
+public class StatFs {
+
+ File file;
+
+ public StatFs(String path) {
+ file = new File(path);
+ }
+
+ public int getBlockSize() {
+ try {
+ return (int)file.toPath().getFileSystem().getFileStores().iterator().next().getBlockSize();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/java/android/util/ArrayMap.java b/src/main/java/android/util/ArrayMap.java
new file mode 100644
index 0000000..3dfe233
--- /dev/null
+++ b/src/main/java/android/util/ArrayMap.java
@@ -0,0 +1,1094 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.Nullable;
+import com.android.internal.util.ArrayUtils;
+import libcore.util.EmptyArray;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+
+/**
+ * ArrayMap is a generic key->value mapping data structure that is
+ * designed to be more memory efficient than a traditional {@link java.util.HashMap}.
+ * It keeps its mappings in an array data structure -- an integer array of hash
+ * codes for each item, and an Object array of the key/value pairs. This allows it to
+ * avoid having to create an extra object for every entry put in to the map, and it
+ * also tries to control the growth of the size of these arrays more aggressively
+ * (since growing them only requires copying the entries in the array, not rebuilding
+ * a hash map).
+ *
+ *
Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items. It is generally slower than a traditional
+ * HashMap, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array. For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ *
Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it. Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size. In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.
+ *
+ *
This structure is NOT thread-safe.
+ */
+public final class ArrayMap implements Map {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "ArrayMap";
+
+ /**
+ * Attempt to spot concurrent modifications to this data structure.
+ *
+ * It's best-effort, but any time we can throw something more diagnostic than an
+ * ArrayIndexOutOfBoundsException deep in the ArrayMap internals it's going to
+ * save a lot of development time.
+ *
+ * Good times to look for CME include after any allocArrays() call and at the end of
+ * functions that change mSize (put/remove/clear).
+ */
+ private static final boolean CONCURRENT_MODIFICATION_EXCEPTIONS = true;
+
+ /**
+ * The minimum amount by which the capacity of a ArrayMap will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+ private static final int BASE_SIZE = 4;
+
+ /**
+ * Maximum number of entries to have in array caches.
+ */
+ private static final int CACHE_SIZE = 10;
+
+ /**
+ * Special hash array value that indicates the container is immutable.
+ */
+ static final int[] EMPTY_IMMUTABLE_INTS = new int[0];
+
+ /**
+ * @hide Special immutable empty ArrayMap.
+ */
+ public static final ArrayMap EMPTY = new ArrayMap<>(-1);
+
+ /**
+ * Caches of small array objects to avoid spamming garbage. The cache
+ * Object[] variable is a pointer to a linked list of array objects.
+ * The first entry in the array is a pointer to the next array in the
+ * list; the second entry is a pointer to the int[] hash code array for it.
+ */
+ static Object[] mBaseCache;
+ static int mBaseCacheSize;
+ static Object[] mTwiceBaseCache;
+ static int mTwiceBaseCacheSize;
+ /**
+ * Separate locks for each cache since each can be accessed independently of the other without
+ * risk of a deadlock.
+ */
+ private static final Object sBaseCacheLock = new Object();
+ private static final Object sTwiceBaseCacheLock = new Object();
+
+ private final boolean mIdentityHashCode;
+ int[] mHashes;
+ Object[] mArray;
+ int mSize;
+ private MapCollections mCollections;
+
+ private static int binarySearchHashes(int[] hashes, int N, int hash) {
+ try {
+ return ContainerHelpers.binarySearch(hashes, N, hash);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
+ throw new ConcurrentModificationException();
+ } else {
+ throw e; // the cache is poisoned at this point, there's not much we can do
+ }
+ }
+ }
+
+ int indexOf(Object key, int hash) {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, hash);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (key.equals(mArray[index<<1])) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == hash; end++) {
+ if (key.equals(mArray[end << 1])) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) {
+ if (key.equals(mArray[i << 1])) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ int indexOfNull() {
+ final int N = mSize;
+
+ // Important fast case: if nothing is in here, nothing to look for.
+ if (N == 0) {
+ return ~0;
+ }
+
+ int index = binarySearchHashes(mHashes, N, 0);
+
+ // If the hash code wasn't found, then we have no entry for this key.
+ if (index < 0) {
+ return index;
+ }
+
+ // If the key at the returned index matches, that's what we want.
+ if (null == mArray[index<<1]) {
+ return index;
+ }
+
+ // Search for a matching key after the index.
+ int end;
+ for (end = index + 1; end < N && mHashes[end] == 0; end++) {
+ if (null == mArray[end << 1]) return end;
+ }
+
+ // Search for a matching key before the index.
+ for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) {
+ if (null == mArray[i << 1]) return i;
+ }
+
+ // Key not found -- return negative value indicating where a
+ // new entry for this key should go. We use the end of the
+ // hash chain to reduce the number of array entries that will
+ // need to be copied when inserting.
+ return ~end;
+ }
+
+ private void allocArrays(final int size) {
+ if (mHashes == EMPTY_IMMUTABLE_INTS) {
+ throw new UnsupportedOperationException("ArrayMap is immutable");
+ }
+ if (size == (BASE_SIZE*2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (mTwiceBaseCache != null) {
+ final Object[] array = mTwiceBaseCache;
+ mArray = array;
+ try {
+ mTwiceBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ mTwiceBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 2x cache " + Arrays.toString(mHashes)
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ mTwiceBaseCache = null;
+ mTwiceBaseCacheSize = 0;
+ }
+ }
+ } else if (size == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (mBaseCache != null) {
+ final Object[] array = mBaseCache;
+ mArray = array;
+ try {
+ mBaseCache = (Object[]) array[0];
+ mHashes = (int[]) array[1];
+ if (mHashes != null) {
+ array[0] = array[1] = null;
+ mBaseCacheSize--;
+ if (DEBUG) {
+ Log.d(TAG, "Retrieving 1x cache " + Arrays.toString(mHashes)
+ + " now have " + mBaseCacheSize + " entries");
+ }
+ return;
+ }
+ } catch (ClassCastException e) {
+ }
+ // Whoops! Someone trampled the array (probably due to not protecting
+ // their access with a lock). Our cache is corrupt; report and give up.
+ Slog.wtf(TAG, "Found corrupt ArrayMap cache: [0]=" + array[0]
+ + " [1]=" + array[1]);
+ mBaseCache = null;
+ mBaseCacheSize = 0;
+ }
+ }
+ }
+
+ mHashes = new int[size];
+ mArray = new Object[size<<1];
+ }
+
+ /**
+ * Make sure NOT to call this method with arrays that can still be modified. In other
+ * words, don't pass mHashes or mArray in directly.
+ */
+ private static void freeArrays(final int[] hashes, final Object[] array, final int size) {
+ if (hashes.length == (BASE_SIZE*2)) {
+ synchronized (sTwiceBaseCacheLock) {
+ if (mTwiceBaseCacheSize < CACHE_SIZE) {
+ array[0] = mTwiceBaseCache;
+ array[1] = hashes;
+ for (int i=(size<<1)-1; i>=2; i--) {
+ array[i] = null;
+ }
+ mTwiceBaseCache = array;
+ mTwiceBaseCacheSize++;
+ if (DEBUG) {
+ Log.d(TAG, "Storing 2x cache " + Arrays.toString(array)
+ + " now have " + mTwiceBaseCacheSize + " entries");
+ }
+ }
+ }
+ } else if (hashes.length == BASE_SIZE) {
+ synchronized (sBaseCacheLock) {
+ if (mBaseCacheSize < CACHE_SIZE) {
+ array[0] = mBaseCache;
+ array[1] = hashes;
+ for (int i=(size<<1)-1; i>=2; i--) {
+ array[i] = null;
+ }
+ mBaseCache = array;
+ mBaseCacheSize++;
+ if (DEBUG) {
+ Log.d(TAG, "Storing 1x cache " + Arrays.toString(array)
+ + " now have " + mBaseCacheSize + " entries");
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Create a new empty ArrayMap. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+ public ArrayMap() {
+ this(0, false);
+ }
+
+ /**
+ * Create a new ArrayMap with a given initial capacity.
+ */
+ public ArrayMap(int capacity) {
+ this(capacity, false);
+ }
+
+ /** {@hide} */
+ public ArrayMap(int capacity, boolean identityHashCode) {
+ mIdentityHashCode = identityHashCode;
+
+ // If this is immutable, use the sentinal EMPTY_IMMUTABLE_INTS
+ // instance instead of the usual EmptyArray.INT. The reference
+ // is checked later to see if the array is allowed to grow.
+ if (capacity < 0) {
+ mHashes = EMPTY_IMMUTABLE_INTS;
+ mArray = EmptyArray.OBJECT;
+ } else if (capacity == 0) {
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ } else {
+ allocArrays(capacity);
+ }
+ mSize = 0;
+ }
+
+ /**
+ * Create a new ArrayMap with the mappings from the given ArrayMap.
+ */
+ public ArrayMap(ArrayMap map) {
+ this();
+ if (map != null) {
+ putAll(map);
+ }
+ }
+
+ /**
+ * Make the array map empty. All storage is released.
+ */
+ @Override
+ public void clear() {
+ if (mSize > 0) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ final int osize = mSize;
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ mSize = 0;
+ freeArrays(ohashes, oarray, osize);
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize > 0) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * @hide
+ * Like {@link #clear}, but doesn't reduce the capacity of the ArrayMap.
+ */
+ public void erase() {
+ if (mSize > 0) {
+ final int N = mSize<<1;
+ final Object[] array = mArray;
+ for (int i=0; iminimumCapacity
+ * items.
+ */
+ public void ensureCapacity(int minimumCapacity) {
+ final int osize = mSize;
+ if (mHashes.length < minimumCapacity) {
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(minimumCapacity);
+ if (mSize > 0) {
+ System.arraycopy(ohashes, 0, mHashes, 0, osize);
+ System.arraycopy(oarray, 0, mArray, 0, osize<<1);
+ }
+ freeArrays(ohashes, oarray, osize);
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && mSize != osize) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * Check whether a key exists in the array.
+ *
+ * @param key The key to search for.
+ * @return Returns true if the key exists, else false.
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ return indexOfKey(key) >= 0;
+ }
+
+ /**
+ * Returns the index of a key in the set.
+ *
+ * @param key The key to search for.
+ * @return Returns the index of the key if it exists, else a negative integer.
+ */
+ public int indexOfKey(Object key) {
+ return key == null ? indexOfNull()
+ : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified value, or a negative number if no keys map to the
+ * specified value.
+ * Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ */
+ public int indexOfValue(Object value) {
+ final int N = mSize*2;
+ final Object[] array = mArray;
+ if (value == null) {
+ for (int i=1; i>1;
+ }
+ }
+ } else {
+ for (int i=1; i>1;
+ }
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Check whether a value exists in the array. This requires a linear search
+ * through the entire array.
+ *
+ * @param value The value to search for.
+ * @return Returns true if the value exists, else false.
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ return indexOfValue(value) >= 0;
+ }
+
+ /**
+ * Retrieve a value from the array.
+ * @param key The key of the value to retrieve.
+ * @return Returns the value associated with the given key,
+ * or null if there is no such key.
+ */
+ @Override
+ public V get(Object key) {
+ final int index = indexOfKey(key);
+ return index >= 0 ? (V)mArray[(index<<1)+1] : null;
+ }
+
+ /**
+ * Return the key at the given index in the array.
+ *
+ *
For indices outside of the range 0...size()-1, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the key stored at the given index.
+ */
+ public K keyAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (K)mArray[index << 1];
+ }
+
+ /**
+ * Return the value at the given index in the array.
+ *
+ *
For indices outside of the range 0...size()-1, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value stored at the given index.
+ */
+ public V valueAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (V)mArray[(index << 1) + 1];
+ }
+
+ /**
+ * Set the value at a given index in the array.
+ *
+ *
For indices outside of the range 0...size()-1, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @param value The new value to store at this index.
+ * @return Returns the previous value at the given index.
+ */
+ public V setValueAt(int index, V value) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ index = (index << 1) + 1;
+ V old = (V)mArray[index];
+ mArray[index] = value;
+ return old;
+ }
+
+ /**
+ * Return true if the array map contains no items.
+ */
+ @Override
+ public boolean isEmpty() {
+ return mSize <= 0;
+ }
+
+ /**
+ * Add a new value to the array map.
+ * @param key The key under which to store the value. If
+ * this key already exists in the array, its value will be replaced.
+ * @param value The value to store for the given key.
+ * @return Returns the old value that was stored for the given key, or null if there
+ * was no such key.
+ */
+ @Override
+ public V put(K key, V value) {
+ final int osize = mSize;
+ final int hash;
+ int index;
+ if (key == null) {
+ hash = 0;
+ index = indexOfNull();
+ } else {
+ hash = mIdentityHashCode ? System.identityHashCode(key) : key.hashCode();
+ index = indexOf(key, hash);
+ }
+ if (index >= 0) {
+ index = (index<<1) + 1;
+ final V old = (V)mArray[index];
+ mArray[index] = value;
+ return old;
+ }
+
+ index = ~index;
+ if (osize >= mHashes.length) {
+ final int n = osize >= (BASE_SIZE*2) ? (osize+(osize>>1))
+ : (osize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE);
+
+ if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ if (mHashes.length > 0) {
+ if (DEBUG) Log.d(TAG, "put: copy 0-" + osize + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length);
+ System.arraycopy(oarray, 0, mArray, 0, oarray.length);
+ }
+
+ freeArrays(ohashes, oarray, osize);
+ }
+
+ if (index < osize) {
+ if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (osize-index)
+ + " to " + (index+1));
+ System.arraycopy(mHashes, index, mHashes, index + 1, osize - index);
+ System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1);
+ }
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS) {
+ if (osize != mSize || index >= mHashes.length) {
+ throw new ConcurrentModificationException();
+ }
+ }
+ mHashes[index] = hash;
+ mArray[index<<1] = key;
+ mArray[(index<<1)+1] = value;
+ mSize++;
+ return null;
+ }
+
+ /**
+ * Special fast path for appending items to the end of the array without validation.
+ * The array must already be large enough to contain the item.
+ * @hide
+ */
+ public void append(K key, V value) {
+ int index = mSize;
+ final int hash = key == null ? 0
+ : (mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ if (index >= mHashes.length) {
+ throw new IllegalStateException("Array is full");
+ }
+ if (index > 0 && mHashes[index-1] > hash) {
+ RuntimeException e = new RuntimeException("here");
+ e.fillInStackTrace();
+ Log.w(TAG, "New hash " + hash
+ + " is before end of array hash " + mHashes[index-1]
+ + " at index " + index + (DEBUG ? " key " + key : ""), e);
+ put(key, value);
+ return;
+ }
+ mSize = index+1;
+ mHashes[index] = hash;
+ index <<= 1;
+ mArray[index] = key;
+ mArray[index+1] = value;
+ }
+
+ /**
+ * The use of the {@link #append} function can result in invalid array maps, in particular
+ * an array map where the same key appears multiple times. This function verifies that
+ * the array map is valid, throwing IllegalArgumentException if a problem is found. The
+ * main use for this method is validating an array map after unpacking from an IPC, to
+ * protect against malicious callers.
+ * @hide
+ */
+ public void validate() {
+ final int N = mSize;
+ if (N <= 1) {
+ // There can't be dups.
+ return;
+ }
+ int basehash = mHashes[0];
+ int basei = 0;
+ for (int i=1; i=basei; j--) {
+ final Object prev = mArray[j<<1];
+ if (cur == prev) {
+ throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
+ }
+ if (cur != null && prev != null && cur.equals(prev)) {
+ throw new IllegalArgumentException("Duplicate key in ArrayMap: " + cur);
+ }
+ }
+ }
+ }
+
+ /**
+ * Perform a {@link #put(Object, Object)} of all key/value pairs in array
+ * @param array The array whose contents are to be retrieved.
+ */
+ public void putAll(ArrayMap extends K, ? extends V> array) {
+ final int N = array.mSize;
+ ensureCapacity(mSize + N);
+ if (mSize == 0) {
+ if (N > 0) {
+ System.arraycopy(array.mHashes, 0, mHashes, 0, N);
+ System.arraycopy(array.mArray, 0, mArray, 0, N<<1);
+ mSize = N;
+ }
+ } else {
+ for (int i=0; i= 0) {
+ return removeAt(index);
+ }
+
+ return null;
+ }
+
+ /**
+ * Remove the key/value mapping at the given index.
+ *
+ *
For indices outside of the range 0...size()-1, the behavior is undefined for
+ * apps targeting {@link android.os.Build.VERSION_CODES#P} and earlier, and an
+ * {@link ArrayIndexOutOfBoundsException} is thrown for apps targeting
+ * {@link android.os.Build.VERSION_CODES#Q} and later.
+ *
+ * @param index The desired index, must be between 0 and {@link #size()}-1.
+ * @return Returns the value that was stored at this index.
+ */
+ public V removeAt(int index) {
+ if (index >= mSize && UtilConfig.sThrowExceptionForUpperArrayOutOfBounds) {
+ // The array might be slightly bigger than mSize, in which case, indexing won't fail.
+ // Check if exception should be thrown outside of the critical path.
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+
+ final Object old = mArray[(index << 1) + 1];
+ final int osize = mSize;
+ final int nsize;
+ if (osize <= 1) {
+ // Now empty.
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0");
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ mHashes = EmptyArray.INT;
+ mArray = EmptyArray.OBJECT;
+ freeArrays(ohashes, oarray, osize);
+ nsize = 0;
+ } else {
+ nsize = osize - 1;
+ if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) {
+ // Shrunk enough to reduce size of arrays. We don't allow it to
+ // shrink smaller than (BASE_SIZE*2) to avoid flapping between
+ // that and BASE_SIZE.
+ final int n = osize > (BASE_SIZE*2) ? (osize + (osize>>1)) : (BASE_SIZE*2);
+
+ if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n);
+
+ final int[] ohashes = mHashes;
+ final Object[] oarray = mArray;
+ allocArrays(n);
+
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+
+ if (index > 0) {
+ if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0");
+ System.arraycopy(ohashes, 0, mHashes, 0, index);
+ System.arraycopy(oarray, 0, mArray, 0, index << 1);
+ }
+ if (index < nsize) {
+ if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + nsize
+ + " to " + index);
+ System.arraycopy(ohashes, index + 1, mHashes, index, nsize - index);
+ System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1,
+ (nsize - index) << 1);
+ }
+ } else {
+ if (index < nsize) {
+ if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + nsize
+ + " to " + index);
+ System.arraycopy(mHashes, index + 1, mHashes, index, nsize - index);
+ System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1,
+ (nsize - index) << 1);
+ }
+ mArray[nsize << 1] = null;
+ mArray[(nsize << 1) + 1] = null;
+ }
+ }
+ if (CONCURRENT_MODIFICATION_EXCEPTIONS && osize != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ mSize = nsize;
+ return (V)old;
+ }
+
+ /**
+ * Return the number of items in this array map.
+ */
+ @Override
+ public int size() {
+ return mSize;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
This implementation returns false if the object is not a map, or
+ * if the maps have different sizes. Otherwise, for each key in this map,
+ * values of both maps are compared. If the values for any key are not
+ * equal, the method returns false, otherwise it returns true.
+ */
+ @Override
+ public boolean equals(@Nullable Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof Map) {
+ Map, ?> map = (Map, ?>) object;
+ if (size() != map.size()) {
+ return false;
+ }
+
+ try {
+ for (int i=0; iThis implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a key or a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (isEmpty()) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i 0) {
+ buffer.append(", ");
+ }
+ Object key = keyAt(i);
+ if (key != this) {
+ buffer.append(key);
+ } else {
+ buffer.append("(this Map)");
+ }
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(ArrayUtils.deepToString(value));
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ // ------------------------------------------------------------------------
+ // Interop with traditional Java containers. Not as efficient as using
+ // specialized collection APIs.
+ // ------------------------------------------------------------------------
+
+ private MapCollections getCollection() {
+ if (mCollections == null) {
+ mCollections = new MapCollections() {
+ @Override
+ protected int colGetSize() {
+ return mSize;
+ }
+
+ @Override
+ protected Object colGetEntry(int index, int offset) {
+ return mArray[(index<<1) + offset];
+ }
+
+ @Override
+ protected int colIndexOfKey(Object key) {
+ return indexOfKey(key);
+ }
+
+ @Override
+ protected int colIndexOfValue(Object value) {
+ return indexOfValue(value);
+ }
+
+ @Override
+ protected Map colGetMap() {
+ return ArrayMap.this;
+ }
+
+ @Override
+ protected void colPut(K key, V value) {
+ put(key, value);
+ }
+
+ @Override
+ protected V colSetValue(int index, V value) {
+ return setValueAt(index, value);
+ }
+
+ @Override
+ protected void colRemoveAt(int index) {
+ removeAt(index);
+ }
+
+ @Override
+ protected void colClear() {
+ clear();
+ }
+ };
+ }
+ return mCollections;
+ }
+
+ /**
+ * Determine if the array map contains all of the keys in the given collection.
+ * @param collection The collection whose contents are to be checked against.
+ * @return Returns true if this array map contains a key for every entry
+ * in collection, else returns false.
+ */
+ public boolean containsAll(Collection> collection) {
+ return MapCollections.containsAllHelper(this, collection);
+ }
+
+ /**
+ * Performs the given action for all elements in the stored order. This implementation overrides
+ * the default implementation to avoid iterating using the {@link #entrySet()} and iterates in
+ * the key-value order consistent with {@link #keyAt(int)} and {@link #valueAt(int)}.
+ *
+ * @param action The action to be performed for each element
+ */
+ @Override
+ public void forEach(BiConsumer super K, ? super V> action) {
+ if (action == null) {
+ throw new NullPointerException("action must not be null");
+ }
+
+ final int size = mSize;
+ for (int i = 0; i < size; ++i) {
+ if (size != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ action.accept(keyAt(i), valueAt(i));
+ }
+ }
+
+ /**
+ * Perform a {@link #put(Object, Object)} of all key/value pairs in map
+ * @param map The map whose contents are to be retrieved.
+ */
+ @Override
+ public void putAll(Map extends K, ? extends V> map) {
+ ensureCapacity(mSize + map.size());
+ for (Map.Entry extends K, ? extends V> entry : map.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ /**
+ * Remove all keys in the array map that exist in the given collection.
+ * @param collection The collection whose contents are to be used to remove keys.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ public boolean removeAll(Collection> collection) {
+ return MapCollections.removeAllHelper(this, collection);
+ }
+
+ /**
+ * Replaces each entry's value with the result of invoking the given function on that entry
+ * until all entries have been processed or the function throws an exception. Exceptions thrown
+ * by the function are relayed to the caller. This implementation overrides
+ * the default implementation to avoid iterating using the {@link #entrySet()} and iterates in
+ * the key-value order consistent with {@link #keyAt(int)} and {@link #valueAt(int)}.
+ *
+ * @param function The function to apply to each entry
+ */
+ @Override
+ public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
+ if (function == null) {
+ throw new NullPointerException("function must not be null");
+ }
+
+ final int size = mSize;
+ try {
+ for (int i = 0; i < size; ++i) {
+ final int valIndex = (i << 1) + 1;
+ //noinspection unchecked
+ mArray[valIndex] = function.apply((K) mArray[i << 1], (V) mArray[valIndex]);
+ }
+ } catch (ArrayIndexOutOfBoundsException e) {
+ throw new ConcurrentModificationException();
+ }
+ if (size != mSize) {
+ throw new ConcurrentModificationException();
+ }
+ }
+
+ /**
+ * Remove all keys in the array map that do not exist in the given collection.
+ * @param collection The collection whose contents are to be used to determine which
+ * keys to keep.
+ * @return Returns true if any keys were removed from the array map, else false.
+ */
+ public boolean retainAll(Collection> collection) {
+ return MapCollections.retainAllHelper(this, collection);
+ }
+
+ /**
+ * Return a {@link java.util.Set} for iterating over and interacting with all mappings
+ * in the array map.
+ *
+ *
Note: this is a very inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.
+ *
+ *
Note:
the semantics of this
+ * Set are subtly different than that of a {@link java.util.HashMap}: most important,
+ * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single
+ * object that exists for the entire iterator, so you can not hold on to it
+ * after calling {@link java.util.Iterator#next() Iterator.next}.
+ */
+ @Override
+ public Set> entrySet() {
+ return getCollection().getEntrySet();
+ }
+
+ /**
+ * Return a {@link java.util.Set} for iterating over and interacting with all keys
+ * in the array map.
+ *
+ *
Note: this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.
+ */
+ @Override
+ public Set keySet() {
+ return getCollection().getKeySet();
+ }
+
+ /**
+ * Return a {@link java.util.Collection} for iterating over and interacting with all values
+ * in the array map.
+ *
+ *
Note: this is a fairly inefficient way to access the array contents, it
+ * requires generating a number of temporary objects and allocates additional state
+ * information associated with the container that will remain for the life of the container.
+ */
+ @Override
+ public Collection values() {
+ return getCollection().getValues();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/android/util/MapCollections.java b/src/main/java/android/util/MapCollections.java
new file mode 100644
index 0000000..6cd56ee
--- /dev/null
+++ b/src/main/java/android/util/MapCollections.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2013 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 android.annotation.Nullable;
+
+import java.lang.reflect.Array;
+import java.util.*;
+
+/**
+ * Helper for writing standard Java collection interfaces to a data
+ * structure like {@link ArrayMap}.
+ * @hide
+ */
+abstract class MapCollections {
+ EntrySet mEntrySet;
+ KeySet mKeySet;
+ ValuesCollection mValues;
+
+ final class ArrayIterator implements Iterator {
+ final int mOffset;
+ int mSize;
+ int mIndex;
+ boolean mCanRemove = false;
+
+ ArrayIterator(int offset) {
+ mOffset = offset;
+ mSize = colGetSize();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIndex < mSize;
+ }
+
+ @Override
+ public T next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ Object res = colGetEntry(mIndex, mOffset);
+ mIndex++;
+ mCanRemove = true;
+ return (T)res;
+ }
+
+ @Override
+ public void remove() {
+ if (!mCanRemove) {
+ throw new IllegalStateException();
+ }
+ mIndex--;
+ mSize--;
+ mCanRemove = false;
+ colRemoveAt(mIndex);
+ }
+ }
+
+ final class MapIterator implements Iterator>, Map.Entry {
+ int mEnd;
+ int mIndex;
+ boolean mEntryValid = false;
+
+ MapIterator() {
+ mEnd = colGetSize() - 1;
+ mIndex = -1;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mIndex < mEnd;
+ }
+
+ @Override
+ public Map.Entry next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ mIndex++;
+ mEntryValid = true;
+ return this;
+ }
+
+ @Override
+ public void remove() {
+ if (!mEntryValid) {
+ throw new IllegalStateException();
+ }
+ colRemoveAt(mIndex);
+ mIndex--;
+ mEnd--;
+ mEntryValid = false;
+ }
+
+ @Override
+ public K getKey() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (K)colGetEntry(mIndex, 0);
+ }
+
+ @Override
+ public V getValue() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return (V)colGetEntry(mIndex, 1);
+ }
+
+ @Override
+ public V setValue(V object) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ return colSetValue(mIndex, object);
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ if (!(o instanceof Map.Entry)) {
+ return false;
+ }
+ Map.Entry, ?> e = (Map.Entry, ?>) o;
+ return Objects.equals(e.getKey(), colGetEntry(mIndex, 0))
+ && Objects.equals(e.getValue(), colGetEntry(mIndex, 1));
+ }
+
+ @Override
+ public final int hashCode() {
+ if (!mEntryValid) {
+ throw new IllegalStateException(
+ "This container does not support retaining Map.Entry objects");
+ }
+ final Object key = colGetEntry(mIndex, 0);
+ final Object value = colGetEntry(mIndex, 1);
+ return (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode());
+ }
+
+ @Override
+ public final String toString() {
+ return getKey() + "=" + getValue();
+ }
+ }
+
+ final class EntrySet implements Set> {
+ @Override
+ public boolean add(Map.Entry object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection extends Map.Entry> collection) {
+ int oldSize = colGetSize();
+ for (Map.Entry entry : collection) {
+ colPut(entry.getKey(), entry.getValue());
+ }
+ return oldSize != colGetSize();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ if (!(o instanceof Map.Entry))
+ return false;
+ Map.Entry, ?> e = (Map.Entry, ?>) o;
+ int index = colIndexOfKey(e.getKey());
+ if (index < 0) {
+ return false;
+ }
+ Object foundVal = colGetEntry(index, 1);
+ return Objects.equals(foundVal, e.getValue());
+ }
+
+ @Override
+ public boolean containsAll(Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return new MapIterator();
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(Collection> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+
+ @Override
+ public Object[] toArray() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public T[] toArray(T[] array) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ return equalsSetHelper(this, object);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ final Object key = colGetEntry(i, 0);
+ final Object value = colGetEntry(i, 1);
+ result += ( (key == null ? 0 : key.hashCode()) ^
+ (value == null ? 0 : value.hashCode()) );
+ }
+ return result;
+ }
+ };
+
+ final class KeySet implements Set {
+
+ @Override
+ public boolean add(K object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection extends K> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfKey(object) >= 0;
+ }
+
+ @Override
+ public boolean containsAll(Collection> collection) {
+ return containsAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new ArrayIterator(0);
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfKey(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection> collection) {
+ return removeAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public boolean retainAll(Collection> collection) {
+ return retainAllHelper(colGetMap(), collection);
+ }
+
+ @Override
+ public int size() {
+ return colGetSize();
+ }
+
+ @Override
+ public Object[] toArray() {
+ return toArrayHelper(0);
+ }
+
+ @Override
+ public T[] toArray(T[] array) {
+ return toArrayHelper(array, 0);
+ }
+
+ @Override
+ public boolean equals(@Nullable Object object) {
+ return equalsSetHelper(this, object);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 0;
+ for (int i=colGetSize()-1; i>=0; i--) {
+ Object obj = colGetEntry(i, 0);
+ result += obj == null ? 0 : obj.hashCode();
+ }
+ return result;
+ }
+ };
+
+ final class ValuesCollection implements Collection {
+
+ @Override
+ public boolean add(V object) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection extends V> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ colClear();
+ }
+
+ @Override
+ public boolean contains(Object object) {
+ return colIndexOfValue(object) >= 0;
+ }
+
+ @Override
+ public boolean containsAll(Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!contains(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return colGetSize() == 0;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new ArrayIterator(1);
+ }
+
+ @Override
+ public boolean remove(Object object) {
+ int index = colIndexOfValue(object);
+ if (index >= 0) {
+ colRemoveAt(index);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection> collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i collection) {
+ int N = colGetSize();
+ boolean changed = false;
+ for (int i=0; i T[] toArray(T[] array) {
+ return toArrayHelper(array, 1);
+ }
+ };
+
+ public static boolean containsAllHelper(Map map, Collection> collection) {
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ if (!map.containsKey(it.next())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public static boolean removeAllHelper(Map map, Collection> collection) {
+ int oldSize = map.size();
+ Iterator> it = collection.iterator();
+ while (it.hasNext()) {
+ map.remove(it.next());
+ }
+ return oldSize != map.size();
+ }
+
+ public static boolean retainAllHelper(Map map, Collection> collection) {
+ int oldSize = map.size();
+ Iterator it = map.keySet().iterator();
+ while (it.hasNext()) {
+ if (!collection.contains(it.next())) {
+ it.remove();
+ }
+ }
+ return oldSize != map.size();
+ }
+
+ public Object[] toArrayHelper(int offset) {
+ final int N = colGetSize();
+ Object[] result = new Object[N];
+ for (int i=0; i T[] toArrayHelper(T[] array, int offset) {
+ final int N = colGetSize();
+ if (array.length < N) {
+ @SuppressWarnings("unchecked") T[] newArray
+ = (T[]) Array.newInstance(array.getClass().getComponentType(), N);
+ array = newArray;
+ }
+ for (int i=0; i N) {
+ array[N] = null;
+ }
+ return array;
+ }
+
+ public static boolean equalsSetHelper(Set set, Object object) {
+ if (set == object) {
+ return true;
+ }
+ if (object instanceof Set) {
+ Set> s = (Set>) object;
+
+ try {
+ return set.size() == s.size() && set.containsAll(s);
+ } catch (NullPointerException ignored) {
+ return false;
+ } catch (ClassCastException ignored) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ public Set> getEntrySet() {
+ if (mEntrySet == null) {
+ mEntrySet = new EntrySet();
+ }
+ return mEntrySet;
+ }
+
+ public Set getKeySet() {
+ if (mKeySet == null) {
+ mKeySet = new KeySet();
+ }
+ return mKeySet;
+ }
+
+ public Collection getValues() {
+ if (mValues == null) {
+ mValues = new ValuesCollection();
+ }
+ return mValues;
+ }
+
+ protected abstract int colGetSize();
+ protected abstract Object colGetEntry(int index, int offset);
+ protected abstract int colIndexOfKey(Object key);
+ protected abstract int colIndexOfValue(Object key);
+ protected abstract Map colGetMap();
+ protected abstract void colPut(K key, V value);
+ protected abstract V colSetValue(int index, V value);
+ protected abstract void colRemoveAt(int index);
+ protected abstract void colClear();
+}
\ No newline at end of file
diff --git a/src/main/java/com/android/internal/util/ArrayUtils.java b/src/main/java/com/android/internal/util/ArrayUtils.java
index d6bb293..b26108f 100644
--- a/src/main/java/com/android/internal/util/ArrayUtils.java
+++ b/src/main/java/com/android/internal/util/ArrayUtils.java
@@ -22,17 +22,20 @@
import dalvik.system.VMRuntime;
import libcore.util.EmptyArray;
+import java.io.File;
import java.lang.reflect.Array;
import java.util.*;
+import java.util.function.IntFunction;
/**
- * ArrayUtils contains some methods that you can call to find out
- * the most efficient increments by which to grow arrays.
+ * Static utility methods for arrays that aren't already included in {@link java.util.Arrays}.
*/
public class ArrayUtils {
private static final int CACHE_SIZE = 73;
private static Object[] sCache = new Object[CACHE_SIZE];
+ public static final File[] EMPTY_FILE = new File[0];
+
private ArrayUtils() { /* cannot be instantiated */ }
public static byte[] newUnpaddedByteArray(int minLen) {
@@ -119,6 +122,13 @@ public static T[] emptyArray(Class kind) {
return (T[]) cache;
}
+ /**
+ * Returns the same array or an empty one if it's null.
+ */
+ public static @NonNull T[] emptyIfNull(@Nullable T[] items, Class kind) {
+ return items != null ? items : emptyArray(kind);
+ }
+
/**
* Checks if given array is null or has zero elements.
*/
@@ -182,6 +192,13 @@ public static int size(@Nullable Collection> collection) {
return collection == null ? 0 : collection.size();
}
+ /**
+ * Length of the given map or 0 if it's null.
+ */
+ public static int size(@Nullable Map, ?> map) {
+ return map == null ? 0 : map.size();
+ }
+
/**
* Checks that value is present as at least one of the elements of the array.
* @param array the array to check in
@@ -283,6 +300,10 @@ public static long total(@Nullable long[] array) {
return total;
}
+ /**
+ * @deprecated use {@code IntArray} instead
+ */
+ @Deprecated
public static int[] convertToIntArray(List list) {
int[] array = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
@@ -291,6 +312,16 @@ public static int[] convertToIntArray(List list) {
return array;
}
+ @NonNull
+ public static int[] convertToIntArray(@NonNull ArraySet set) {
+ final int size = set.size();
+ int[] array = new int[size];
+ for (int i = 0; i < size; i++) {
+ array[i] = set.valueAt(i);
+ }
+ return array;
+ }
+
public static @Nullable long[] convertToLongArray(@Nullable int[] intArray) {
if (intArray == null) return null;
long[] array = new long[intArray.length];
@@ -300,6 +331,81 @@ public static int[] convertToIntArray(List list) {
return array;
}
+ /**
+ * Returns the concatenation of the given arrays. Only works for object arrays, not for
+ * primitive arrays. See {@link #concat(byte[]...)} for a variant that works on byte arrays.
+ *
+ * @param kind The class of the array elements
+ * @param arrays The arrays to concatenate. Null arrays are treated as empty.
+ * @param The class of the array elements (inferred from kind).
+ * @return A single array containing all the elements of the parameter arrays.
+ */
+ @SuppressWarnings("unchecked")
+ public static @NonNull T[] concat(Class kind, @Nullable T[]... arrays) {
+ if (arrays == null || arrays.length == 0) {
+ return createEmptyArray(kind);
+ }
+
+ int totalLength = 0;
+ for (T[] item : arrays) {
+ if (item == null) {
+ continue;
+ }
+
+ totalLength += item.length;
+ }
+
+ // Optimization for entirely empty arrays.
+ if (totalLength == 0) {
+ return createEmptyArray(kind);
+ }
+
+ final T[] all = (T[]) Array.newInstance(kind, totalLength);
+ int pos = 0;
+ for (T[] item : arrays) {
+ if (item == null || item.length == 0) {
+ continue;
+ }
+ System.arraycopy(item, 0, all, pos, item.length);
+ pos += item.length;
+ }
+ return all;
+ }
+
+ private static @NonNull T[] createEmptyArray(Class kind) {
+ if (kind == String.class) {
+ return (T[]) EmptyArray.STRING;
+ } else if (kind == Object.class) {
+ return (T[]) EmptyArray.OBJECT;
+ }
+
+ return (T[]) Array.newInstance(kind, 0);
+ }
+
+ /**
+ * Returns the concatenation of the given byte arrays. Null arrays are treated as empty.
+ */
+ public static @NonNull byte[] concat(@Nullable byte[]... arrays) {
+ if (arrays == null) {
+ return new byte[0];
+ }
+ int totalLength = 0;
+ for (byte[] a : arrays) {
+ if (a != null) {
+ totalLength += a.length;
+ }
+ }
+ final byte[] result = new byte[totalLength];
+ int pos = 0;
+ for (byte[] a : arrays) {
+ if (a != null) {
+ System.arraycopy(a, 0, result, pos, a.length);
+ pos += a.length;
+ }
+ }
+ return result;
+ }
+
/**
* Adds value to given array if not already present, providing set-like
* behavior.
@@ -314,7 +420,7 @@ public static int[] convertToIntArray(List list) {
*/
@SuppressWarnings("unchecked")
public static @NonNull T[] appendElement(Class kind, @Nullable T[] array, T element,
- boolean allowDuplicates) {
+ boolean allowDuplicates) {
final T[] result;
final int end;
if (array != null) {
@@ -357,7 +463,7 @@ public static int[] convertToIntArray(List list) {
* Adds value to given array.
*/
public static @NonNull int[] appendInt(@Nullable int[] cur, int val,
- boolean allowDuplicates) {
+ boolean allowDuplicates) {
if (cur == null) {
return new int[] { val };
}
@@ -434,7 +540,7 @@ public static int[] convertToIntArray(List list) {
* behavior.
*/
public static @NonNull long[] appendLong(@Nullable long[] cur, long val,
- boolean allowDuplicates) {
+ boolean allowDuplicates) {
if (cur == null) {
return new long[] { val };
}
@@ -487,6 +593,13 @@ public static int[] convertToIntArray(List list) {
return (array != null) ? array.clone() : null;
}
+ /**
+ * Clones an array or returns null if the array is null.
+ */
+ public static @Nullable T[] cloneOrNull(@Nullable T[] array) {
+ return (array != null) ? array.clone() : null;
+ }
+
public static @Nullable ArraySet cloneOrNull(@Nullable ArraySet array) {
return (array != null) ? new ArraySet(array) : null;
}
@@ -499,6 +612,20 @@ public static int[] convertToIntArray(List list) {
return cur;
}
+ /**
+ * Similar to {@link Set#addAll(Collection)}}, but with support for set values of {@code null}.
+ */
+ public static @NonNull ArraySet addAll(@Nullable ArraySet cur,
+ @Nullable Collection val) {
+ if (cur == null) {
+ cur = new ArraySet<>();
+ }
+ if (val != null) {
+ cur.addAll(val);
+ }
+ return cur;
+ }
+
public static @Nullable ArraySet remove(@Nullable ArraySet cur, T val) {
if (cur == null) {
return null;
@@ -519,6 +646,14 @@ public static int[] convertToIntArray(List list) {
return cur;
}
+ public static @NonNull ArrayList add(@Nullable ArrayList cur, int index, T val) {
+ if (cur == null) {
+ cur = new ArrayList<>();
+ }
+ cur.add(index, val);
+ return cur;
+ }
+
public static @Nullable ArrayList remove(@Nullable ArrayList cur, T val) {
if (cur == null) {
return null;
@@ -618,4 +753,177 @@ public static int unstableRemoveIf(@Nullable ArrayList collection,
public static @NonNull String[] defeatNullable(@Nullable String[] val) {
return (val != null) ? val : EmptyArray.STRING;
}
-}
+
+ public static @NonNull File[] defeatNullable(@Nullable File[] val) {
+ return (val != null) ? val : EMPTY_FILE;
+ }
+
+ /**
+ * Throws {@link ArrayIndexOutOfBoundsException} if the index is out of bounds.
+ *
+ * @param len length of the array. Must be non-negative
+ * @param index the index to check
+ * @throws ArrayIndexOutOfBoundsException if the {@code index} is out of bounds of the array
+ */
+ public static void checkBounds(int len, int index) {
+ if (index < 0 || len <= index) {
+ throw new ArrayIndexOutOfBoundsException("length=" + len + "; index=" + index);
+ }
+ }
+
+ /**
+ * Throws {@link ArrayIndexOutOfBoundsException} if the range is out of bounds.
+ * @param len length of the array. Must be non-negative
+ * @param offset start index of the range. Must be non-negative
+ * @param count length of the range. Must be non-negative
+ * @throws ArrayIndexOutOfBoundsException if the range from {@code offset} with length
+ * {@code count} is out of bounds of the array
+ */
+ public static void throwsIfOutOfBounds(int len, int offset, int count) {
+ if (len < 0) {
+ throw new ArrayIndexOutOfBoundsException("Negative length: " + len);
+ }
+
+ if ((offset | count) < 0 || offset > len - count) {
+ throw new ArrayIndexOutOfBoundsException(
+ "length=" + len + "; regionStart=" + offset + "; regionLength=" + count);
+ }
+ }
+
+ /**
+ * Returns an array with values from {@code val} minus {@code null} values
+ *
+ * @param arrayConstructor typically {@code T[]::new} e.g. {@code String[]::new}
+ */
+ public static T[] filterNotNull(T[] val, IntFunction arrayConstructor) {
+ int nullCount = 0;
+ int size = size(val);
+ for (int i = 0; i < size; i++) {
+ if (val[i] == null) {
+ nullCount++;
+ }
+ }
+ if (nullCount == 0) {
+ return val;
+ }
+ T[] result = arrayConstructor.apply(size - nullCount);
+ int outIdx = 0;
+ for (int i = 0; i < size; i++) {
+ if (val[i] != null) {
+ result[outIdx++] = val[i];
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns an array containing elements from the given one that match the given predicate.
+ * The returned array may, in some cases, be the reference to the input array.
+ */
+ public static @Nullable T[] filter(@Nullable T[] items,
+ @NonNull IntFunction arrayConstructor,
+ @NonNull java.util.function.Predicate predicate) {
+ if (isEmpty(items)) {
+ return items;
+ }
+
+ int matchesCount = 0;
+ int size = size(items);
+ final boolean[] tests = new boolean[size];
+ for (int i = 0; i < size; i++) {
+ tests[i] = predicate.test(items[i]);
+ if (tests[i]) {
+ matchesCount++;
+ }
+ }
+ if (matchesCount == items.length) {
+ return items;
+ }
+ T[] result = arrayConstructor.apply(matchesCount);
+ if (matchesCount == 0) {
+ return result;
+ }
+ int outIdx = 0;
+ for (int i = 0; i < size; i++) {
+ if (tests[i]) {
+ result[outIdx++] = items[i];
+ }
+ }
+ return result;
+ }
+
+ public static boolean startsWith(byte[] cur, byte[] val) {
+ if (cur == null || val == null) return false;
+ if (cur.length < val.length) return false;
+ for (int i = 0; i < val.length; i++) {
+ if (cur[i] != val[i]) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the first element from the array for which
+ * condition {@code predicate} is true, or null if there is no such element
+ */
+ public static @Nullable T find(@Nullable T[] items,
+ @NonNull java.util.function.Predicate predicate) {
+ if (isEmpty(items)) return null;
+ for (final T item : items) {
+ if (predicate.test(item)) return item;
+ }
+ return null;
+ }
+
+ public static String deepToString(Object value) {
+ if (value != null && value.getClass().isArray()) {
+ if (value.getClass() == boolean[].class) {
+ return Arrays.toString((boolean[]) value);
+ } else if (value.getClass() == byte[].class) {
+ return Arrays.toString((byte[]) value);
+ } else if (value.getClass() == char[].class) {
+ return Arrays.toString((char[]) value);
+ } else if (value.getClass() == double[].class) {
+ return Arrays.toString((double[]) value);
+ } else if (value.getClass() == float[].class) {
+ return Arrays.toString((float[]) value);
+ } else if (value.getClass() == int[].class) {
+ return Arrays.toString((int[]) value);
+ } else if (value.getClass() == long[].class) {
+ return Arrays.toString((long[]) value);
+ } else if (value.getClass() == short[].class) {
+ return Arrays.toString((short[]) value);
+ } else {
+ return Arrays.deepToString((Object[]) value);
+ }
+ } else {
+ return String.valueOf(value);
+ }
+ }
+
+ /**
+ * Returns the {@code i}-th item in {@code items}, if it exists and {@code items} is not {@code
+ * null}, otherwise returns {@code null}.
+ */
+ @Nullable
+ public static T getOrNull(@Nullable T[] items, int i) {
+ return (items != null && items.length > i) ? items[i] : null;
+ }
+
+ public static @Nullable T firstOrNull(T[] items) {
+ return items.length > 0 ? items[0] : null;
+ }
+
+ /**
+ * Creates a {@link List} from an array. Different from {@link Arrays#asList(Object[])} as that
+ * will use the parameter as the backing array, meaning changes are not isolated.
+ */
+ public static List toList(T[] array) {
+ List list = new ArrayList<>(array.length);
+ //noinspection ManualArrayToCollectionCopy
+ for (T item : array) {
+ //noinspection UseBulkOperation
+ list.add(item);
+ }
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/libcore/util/EmptyArray.java b/src/main/java/libcore/util/EmptyArray.java
new file mode 100644
index 0000000..28143d4
--- /dev/null
+++ b/src/main/java/libcore/util/EmptyArray.java
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+package libcore.util;
+
+import android.annotation.SystemApi;
+
+import java.lang.annotation.Annotation;
+
+import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;
+
+/**
+ * Empty array is immutable. Use a shared empty array to avoid allocation.
+ *
+ * @hide
+ */
+@SystemApi(client = MODULE_LIBRARIES)
+public final class EmptyArray {
+ private EmptyArray() {}
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull boolean[] BOOLEAN = new boolean[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull byte[] BYTE = new byte[0];
+
+ /** @hide */
+ public static final char[] CHAR = new char[0];
+
+ /** @hide */
+ public static final double[] DOUBLE = new double[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull float[] FLOAT = new float[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull int[] INT = new int[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull long[] LONG = new long[0];
+
+ /** @hide */
+ public static final Class>[] CLASS = new Class[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull Object[] OBJECT = new Object[0];
+
+ /** @hide */
+ @SystemApi(client = MODULE_LIBRARIES)
+ public static final @NonNull String[] STRING = new String[0];
+
+ /** @hide */
+ public static final Throwable[] THROWABLE = new Throwable[0];
+
+ /** @hide */
+ public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
+
+ /** @hide */
+ public static final java.lang.reflect.Type[] TYPE = new java.lang.reflect.Type[0];
+
+ /** @hide */
+ public static final java.lang.reflect.TypeVariable[] TYPE_VARIABLE =
+ new java.lang.reflect.TypeVariable[0];
+ /** @hide */
+ public static final Annotation[] ANNOTATION = new Annotation[0];
+}
\ No newline at end of file
From 7506a3e8bb62ecd4f647baa85f04931e5242577c Mon Sep 17 00:00:00 2001
From: nbransby
Date: Fri, 12 Jan 2024 16:14:13 +1100
Subject: [PATCH 7/7] bump version
---
gradle.properties | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 320f703..7ed95e7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1 +1 @@
-version=0.3.1
\ No newline at end of file
+version=0.4.0
\ No newline at end of file