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

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

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

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

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

This class appeared in Android 3.1 (Honeycomb MR1); it's available as part + * of Android's + * Support Package for earlier releases. + */ +public class LruCache { + private final LinkedHashMap map; + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + /** + * Sets the size of the cache. + * + * @param maxSize The new maximum size. + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + V createdValue = create(key); + if (createdValue == null) { + return null; + } + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + if (previous != null) { + entryRemoved(false, key, previous, value); + } + trimToSize(maxSize); + return previous; + } + /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + public void trimToSize(int maxSize) { + 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. + *
    + *
  1. {@link #STATEMENT_SELECT}
  2. + *
  3. {@link #STATEMENT_UPDATE}
  4. + *
  5. {@link #STATEMENT_ATTACH}
  6. + *
  7. {@link #STATEMENT_BEGIN}
  8. + *
  9. {@link #STATEMENT_COMMIT}
  10. + *
  11. {@link #STATEMENT_ABORT}
  12. + *
  13. {@link #STATEMENT_OTHER}
  14. + *
+ * @param sql the SQL statement whose type is returned by this method + * @return one of the values listed above + */ + public static int getSqlStatementType(String sql) { + sql = sql.trim(); + if (sql.length() < 3) { + return STATEMENT_OTHER; + } + String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT); + if (prefixSql.equals("SEL")) { + return STATEMENT_SELECT; + } else if (prefixSql.equals("INS") || + prefixSql.equals("UPD") || + prefixSql.equals("REP") || + prefixSql.equals("DEL")) { + return STATEMENT_UPDATE; + } else if (prefixSql.equals("ATT")) { + return STATEMENT_ATTACH; + } else if (prefixSql.equals("COM")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("END")) { + return STATEMENT_COMMIT; + } else if (prefixSql.equals("ROL")) { + return STATEMENT_ABORT; + } else if (prefixSql.equals("BEG")) { + return STATEMENT_BEGIN; + } else if (prefixSql.equals("PRA")) { + return STATEMENT_PRAGMA; + } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") || + prefixSql.equals("ALT")) { + return STATEMENT_DDL; + } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) { + return STATEMENT_UNPREPARED; + } + return STATEMENT_OTHER; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } + + /** + * Returns column index of "_id" column, or -1 if not found. + * @hide + */ + public static int findRowIdColumnIndex(String[] columnNames) { + int length = columnNames.length; + for (int i = 0; i < length; i++) { + if (columnNames[i].equals("_id")) { + return i; + } + } + return -1; + } +} diff --git a/src/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: + *

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

+ * If null is specified for the DatabaseErrorHandler param in the above calls, this class is used + * as the default {@link DatabaseErrorHandler}. + */ +public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler { + + private static final String TAG = "DefaultDatabaseErrorHandler"; + + /** + * defines the default method to be invoked when database corruption is detected. + * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption + * is detected. + */ + public void onCorruption(SQLiteDatabase dbObj) { + Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath()); + + // If this is a SEE build, do not delete any database files. + // It may be that the user has specified an incorrect password. + if( SQLiteDatabase.hasCodec() ) return; + + // is the corruption detected even before database could be 'opened'? + if (!dbObj.isOpen()) { + // database files are not even openable. delete this database file. + // NOTE if the database has attached databases, then any of them could be corrupt. + // and not deleting all of them could cause corrupted database file to remain and + // make the application crash on database open operation. To avoid this problem, + // the application should provide its own {@link DatabaseErrorHandler} impl class + // to delete ALL files of the database (including the attached databases). + deleteDatabaseFile(dbObj.getPath()); + return; + } + + List> attachedDbs = null; + try { + // Close the database, which will cause subsequent operations to fail. + // before that, get the attached database list first. + try { + attachedDbs = dbObj.getAttachedDbs(); + } catch (SQLiteException e) { + /* ignore */ + } + try { + dbObj.close(); + } catch (SQLiteException e) { + /* ignore */ + } + } finally { + // Delete all files of this corrupt database and/or attached databases + if (attachedDbs != null) { + for (Pair p : attachedDbs) { + deleteDatabaseFile(p.second); + } + } else { + // attachedDbs = null is possible when the database is so corrupt that even + // "PRAGMA database_list;" also fails. delete the main database file + deleteDatabaseFile(dbObj.getPath()); + } + } + } + + private void deleteDatabaseFile(String fileName) { + if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) { + return; + } + Log.e(TAG, "deleting the database file: " + fileName); + try { + SQLiteDatabase.deleteDatabase(new File(fileName)); + } catch (Exception e) { + /* print warning and ignore exception */ + Log.w(TAG, "delete failed: " + e.getMessage()); + } + } +} diff --git a/src/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). + *

+ * A simple example:

   {@code
+ *   class Foo {
+ *
+ *       private final CloseGuard guard = CloseGuard.get();
+ *
+ *       ...
+ *
+ *       public Foo() {
+ *           ...;
+ *           guard.open("cleanup");
+ *       }
+ *
+ *       public void cleanup() {
+ *          guard.close();
+ *          ...;
+ *       }
+ *
+ *       protected void finalize() throws Throwable {
+ *           try {
+ *               if (guard != null) {
+ *                   guard.warnIfOpen();
+ *               }
+ *               cleanup();
+ *           } finally {
+ *               super.finalize();
+ *           }
+ *       }
+ *   }
+ * }
+ * + * In usage where the resource to be explicitly cleaned up are + * allocated after object construction, CloseGuard protection can + * be deferred. For example:
   {@code
+ *   class Bar {
+ *
+ *       private final CloseGuard guard = CloseGuard.get();
+ *
+ *       ...
+ *
+ *       public Bar() {
+ *           ...;
+ *       }
+ *
+ *       public void connect() {
+ *          ...;
+ *          guard.open("cleanup");
+ *       }
+ *
+ *       public void cleanup() {
+ *          guard.close();
+ *          ...;
+ *       }
+ *
+ *       protected void finalize() throws Throwable {
+ *           try {
+ *               if (guard != null) {
+ *                   guard.warnIfOpen();
+ *               }
+ *               cleanup();
+ *           } finally {
+ *               super.finalize();
+ *           }
+ *       }
+ *   }
+ * }
+ * + * 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(); + } else { + operation.mBindArgs.clear(); + } + for (int i = 0; i < bindArgs.length; i++) { + final Object arg = bindArgs[i]; + if (arg != null && arg instanceof byte[]) { + // Don't hold onto the real byte array longer than necessary. + operation.mBindArgs.add(EMPTY_BYTE_ARRAY); + } else { + operation.mBindArgs.add(arg); + } + } + } + operation.mCookie = newOperationCookieLocked(index); + mIndex = index; + return operation.mCookie; + } + } + + public void failOperation(int cookie, Exception ex) { + synchronized (mOperations) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mException = ex; + } + } + } + + public void endOperation(int cookie) { + synchronized (mOperations) { + if (endOperationDeferLogLocked(cookie)) { + logOperationLocked(cookie, null); + } + } + } + + public boolean endOperationDeferLog(int cookie) { + synchronized (mOperations) { + return endOperationDeferLogLocked(cookie); + } + } + + public void logOperation(int cookie, String detail) { + synchronized (mOperations) { + logOperationLocked(cookie, detail); + } + } + + private boolean endOperationDeferLogLocked(int cookie) { + final Operation operation = getOperationLocked(cookie); + if (operation != null) { + operation.mEndTime = SystemClock.uptimeMillis(); + operation.mFinished = true; + return SQLiteDebug.DEBUG_LOG_SLOW_QUERIES && SQLiteDebug.shouldLogSlowQuery( + operation.mEndTime - operation.mStartTime); + } + return false; + } + + private void logOperationLocked(int cookie, String detail) { + final Operation operation = getOperationLocked(cookie); + StringBuilder msg = new StringBuilder(); + operation.describe(msg, false); + if (detail != null) { + msg.append(", ").append(detail); + } + Log.d(TAG, msg.toString()); + } + + private int newOperationCookieLocked(int index) { + final int generation = mGeneration++; + return generation << COOKIE_GENERATION_SHIFT | index; + } + + private Operation getOperationLocked(int cookie) { + final int index = cookie & COOKIE_INDEX_MASK; + final Operation operation = mOperations[index]; + return operation.mCookie == cookie ? operation : null; + } + + public String describeCurrentOperation() { + synchronized (mOperations) { + final Operation operation = mOperations[mIndex]; + if (operation != null && !operation.mFinished) { + StringBuilder msg = new StringBuilder(); + operation.describe(msg, false); + return msg.toString(); + } + return null; + } + } + + public void dump(Printer printer, boolean verbose) { + synchronized (mOperations) { + printer.println(" Most recently executed operations:"); + int index = mIndex; + Operation operation = mOperations[index]; + if (operation != null) { + int n = 0; + do { + StringBuilder msg = new StringBuilder(); + msg.append(" ").append(n).append(": ["); + msg.append(operation.getFormattedStartTime()); + msg.append("] "); + operation.describe(msg, verbose); + printer.println(msg.toString()); + + if (index > 0) { + index -= 1; + } else { + index = MAX_RECENT_OPERATIONS - 1; + } + n += 1; + operation = mOperations[index]; + } while (operation != null && n < MAX_RECENT_OPERATIONS); + } else { + printer.println(" "); + } + } + } + } + + private static final class Operation { + // Trim all SQL statements to 256 characters inside the trace marker. + // This limit gives plenty of context while leaving space for other + // entries in the trace buffer (and ensures atrace doesn't truncate the + // marker for us, potentially losing metadata in the process). + private static final int MAX_TRACE_METHOD_NAME_LEN = 256; + + public long mStartWallTime; // in System.currentTimeMillis() + public long mStartTime; // in SystemClock.uptimeMillis(); + public long mEndTime; // in SystemClock.uptimeMillis(); + public String mKind; + public String mSql; + public ArrayList mBindArgs; + public boolean mFinished; + public Exception mException; + public int mCookie; + + public void describe(StringBuilder msg, boolean verbose) { + msg.append(mKind); + if (mFinished) { + msg.append(" took ").append(mEndTime - mStartTime).append("ms"); + } else { + msg.append(" started ").append(System.currentTimeMillis() - mStartWallTime) + .append("ms ago"); + } + msg.append(" - ").append(getStatus()); + if (mSql != null) { + msg.append(", sql=\"").append(trimSqlForDisplay(mSql)).append("\""); + } + if (verbose && mBindArgs != null && mBindArgs.size() != 0) { + msg.append(", bindArgs=["); + final int count = mBindArgs.size(); + for (int i = 0; i < count; i++) { + final Object arg = mBindArgs.get(i); + if (i != 0) { + msg.append(", "); + } + if (arg == null) { + msg.append("null"); + } else if (arg instanceof byte[]) { + msg.append(""); + } else if (arg instanceof String) { + msg.append("\"").append((String)arg).append("\""); + } else { + msg.append(arg); + } + } + msg.append("]"); + } + if (mException != null) { + msg.append(", exception=\"").append(mException.getMessage()).append("\""); + } + } + + private String getStatus() { + if (!mFinished) { + return "running"; + } + return mException != null ? "failed" : "succeeded"; + } + + private String getTraceMethodName() { + String methodName = mKind + " " + mSql; + if (methodName.length() > MAX_TRACE_METHOD_NAME_LEN) + return methodName.substring(0, MAX_TRACE_METHOD_NAME_LEN); + return methodName; + } + + private String getFormattedStartTime() { + // Note: SimpleDateFormat is not thread-safe, cannot be compile-time created, and is + // relatively expensive to create during preloading. This method is only used + // when dumping a connection, which is a rare (mainly error) case. So: + // DO NOT CACHE. + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(mStartWallTime)); + } + } +} diff --git a/src/main/java/android/database/sqlite/SQLiteConnectionPool.java b/src/main/java/android/database/sqlite/SQLiteConnectionPool.java new file mode 100644 index 0000000..bf5202d --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteConnectionPool.java @@ -0,0 +1,1086 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +import android.database.sqlite.CloseGuard; + +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.CancellationSignal; +import android.os.OperationCanceledException; +import android.os.SystemClock; +import android.util.Log; +import android.util.Printer; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.LockSupport; + +/** + * Maintains a pool of active SQLite database connections. + *

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

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

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

+ * + *

Exception safety

+ *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * + * @param connection The connection to release. Must not be null. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public void releaseConnection(SQLiteConnection connection) { + synchronized (mLock) { + AcquiredConnectionStatus status = mAcquiredConnections.remove(connection); + if (status == null) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + closeConnectionAndLogExceptionsLocked(connection); + } else if (connection.isPrimaryConnection()) { + if (recycleConnectionLocked(connection, status)) { + assert mAvailablePrimaryConnection == null; + mAvailablePrimaryConnection = connection; + } + wakeConnectionWaitersLocked(); + } else if (mAvailableNonPrimaryConnections.size() >= mMaxConnectionPoolSize - 1) { + closeConnectionAndLogExceptionsLocked(connection); + } else { + if (recycleConnectionLocked(connection, status)) { + mAvailableNonPrimaryConnections.add(connection); + } + wakeConnectionWaitersLocked(); + } + } + } + + // Can't throw. + private boolean recycleConnectionLocked(SQLiteConnection connection, + AcquiredConnectionStatus status) { + if (status == AcquiredConnectionStatus.RECONFIGURE) { + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure released connection, closing it: " + + connection, ex); + status = AcquiredConnectionStatus.DISCARD; + } + } + if (status == AcquiredConnectionStatus.DISCARD) { + closeConnectionAndLogExceptionsLocked(connection); + return false; + } + return true; + } + + /** + * Returns true if the session should yield the connection due to + * contention over available database connections. + * + * @param connection The connection owned by the session. + * @param connectionFlags The connection request flags. + * @return True if the session should yield its connection. + * + * @throws IllegalStateException if the connection was not acquired + * from this pool or if it has already been released. + */ + public boolean shouldYieldConnection(SQLiteConnection connection, int connectionFlags) { + synchronized (mLock) { + if (!mAcquiredConnections.containsKey(connection)) { + throw new IllegalStateException("Cannot perform this operation " + + "because the specified connection was not acquired " + + "from this pool or has already been released."); + } + + if (!mIsOpen) { + return false; + } + + return isSessionBlockingImportantConnectionWaitersLocked( + connection.isPrimaryConnection(), connectionFlags); + } + } + + /** + * Collects statistics about database connection memory usage. + * + * @param dbStatsList The list to populate. + */ + public void collectDbStats(ArrayList dbStatsList) { + synchronized (mLock) { + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAvailableNonPrimaryConnections) { + connection.collectDbStats(dbStatsList); + } + + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + connection.collectDbStatsUnsafe(dbStatsList); + } + } + } + + // Might throw. + private SQLiteConnection openConnectionLocked(SQLiteDatabaseConfiguration configuration, + boolean primaryConnection) { + final int connectionId = mNextConnectionId++; + return SQLiteConnection.open(this, configuration, + connectionId, primaryConnection); // might throw + } + + void onConnectionLeaked() { + // This code is running inside of the SQLiteConnection finalizer. + // + // We don't know whether it is just the connection that has been finalized (and leaked) + // or whether the connection pool has also been or is about to be finalized. + // Consequently, it would be a bad idea to try to grab any locks or to + // do any significant work here. So we do the simplest possible thing and + // set a flag. waitForConnection() periodically checks this flag (when it + // times out) so that it can recover from leaked connections and wake + // itself or other threads up if necessary. + // + // You might still wonder why we don't try to do more to wake up the waiters + // immediately. First, as explained above, it would be hard to do safely + // unless we started an extra Thread to function as a reference queue. Second, + // this is never supposed to happen in normal operation. Third, there is no + // guarantee that the GC will actually detect the leak in a timely manner so + // it's not all that important that we recover from the leak in a timely manner + // either. Fourth, if a badly behaved application finds itself hung waiting for + // several seconds while waiting for a leaked connection to be detected and recreated, + // then perhaps its authors will have added incentive to fix the problem! + + Log.w(TAG, "A SQLiteConnection object for database '" + + mConfiguration.label + "' was leaked! Please fix your application " + + "to end transactions in progress properly and to close the database " + + "when it is no longer needed."); + + mConnectionLeaked.set(true); + } + + // Can't throw. + private void closeAvailableConnectionsAndLogExceptionsLocked() { + closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked(); + + if (mAvailablePrimaryConnection != null) { + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + } + + // Can't throw. + private void closeAvailableNonPrimaryConnectionsAndLogExceptionsLocked() { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + closeConnectionAndLogExceptionsLocked(mAvailableNonPrimaryConnections.get(i)); + } + mAvailableNonPrimaryConnections.clear(); + } + + // Can't throw. + private void closeExcessConnectionsAndLogExceptionsLocked() { + int availableCount = mAvailableNonPrimaryConnections.size(); + while (availableCount-- > mMaxConnectionPoolSize - 1) { + SQLiteConnection connection = + mAvailableNonPrimaryConnections.remove(availableCount); + closeConnectionAndLogExceptionsLocked(connection); + } + } + + // Can't throw. + private void closeConnectionAndLogExceptionsLocked(SQLiteConnection connection) { + try { + connection.close(); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to close connection, its fate is now in the hands " + + "of the merciful GC: " + connection, ex); + } + } + + // Can't throw. + private void discardAcquiredConnectionsLocked() { + markAcquiredConnectionsLocked(AcquiredConnectionStatus.DISCARD); + } + + // Can't throw. + private void reconfigureAllConnectionsLocked() { + if (mAvailablePrimaryConnection != null) { + try { + mAvailablePrimaryConnection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available primary connection, closing it: " + + mAvailablePrimaryConnection, ex); + closeConnectionAndLogExceptionsLocked(mAvailablePrimaryConnection); + mAvailablePrimaryConnection = null; + } + } + + int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + final SQLiteConnection connection = mAvailableNonPrimaryConnections.get(i); + try { + connection.reconfigure(mConfiguration); // might throw + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to reconfigure available non-primary connection, closing it: " + + connection, ex); + closeConnectionAndLogExceptionsLocked(connection); + mAvailableNonPrimaryConnections.remove(i--); + count -= 1; + } + } + + markAcquiredConnectionsLocked(AcquiredConnectionStatus.RECONFIGURE); + } + + // Can't throw. + private void markAcquiredConnectionsLocked(AcquiredConnectionStatus status) { + if (!mAcquiredConnections.isEmpty()) { + ArrayList keysToUpdate = new ArrayList( + mAcquiredConnections.size()); + for (Map.Entry entry + : mAcquiredConnections.entrySet()) { + AcquiredConnectionStatus oldStatus = entry.getValue(); + if (status != oldStatus + && oldStatus != AcquiredConnectionStatus.DISCARD) { + keysToUpdate.add(entry.getKey()); + } + } + final int updateCount = keysToUpdate.size(); + for (int i = 0; i < updateCount; i++) { + mAcquiredConnections.put(keysToUpdate.get(i), status); + } + } + } + + // Might throw. + private SQLiteConnection waitForConnection(String sql, int connectionFlags, + CancellationSignal cancellationSignal) { + final boolean wantPrimaryConnection = + (connectionFlags & CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY) != 0; + + final ConnectionWaiter waiter; + final int nonce; + synchronized (mLock) { + throwIfClosedLocked(); + + // Abort if canceled. + if (cancellationSignal != null) { + cancellationSignal.throwIfCanceled(); + } + + // Try to acquire a connection. + SQLiteConnection connection = null; + if (!wantPrimaryConnection) { + connection = tryAcquireNonPrimaryConnectionLocked( + sql, connectionFlags); // might throw + } + if (connection == null) { + connection = tryAcquirePrimaryConnectionLocked(connectionFlags); // might throw + } + if (connection != null) { + return connection; + } + + // No connections available. Enqueue a waiter in priority order. + final int priority = getPriority(connectionFlags); + final long startTime = SystemClock.uptimeMillis(); + waiter = obtainConnectionWaiterLocked(Thread.currentThread(), startTime, + priority, wantPrimaryConnection, sql, connectionFlags); + ConnectionWaiter predecessor = null; + ConnectionWaiter successor = mConnectionWaiterQueue; + while (successor != null) { + if (priority > successor.mPriority) { + waiter.mNext = successor; + break; + } + predecessor = successor; + successor = successor.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter; + } else { + mConnectionWaiterQueue = waiter; + } + + nonce = waiter.mNonce; + } + + // Set up the cancellation listener. + if (cancellationSignal != null) { + cancellationSignal.setOnCancelListener(new CancellationSignal.OnCancelListener() { + @Override + public void onCancel() { + synchronized (mLock) { + if (waiter.mNonce == nonce) { + cancelConnectionWaiterLocked(waiter); + } + } + } + }); + } + try { + // Park the thread until a connection is assigned or the pool is closed. + // Rethrow an exception from the wait, if we got one. + long busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + long nextBusyTimeoutTime = waiter.mStartTime + busyTimeoutMillis; + for (;;) { + // Detect and recover from connection leaks. + if (mConnectionLeaked.compareAndSet(true, false)) { + synchronized (mLock) { + wakeConnectionWaitersLocked(); + } + } + + // Wait to be unparked (may already have happened), a timeout, or interruption. + LockSupport.parkNanos(this, busyTimeoutMillis * 1000000L); + + // Clear the interrupted flag, just in case. + Thread.interrupted(); + + // Check whether we are done waiting yet. + synchronized (mLock) { + throwIfClosedLocked(); + + final SQLiteConnection connection = waiter.mAssignedConnection; + final RuntimeException ex = waiter.mException; + if (connection != null || ex != null) { + recycleConnectionWaiterLocked(waiter); + if (connection != null) { + return connection; + } + throw ex; // rethrow! + } + + final long now = SystemClock.uptimeMillis(); + if (now < nextBusyTimeoutTime) { + busyTimeoutMillis = now - nextBusyTimeoutTime; + } else { + logConnectionPoolBusyLocked(now - waiter.mStartTime, connectionFlags); + busyTimeoutMillis = CONNECTION_POOL_BUSY_MILLIS; + nextBusyTimeoutTime = now + busyTimeoutMillis; + } + } + } + } finally { + // Remove the cancellation listener. + if (cancellationSignal != null) { + cancellationSignal.setOnCancelListener(null); + } + } + } + + // Can't throw. + private void cancelConnectionWaiterLocked(ConnectionWaiter waiter) { + if (waiter.mAssignedConnection != null || waiter.mException != null) { + // Waiter is done waiting but has not woken up yet. + return; + } + + // Waiter must still be waiting. Dequeue it. + ConnectionWaiter predecessor = null; + ConnectionWaiter current = mConnectionWaiterQueue; + while (current != waiter) { + assert current != null; + predecessor = current; + current = current.mNext; + } + if (predecessor != null) { + predecessor.mNext = waiter.mNext; + } else { + mConnectionWaiterQueue = waiter.mNext; + } + + // Send the waiter an exception and unpark it. + waiter.mException = new OperationCanceledException(); + LockSupport.unpark(waiter.mThread); + + // Check whether removing this waiter will enable other waiters to make progress. + wakeConnectionWaitersLocked(); + } + + // Can't throw. + private void logConnectionPoolBusyLocked(long waitMillis, int connectionFlags) { + final Thread thread = Thread.currentThread(); + StringBuilder msg = new StringBuilder(); + msg.append("The connection pool for database '").append(mConfiguration.label); + msg.append("' has been unable to grant a connection to thread "); + msg.append(thread.getId()).append(" (").append(thread.getName()).append(") "); + msg.append("with flags 0x").append(Integer.toHexString(connectionFlags)); + msg.append(" for ").append(waitMillis * 0.001f).append(" seconds.\n"); + + ArrayList requests = new ArrayList(); + int activeConnections = 0; + int idleConnections = 0; + if (!mAcquiredConnections.isEmpty()) { + for (SQLiteConnection connection : mAcquiredConnections.keySet()) { + String description = connection.describeCurrentOperationUnsafe(); + if (description != null) { + requests.add(description); + activeConnections += 1; + } else { + idleConnections += 1; + } + } + } + int availableConnections = mAvailableNonPrimaryConnections.size(); + if (mAvailablePrimaryConnection != null) { + availableConnections += 1; + } + + msg.append("Connections: ").append(activeConnections).append(" active, "); + msg.append(idleConnections).append(" idle, "); + msg.append(availableConnections).append(" available.\n"); + + if (!requests.isEmpty()) { + msg.append("\nRequests in progress:\n"); + for (String request : requests) { + msg.append(" ").append(request).append("\n"); + } + } + + Log.w(TAG, msg.toString()); + } + + // Can't throw. + private void wakeConnectionWaitersLocked() { + // Unpark all waiters that have requests that we can fulfill. + // This method is designed to not throw runtime exceptions, although we might send + // a waiter an exception for it to rethrow. + ConnectionWaiter predecessor = null; + ConnectionWaiter waiter = mConnectionWaiterQueue; + boolean primaryConnectionNotAvailable = false; + boolean nonPrimaryConnectionNotAvailable = false; + while (waiter != null) { + boolean unpark = false; + if (!mIsOpen) { + unpark = true; + } else { + try { + SQLiteConnection connection = null; + if (!waiter.mWantPrimaryConnection && !nonPrimaryConnectionNotAvailable) { + connection = tryAcquireNonPrimaryConnectionLocked( + waiter.mSql, waiter.mConnectionFlags); // might throw + if (connection == null) { + nonPrimaryConnectionNotAvailable = true; + } + } + if (connection == null && !primaryConnectionNotAvailable) { + connection = tryAcquirePrimaryConnectionLocked( + waiter.mConnectionFlags); // might throw + if (connection == null) { + primaryConnectionNotAvailable = true; + } + } + if (connection != null) { + waiter.mAssignedConnection = connection; + unpark = true; + } else if (nonPrimaryConnectionNotAvailable && primaryConnectionNotAvailable) { + // There are no connections available and the pool is still open. + // We cannot fulfill any more connection requests, so stop here. + break; + } + } catch (RuntimeException ex) { + // Let the waiter handle the exception from acquiring a connection. + waiter.mException = ex; + unpark = true; + } + } + + final ConnectionWaiter successor = waiter.mNext; + if (unpark) { + if (predecessor != null) { + predecessor.mNext = successor; + } else { + mConnectionWaiterQueue = successor; + } + waiter.mNext = null; + + LockSupport.unpark(waiter.mThread); + } else { + predecessor = waiter; + } + waiter = successor; + } + } + + // Might throw. + private SQLiteConnection tryAcquirePrimaryConnectionLocked(int connectionFlags) { + // If the primary connection is available, acquire it now. + SQLiteConnection connection = mAvailablePrimaryConnection; + if (connection != null) { + mAvailablePrimaryConnection = null; + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Make sure that the primary connection actually exists and has just been acquired. + for (SQLiteConnection acquiredConnection : mAcquiredConnections.keySet()) { + if (acquiredConnection.isPrimaryConnection()) { + return null; + } + } + + // Uhoh. No primary connection! Either this is the first time we asked + // for it, or maybe it leaked? + connection = openConnectionLocked(mConfiguration, + true /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private SQLiteConnection tryAcquireNonPrimaryConnectionLocked( + String sql, int connectionFlags) { + // Try to acquire the next connection in the queue. + SQLiteConnection connection; + final int availableCount = mAvailableNonPrimaryConnections.size(); + if (availableCount > 1 && sql != null) { + // If we have a choice, then prefer a connection that has the + // prepared statement in its cache. + for (int i = 0; i < availableCount; i++) { + connection = mAvailableNonPrimaryConnections.get(i); + if (connection.isPreparedStatementInCache(sql)) { + mAvailableNonPrimaryConnections.remove(i); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + } + } + if (availableCount > 0) { + // Otherwise, just grab the next one. + connection = mAvailableNonPrimaryConnections.remove(availableCount - 1); + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Expand the pool if needed. + int openConnections = mAcquiredConnections.size(); + if (mAvailablePrimaryConnection != null) { + openConnections += 1; + } + if (openConnections >= mMaxConnectionPoolSize) { + return null; + } + connection = openConnectionLocked(mConfiguration, + false /*primaryConnection*/); // might throw + finishAcquireConnectionLocked(connection, connectionFlags); // might throw + return connection; + } + + // Might throw. + private void finishAcquireConnectionLocked(SQLiteConnection connection, int connectionFlags) { + try { + final boolean readOnly = (connectionFlags & CONNECTION_FLAG_READ_ONLY) != 0; + connection.setOnlyAllowReadOnlyOperations(readOnly); + + mAcquiredConnections.put(connection, AcquiredConnectionStatus.NORMAL); + } catch (RuntimeException ex) { + Log.e(TAG, "Failed to prepare acquired connection for session, closing it: " + + connection +", connectionFlags=" + connectionFlags); + closeConnectionAndLogExceptionsLocked(connection); + throw ex; // rethrow! + } + } + + private boolean isSessionBlockingImportantConnectionWaitersLocked( + boolean holdingPrimaryConnection, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterQueue; + if (waiter != null) { + final int priority = getPriority(connectionFlags); + do { + // Only worry about blocked connections that have same or lower priority. + if (priority > waiter.mPriority) { + break; + } + + // If we are holding the primary connection then we are blocking the waiter. + // Likewise, if we are holding a non-primary connection and the waiter + // would accept a non-primary connection, then we are blocking the waier. + if (holdingPrimaryConnection || !waiter.mWantPrimaryConnection) { + return true; + } + + waiter = waiter.mNext; + } while (waiter != null); + } + return false; + } + + private static int getPriority(int connectionFlags) { + return (connectionFlags & CONNECTION_FLAG_INTERACTIVE) != 0 ? 1 : 0; + } + + private void setMaxConnectionPoolSizeLocked() { + if ((mConfiguration.openFlags & SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) != 0) { + mMaxConnectionPoolSize = SQLiteGlobal.getWALConnectionPoolSize(); + } else { + // TODO: We don't actually need to restrict the connection pool size to 1 + // for non-WAL databases. There might be reasons to use connection pooling + // with other journal modes. For now, enabling connection pooling and + // using WAL are the same thing in the API. + mMaxConnectionPoolSize = 1; + } + } + + private void throwIfClosedLocked() { + if (!mIsOpen) { + throw new IllegalStateException("Cannot perform this operation " + + "because the connection pool has been closed."); + } + } + + private ConnectionWaiter obtainConnectionWaiterLocked(Thread thread, long startTime, + int priority, boolean wantPrimaryConnection, String sql, int connectionFlags) { + ConnectionWaiter waiter = mConnectionWaiterPool; + if (waiter != null) { + mConnectionWaiterPool = waiter.mNext; + waiter.mNext = null; + } else { + waiter = new ConnectionWaiter(); + } + waiter.mThread = thread; + waiter.mStartTime = startTime; + waiter.mPriority = priority; + waiter.mWantPrimaryConnection = wantPrimaryConnection; + waiter.mSql = sql; + waiter.mConnectionFlags = connectionFlags; + return waiter; + } + + private void recycleConnectionWaiterLocked(ConnectionWaiter waiter) { + waiter.mNext = mConnectionWaiterPool; + waiter.mThread = null; + waiter.mSql = null; + waiter.mAssignedConnection = null; + waiter.mException = null; + waiter.mNonce += 1; + mConnectionWaiterPool = waiter; + } + + public void enableLocalizedCollators() { + synchronized (mLock) { + if( !mAcquiredConnections.isEmpty() || mAvailablePrimaryConnection==null ) { + throw new IllegalStateException( + "Cannot enable localized collators while database is in use" + ); + } + mAvailablePrimaryConnection.enableLocalizedCollators(); + } + } + + /** + * Dumps debugging information about this connection pool. + * + * @param printer The printer to receive the dump, not null. + * @param verbose True to dump more verbose information. + */ + public void dump(Printer printer, boolean verbose) { + Printer indentedPrinter = printer; + synchronized (mLock) { + printer.println("Connection pool for " + mConfiguration.path + ":"); + printer.println(" Open: " + mIsOpen); + printer.println(" Max connections: " + mMaxConnectionPoolSize); + + printer.println(" Available primary connection:"); + if (mAvailablePrimaryConnection != null) { + mAvailablePrimaryConnection.dump(indentedPrinter, verbose); + } else { + indentedPrinter.println(""); + } + + printer.println(" Available non-primary connections:"); + if (!mAvailableNonPrimaryConnections.isEmpty()) { + final int count = mAvailableNonPrimaryConnections.size(); + for (int i = 0; i < count; i++) { + mAvailableNonPrimaryConnections.get(i).dump(indentedPrinter, verbose); + } + } else { + indentedPrinter.println(""); + } + + printer.println(" Acquired connections:"); + if (!mAcquiredConnections.isEmpty()) { + for (Map.Entry entry : + mAcquiredConnections.entrySet()) { + final SQLiteConnection connection = entry.getKey(); + connection.dumpUnsafe(indentedPrinter, verbose); + indentedPrinter.println(" Status: " + entry.getValue()); + } + } else { + indentedPrinter.println(""); + } + + printer.println(" Connection waiters:"); + if (mConnectionWaiterQueue != null) { + int i = 0; + final long now = SystemClock.uptimeMillis(); + for (ConnectionWaiter waiter = mConnectionWaiterQueue; waiter != null; + waiter = waiter.mNext, i++) { + indentedPrinter.println(i + ": waited for " + + ((now - waiter.mStartTime) * 0.001f) + + " ms - thread=" + waiter.mThread + + ", priority=" + waiter.mPriority + + ", sql='" + waiter.mSql + "'"); + } + } else { + indentedPrinter.println(""); + } + } + } + + @Override + public String toString() { + return "SQLiteConnectionPool: " + mConfiguration.path; + } + + private static final class ConnectionWaiter { + public ConnectionWaiter mNext; + public Thread mThread; + public long mStartTime; + public int mPriority; + public boolean mWantPrimaryConnection; + public String mSql; + public int mConnectionFlags; + public SQLiteConnection mAssignedConnection; + public RuntimeException mException; + public int mNonce; + } +} diff --git a/src/main/java/android/database/sqlite/SQLiteConstraintException.java b/src/main/java/android/database/sqlite/SQLiteConstraintException.java new file mode 100644 index 0000000..e5260b2 --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteConstraintException.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * An exception that indicates that an integrity constraint was violated. + */ +public class SQLiteConstraintException extends SQLiteException { + public SQLiteConstraintException() {} + + public SQLiteConstraintException(String error) { + super(error); + } +} diff --git a/src/main/java/android/database/sqlite/SQLiteCursor.java b/src/main/java/android/database/sqlite/SQLiteCursor.java new file mode 100644 index 0000000..bb4b93d --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteCursor.java @@ -0,0 +1,284 @@ +/* + * 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.AbstractWindowedCursor; +import android.database.CursorWindow; +import android.database.DatabaseUtils; + +import android.os.StrictMode; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; + +/** + * A Cursor implementation that exposes results from a query on a + * {@link SQLiteDatabase}. + * + * SQLiteCursor is not internally synchronized so code using a SQLiteCursor from multiple + * threads should perform its own synchronization when using the SQLiteCursor. + */ +public class SQLiteCursor extends AbstractWindowedCursor { + static final String TAG = "SQLiteCursor"; + static final int NO_COUNT = -1; + + /** The name of the table to edit */ + private final String mEditTable; + + /** The names of the columns in the rows */ + private final String[] mColumns; + + /** The query object for the cursor */ + private final SQLiteQuery mQuery; + + /** The compiled query this cursor came from */ + private final SQLiteCursorDriver mDriver; + + /** The number of rows in the cursor */ + private int mCount = NO_COUNT; + + /** The number of rows that can fit in the cursor window, 0 if unknown */ + private int mCursorWindowCapacity; + + /** A mapping of column names to column indices, to speed up lookups */ + private Map mColumnNameMap; + + /** + * Execute a query and provide access to its result set through a Cursor + * interface. For a query such as: {@code SELECT name, birth, phone FROM + * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, + * phone) would be in the projection argument and everything from + * {@code FROM} onward would be in the params argument. + * + * @param db a reference to a Database object that is already constructed + * and opened. This param is not used any longer + * @param editTable the name of the table used for this query + * @param query the rest of the query terms + * cursor is finalized + * @deprecated use {@link #SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)} instead + */ + @Deprecated + public SQLiteCursor(SQLiteDatabase db, SQLiteCursorDriver driver, + String editTable, SQLiteQuery query) { + this(driver, editTable, query); + } + + /** + * Execute a query and provide access to its result set through a Cursor + * interface. For a query such as: {@code SELECT name, birth, phone FROM + * myTable WHERE ... LIMIT 1,20 ORDER BY...} the column names (name, birth, + * phone) would be in the projection argument and everything from + * {@code FROM} onward would be in the params argument. + * + * @param editTable the name of the table used for this query + * @param query the {@link SQLiteQuery} object associated with this cursor object. + */ + public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) { + if (query == null) { + throw new IllegalArgumentException("query object cannot be null"); + } + mDriver = driver; + mEditTable = editTable; + mColumnNameMap = null; + mQuery = query; + + mColumns = query.getColumnNames(); + } + + /** + * Get the database that this cursor is associated with. + * @return the SQLiteDatabase that this cursor is associated with. + */ + public SQLiteDatabase getDatabase() { + return mQuery.getDatabase(); + } + + @Override + public boolean onMove(int oldPosition, int newPosition) { + // Make sure the row at newPosition is present in the window + if (mWindow == null || newPosition < mWindow.getStartPosition() || + newPosition >= (mWindow.getStartPosition() + mWindow.getNumRows())) { + fillWindow(newPosition); + } + + return true; + } + + @Override + public int getCount() { + if (mCount == NO_COUNT) { + fillWindow(0); + } + return mCount; + } + + /* + ** The AbstractWindowClass contains protected methods clearOrCreateWindow() and + ** closeWindow(), which are used by the android.database.sqlite.* version of this + ** class. But, since they are marked with "@hide", the following replacement + ** versions are required. + */ + private void awc_clearOrCreateWindow(String name){ + CursorWindow win = getWindow(); + if( win==null ){ + win = new CursorWindow(name); + setWindow(win); + }else{ + win.clear(); + } + } + private void awc_closeWindow(){ + setWindow(null); + } + + private void fillWindow(int requiredPos) { + awc_clearOrCreateWindow(getDatabase().getPath()); + + try { + if (mCount == NO_COUNT) { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, 0); + mCount = mQuery.fillWindow(mWindow, startPos, requiredPos, true); + mCursorWindowCapacity = mWindow.getNumRows(); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "received count(*) from native_fill_window: " + mCount); + } + } else { + int startPos = DatabaseUtils.cursorPickFillWindowStartPosition(requiredPos, + mCursorWindowCapacity); + mQuery.fillWindow(mWindow, startPos, requiredPos, false); + } + } catch (RuntimeException ex) { + // Close the cursor window if the query failed and therefore will + // not produce any results. This helps to avoid accidentally leaking + // the cursor window if the client does not correctly handle exceptions + // and fails to close the cursor. + awc_closeWindow(); + throw ex; + } + } + + @Override + public int getColumnIndex(String columnName) { + // Create mColumnNameMap on demand + if (mColumnNameMap == null) { + String[] columns = mColumns; + int columnCount = columns.length; + HashMap map = new HashMap(columnCount, 1); + for (int i = 0; i < columnCount; i++) { + map.put(columns[i], i); + } + mColumnNameMap = map; + } + + // Hack according to bug 903852 + final int periodIndex = columnName.lastIndexOf('.'); + if (periodIndex != -1) { + Exception e = new Exception(); + Log.e(TAG, "requesting column name with table name -- " + columnName, e); + columnName = columnName.substring(periodIndex + 1); + } + + Integer i = mColumnNameMap.get(columnName); + if (i != null) { + return i.intValue(); + } else { + return -1; + } + } + + @Override + public String[] getColumnNames() { + return mColumns; + } + + @Override + public void deactivate() { + super.deactivate(); + mDriver.cursorDeactivated(); + } + + @Override + public void close() { + super.close(); + synchronized (this) { + mQuery.close(); + mDriver.cursorClosed(); + } + } + + @Override + public boolean requery() { + if (isClosed()) { + return false; + } + + synchronized (this) { + if (!mQuery.getDatabase().isOpen()) { + return false; + } + + if (mWindow != null) { + mWindow.clear(); + } + mPos = -1; + mCount = NO_COUNT; + + mDriver.cursorRequeried(this); + } + + try { + return super.requery(); + } catch (IllegalStateException e) { + // for backwards compatibility, just return false + Log.w(TAG, "requery() failed " + e.getMessage(), e); + return false; + } + } + + @Override + public void setWindow(CursorWindow window) { + super.setWindow(window); + mCount = NO_COUNT; + } + + /** + * Changes the selection arguments. The new values take effect after a call to requery(). + */ + public void setSelectionArguments(String[] selectionArgs) { + mDriver.setBindArguments(selectionArgs); + } + + /** + * Release the native resources, if they haven't been released yet. + */ + @Override + protected void finalize() { + try { + // if the cursor hasn't been closed yet, close it first + if (mWindow != null) { + close(); + } + } finally { + super.finalize(); + } + } +} diff --git a/src/main/java/android/database/sqlite/SQLiteCursorDriver.java b/src/main/java/android/database/sqlite/SQLiteCursorDriver.java new file mode 100644 index 0000000..9741a35 --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteCursorDriver.java @@ -0,0 +1,60 @@ +/* + * 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; + +/** + * A driver for SQLiteCursors that is used to create them and gets notified + * by the cursors it creates on significant events in their lifetimes. + */ +public interface SQLiteCursorDriver { + /** + * Executes the query returning a Cursor over the result set. + * + * @param factory The CursorFactory to use when creating the Cursors, or + * null if standard SQLiteCursors should be returned. + * @return a Cursor over the result set + */ + Cursor query(CursorFactory factory, String[] bindArgs); + + /** + * Called by a SQLiteCursor when it is released. + */ + void cursorDeactivated(); + + /** + * Called by a SQLiteCursor when it is requeried. + */ + void cursorRequeried(Cursor cursor); + + /** + * Called by a SQLiteCursor when it it closed to destroy this object as well. + */ + void cursorClosed(); + + /** + * Set new bind arguments. These will take effect in cursorRequeried(). + * @param bindArgs the new arguments + */ + public void setBindArguments(String[] bindArgs); +} diff --git a/src/main/java/android/database/sqlite/SQLiteCustomFunction.java b/src/main/java/android/database/sqlite/SQLiteCustomFunction.java new file mode 100644 index 0000000..65a02eb --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteCustomFunction.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* +** Modified to support SQLite extensions by the SQLite developers: +** sqlite-dev@sqlite.org. +*/ + +package android.database.sqlite; + +/** + * Describes a custom SQL function. + * + * @hide + */ +public final class SQLiteCustomFunction { + public final String name; + public final int numArgs; + public final SQLiteDatabase.CustomFunction callback; + + /** + * Create custom function. + * + * @param name The name of the sqlite3 function. + * @param numArgs The number of arguments for the function, or -1 to + * support any number of arguments. + * @param callback The callback to invoke when the function is executed. + */ + public SQLiteCustomFunction(String name, int numArgs, + SQLiteDatabase.CustomFunction callback) { + if (name == null) { + throw new IllegalArgumentException("name must not be null."); + } + + this.name = name; + this.numArgs = numArgs; + this.callback = callback; + } + + // Called from native. + @SuppressWarnings("unused") + private void dispatchCallback(String[] args) { + callback.callback(args); + } +} diff --git a/src/main/java/android/database/sqlite/SQLiteDatabase.java b/src/main/java/android/database/sqlite/SQLiteDatabase.java new file mode 100644 index 0000000..3a63f93 --- /dev/null +++ b/src/main/java/android/database/sqlite/SQLiteDatabase.java @@ -0,0 +1,2223 @@ +/* + * 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.content.ContentValues; +import android.database.Cursor; +import android.database.DatabaseErrorHandler; +import android.database.DatabaseUtils; +import android.database.DefaultDatabaseErrorHandler; +import android.database.SQLException; +import android.database.sqlite.SQLiteDebug.DbStats; +import android.os.CancellationSignal; +import android.os.Looper; +import android.os.OperationCanceledException; +import android.text.TextUtils; +import android.util.EventLog; +import android.util.Log; +import android.util.Pair; +import android.util.Printer; + +import android.database.sqlite.CloseGuard; + +import java.io.File; +import java.io.FileFilter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Exposes methods to manage a SQLite database. + * + *

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

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

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

+ * + *

Localized Collation - ORDER BY

+ *

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

+ */ +public final class SQLiteDatabase extends SQLiteClosable { + private static final String TAG = "SQLiteDatabase"; + + private static final int EVENT_DB_CORRUPT = 75004; + + // Stores reference to all databases opened in the current process. + // (The referent Object is not used at this time.) + // INVARIANT: Guarded by sActiveDatabases. + private static WeakHashMap sActiveDatabases = + new WeakHashMap(); + + // Thread-local for database sessions that belong to this database. + // Each thread has its own database session. + // INVARIANT: Immutable. + private final ThreadLocal mThreadSession = new ThreadLocal() { + @Override + protected SQLiteSession initialValue() { + return createSession(); + } + }; + + // The optional factory to use when creating new Cursors. May be null. + // INVARIANT: Immutable. + private final CursorFactory mCursorFactory; + + // Error handler to be used when SQLite returns corruption errors. + // INVARIANT: Immutable. + private final DatabaseErrorHandler mErrorHandler; + + // Shared database state lock. + // This lock guards all of the shared state of the database, such as its + // configuration, whether it is open or closed, and so on. This lock should + // be held for as little time as possible. + // + // The lock MUST NOT be held while attempting to acquire database connections or + // while executing SQL statements on behalf of the client as it can lead to deadlock. + // + // It is ok to hold the lock while reconfiguring the connection pool or dumping + // statistics because those operations are non-reentrant and do not try to acquire + // connections that might be held by other threads. + // + // Basic rule: grab the lock, access or modify global state, release the lock, then + // do the required SQL work. + private final Object mLock = new Object(); + + // Warns if the database is finalized without being closed properly. + // INVARIANT: Guarded by mLock. + private final CloseGuard mCloseGuardLocked = CloseGuard.get(); + + // The database configuration. + // INVARIANT: Guarded by mLock. + private final SQLiteDatabaseConfiguration mConfigurationLocked; + + // The connection pool for the database, null when closed. + // The pool itself is thread-safe, but the reference to it can only be acquired + // when the lock is held. + // INVARIANT: Guarded by mLock. + private SQLiteConnectionPool mConnectionPoolLocked; + + // True if the database has attached databases. + // INVARIANT: Guarded by mLock. + private boolean mHasAttachedDbsLocked; + + /** + * When a constraint violation occurs, an immediate ROLLBACK occurs, + * thus ending the current transaction, and the command aborts with a + * return code of SQLITE_CONSTRAINT. If no transaction is active + * (other than the implied transaction that is created on every command) + * then this algorithm works the same as ABORT. + */ + public static final int CONFLICT_ROLLBACK = 1; + + /** + * When a constraint violation occurs,no ROLLBACK is executed + * so changes from prior commands within the same transaction + * are preserved. This is the default behavior. + */ + public static final int CONFLICT_ABORT = 2; + + /** + * When a constraint violation occurs, the command aborts with a return + * code SQLITE_CONSTRAINT. But any changes to the database that + * the command made prior to encountering the constraint violation + * are preserved and are not backed out. + */ + public static final int CONFLICT_FAIL = 3; + + /** + * When a constraint violation occurs, the one row that contains + * the constraint violation is not inserted or changed. + * But the command continues executing normally. Other rows before and + * after the row that contained the constraint violation continue to be + * inserted or updated normally. No error is returned. + */ + public static final int CONFLICT_IGNORE = 4; + + /** + * When a UNIQUE constraint violation occurs, the pre-existing rows that + * are causing the constraint violation are removed prior to inserting + * or updating the current row. Thus the insert or update always occurs. + * The command continues executing normally. No error is returned. + * If a NOT NULL constraint violation occurs, the NULL value is replaced + * by the default value for that column. If the column has no default + * value, then the ABORT algorithm is used. If a CHECK constraint + * violation occurs then the IGNORE algorithm is used. When this conflict + * resolution strategy deletes rows in order to satisfy a constraint, + * it does not invoke delete triggers on those rows. + * This behavior might change in a future release. + */ + public static final int CONFLICT_REPLACE = 5; + + /** + * Use the following when no conflict action is specified. + */ + public static final int CONFLICT_NONE = 0; + + private static final String[] CONFLICT_VALUES = new String[] + {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; + + /** + * Maximum Length Of A LIKE Or GLOB Pattern + * The pattern matching algorithm used in the default LIKE and GLOB implementation + * of SQLite can exhibit O(N^2) performance (where N is the number of characters in + * the pattern) for certain pathological cases. To avoid denial-of-service attacks + * the length of the LIKE or GLOB pattern is limited to SQLITE_MAX_LIKE_PATTERN_LENGTH bytes. + * The default value of this limit is 50000. A modern workstation can evaluate + * even a pathological LIKE or GLOB pattern of 50000 bytes relatively quickly. + * The denial of service problem only comes into play when the pattern length gets + * into millions of bytes. Nevertheless, since most useful LIKE or GLOB patterns + * are at most a few dozen bytes in length, paranoid application developers may + * want to reduce this parameter to something in the range of a few hundred + * if they know that external users are able to generate arbitrary patterns. + */ + public static final int SQLITE_MAX_LIKE_PATTERN_LENGTH = 50000; + + /** + * Open flag: Flag for {@link #openDatabase} to open the database for reading and writing. + * If the disk is full, this may fail even before you actually write anything. + * + * {@more} Note that the value of this flag is 0, so it is the default. + */ + public static final int OPEN_READWRITE = 0x00000000; // update native code if changing + + /** + * Open flag: Flag for {@link #openDatabase} to open the database for reading only. + * This is the only reliable way to open a database if the disk may be full. + */ + public static final int OPEN_READONLY = 0x00000001; // update native code if changing + + private static final int OPEN_READ_MASK = 0x00000001; // update native code if changing + + /** + * Open flag: Flag for {@link #openDatabase} to open the database without support for + * localized collators. + * + * {@more} This causes the collator LOCALIZED not to be created. + * You must be consistent when using this flag to use the setting the database was + * created with. If this is set, {@link #setLocale} will do nothing. + */ + public static final int NO_LOCALIZED_COLLATORS = 0x00000010; // update native code if changing + + /** + * Open flag: Flag for {@link #openDatabase} to create the database file if it does not + * already exist. + */ + public static final int CREATE_IF_NECESSARY = 0x10000000; // update native code if changing + + /** + * Open flag: Flag for {@link #openDatabase} to open the database file with + * write-ahead logging enabled by default. Using this flag is more efficient + * than calling {@link #enableWriteAheadLogging}. + * + * 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. + * + * @see #enableWriteAheadLogging + */ + public static final int ENABLE_WRITE_AHEAD_LOGGING = 0x20000000; + + /** + * Absolute max value that can be set by {@link #setMaxSqlCacheSize(int)}. + * + * Each prepared-statement is between 1K - 6K, depending on the complexity of the + * SQL statement & schema. A large SQL cache may use a significant amount of memory. + */ + public static final int MAX_SQL_CACHE_SIZE = 100; + + private SQLiteDatabase(String path, int openFlags, CursorFactory cursorFactory, + DatabaseErrorHandler errorHandler) { + mCursorFactory = cursorFactory; + mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler(); + mConfigurationLocked = new SQLiteDatabaseConfiguration(path, openFlags); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(true); + } finally { + super.finalize(); + } + } + + @Override + protected void onAllReferencesReleased() { + dispose(false); + } + + private void dispose(boolean finalized) { + final SQLiteConnectionPool pool; + synchronized (mLock) { + if (mCloseGuardLocked != null) { + if (finalized) { + mCloseGuardLocked.warnIfOpen(); + } + mCloseGuardLocked.close(); + } + + pool = mConnectionPoolLocked; + mConnectionPoolLocked = null; + } + + if (!finalized) { + synchronized (sActiveDatabases) { + sActiveDatabases.remove(this); + } + + if (pool != null) { + pool.close(); + } + } + } + + /** + * Attempts to release memory that SQLite holds but does not require to + * operate properly. Typically this memory will come from the page cache. + * + * @return the number of bytes actually released + */ + public static int releaseMemory() { + return SQLiteGlobal.releaseMemory(); + } + + /** + * Control whether or not the SQLiteDatabase is made thread-safe by using locks + * around critical sections. This is pretty expensive, so if you know that your + * DB will only be used by a single thread then you should set this to false. + * The default is true. + * @param lockingEnabled set to true to enable locks, false otherwise + * + * @deprecated This method now does nothing. Do not use. + */ + @Deprecated + public void setLockingEnabled(boolean lockingEnabled) { + } + + /** + * Gets a label to use when describing the database in log messages. + * @return The label. + */ + String getLabel() { + synchronized (mLock) { + return mConfigurationLocked.label; + } + } + + /** + * Sends a corruption message to the database error handler. + */ + void onCorruption() { + EventLog.writeEvent(EVENT_DB_CORRUPT, getLabel()); + mErrorHandler.onCorruption(this); + } + + /** + * Gets the {@link SQLiteSession} that belongs to this thread for this database. + * Once a thread has obtained a session, it will continue to obtain the same + * session even after the database has been closed (although the session will not + * be usable). However, a thread that does not already have a session cannot + * obtain one after the database has been closed. + * + * The idea is that threads that have active connections to the database may still + * have work to complete even after the call to {@link #close}. Active database + * connections are not actually disposed until they are released by the threads + * that own them. + * + * @return The session, never null. + * + * @throws IllegalStateException if the thread does not yet have a session and + * the database is not open. + */ + SQLiteSession getThreadSession() { + return mThreadSession.get(); // initialValue() throws if database closed + } + + SQLiteSession createSession() { + final SQLiteConnectionPool pool; + synchronized (mLock) { + throwIfNotOpenLocked(); + pool = mConnectionPoolLocked; + } + return new SQLiteSession(pool); + } + + /** + * Gets default connection flags that are appropriate for this thread, taking into + * account whether the thread is acting on behalf of the UI. + * + * @param readOnly True if the connection should be read-only. + * @return The connection flags. + */ + int getThreadDefaultConnectionFlags(boolean readOnly) { + int flags = readOnly ? SQLiteConnectionPool.CONNECTION_FLAG_READ_ONLY : + SQLiteConnectionPool.CONNECTION_FLAG_PRIMARY_CONNECTION_AFFINITY; + if (isMainThread()) { + flags |= SQLiteConnectionPool.CONNECTION_FLAG_INTERACTIVE; + } + return flags; + } + + private static boolean isMainThread() { + // FIXME: There should be a better way to do this. + // Would also be nice to have something that would work across Binder calls. + Looper looper = Looper.myLooper(); + return looper != null && looper == Looper.getMainLooper(); + } + + /** + * Begins a transaction in EXCLUSIVE mode. + *

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

+ *

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

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

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

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

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

+ *

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

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

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

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

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

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

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

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

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

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

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

+ * + *

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

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

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

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

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

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

+ *

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

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

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

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

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

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

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

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

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

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

+ *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * Note that 'pragma integrity_check' on a database can take a long time. + * + * @return true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + */ + public boolean isDatabaseIntegrityOk() { + acquireReference(); + try { + List> attachedDbs = null; + try { + attachedDbs = getAttachedDbs(); + if (attachedDbs == null) { + throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " + + "be retrieved. probably because the database is closed"); + } + } catch (SQLiteException e) { + // can't get attachedDb list. do integrity check on the main database + attachedDbs = new ArrayList>(); + attachedDbs.add(new Pair("main", getPath())); + } + + for (int i = 0; i < attachedDbs.size(); i++) { + Pair p = attachedDbs.get(i); + SQLiteStatement prog = null; + try { + prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);"); + String rslt = prog.simpleQueryForString(); + if (!rslt.equalsIgnoreCase("ok")) { + // integrity_checker failed on main or attached databases + Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt); + return false; + } + } finally { + if (prog != null) prog.close(); + } + } + } finally { + releaseReference(); + } + return true; + } + + @Override + public String toString() { + return "SQLiteDatabase: " + getPath(); + } + + private void throwIfNotOpenLocked() { + if (mConnectionPoolLocked == null) { + throw new IllegalStateException("The database '" + mConfigurationLocked.label + + "' is not open."); + } + } + + /** + * Used to allow returning sub-classes of {@link Cursor} when calling query. + */ + public interface CursorFactory { + /** + * See {@link SQLiteCursor#SQLiteCursor(SQLiteCursorDriver, String, SQLiteQuery)}. + */ + public Cursor newCursor(SQLiteDatabase db, + SQLiteCursorDriver masterQuery, String editTable, + SQLiteQuery query); + } + + /** + * 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 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 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 map) { + ensureCapacity(mSize + map.size()); + for (Map.Entry 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 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> 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 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 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