From 41e8c3b950ea4d2ad79c5f5256d7f8d1f1c8207f Mon Sep 17 00:00:00 2001
From: M66B
Date: Sat, 24 Sep 2022 09:22:13 +0200
Subject: [PATCH] Build requery/sqlite inline
---
app/build.gradle | 3 +-
settings.gradle | 3 +-
sqlite-android/.gitignore | 7 +
sqlite-android/build.gradle | 149 +
sqlite-android/proguard-rules.pro | 14 +
sqlite-android/src/main/AndroidManifest.xml | 2 +
.../android/database/AbstractCursor.java | 421 +++
.../database/AbstractWindowedCursor.java | 177 ++
.../android/database/CursorWindow.java | 507 ++++
.../CursorWindowAllocationException.java | 29 +
.../database/DatabaseErrorHandler.java | 33 +
.../database/DefaultDatabaseErrorHandler.java | 106 +
.../android/database/sqlite/CloseGuard.java | 234 ++
.../RequerySQLiteOpenHelperFactory.java | 95 +
.../database/sqlite/SQLiteClosable.java | 80 +
.../database/sqlite/SQLiteConnection.java | 1585 ++++++++++
.../database/sqlite/SQLiteConnectionPool.java | 1084 +++++++
.../android/database/sqlite/SQLiteCursor.java | 260 ++
.../database/sqlite/SQLiteCursorDriver.java | 56 +
.../sqlite/SQLiteCustomExtension.java | 42 +
.../database/sqlite/SQLiteCustomFunction.java | 54 +
.../database/sqlite/SQLiteDatabase.java | 2603 +++++++++++++++++
.../sqlite/SQLiteDatabaseConfiguration.java | 203 ++
.../android/database/sqlite/SQLiteDebug.java | 173 ++
.../sqlite/SQLiteDirectCursorDriver.java | 85 +
.../database/sqlite/SQLiteFunction.java | 185 ++
.../android/database/sqlite/SQLiteGlobal.java | 116 +
.../database/sqlite/SQLiteOpenHelper.java | 410 +++
.../database/sqlite/SQLiteProgram.java | 246 ++
.../android/database/sqlite/SQLiteQuery.java | 86 +
.../database/sqlite/SQLiteQueryBuilder.java | 612 ++++
.../database/sqlite/SQLiteSession.java | 975 ++++++
.../database/sqlite/SQLiteStatement.java | 172 ++
.../database/sqlite/SQLiteStatementInfo.java | 40 +
.../database/sqlite/SQLiteStatementType.java | 107 +
sqlite-android/src/main/jni/Android.mk | 4 +
sqlite-android/src/main/jni/Application.mk | 5 +
.../src/main/jni/sqlite/ALog-priv.h | 72 +
sqlite-android/src/main/jni/sqlite/Android.mk | 67 +
.../src/main/jni/sqlite/CursorWindow.cpp | 283 ++
.../src/main/jni/sqlite/CursorWindow.h | 188 ++
sqlite-android/src/main/jni/sqlite/Errors.h | 88 +
.../src/main/jni/sqlite/JNIHelp.cpp | 217 ++
sqlite-android/src/main/jni/sqlite/JNIHelp.h | 178 ++
.../src/main/jni/sqlite/JNIString.cpp | 118 +
sqlite-android/src/main/jni/sqlite/README | 32 +
.../sqlite/android_database_CursorWindow.cpp | 412 +++
.../sqlite/android_database_SQLiteCommon.cpp | 140 +
.../sqlite/android_database_SQLiteCommon.h | 52 +
.../android_database_SQLiteConnection.cpp | 1048 +++++++
.../sqlite/android_database_SQLiteDebug.cpp | 81 +
.../android_database_SQLiteFunction.cpp | 229 ++
.../sqlite/android_database_SQLiteGlobal.cpp | 92 +
53 files changed, 14258 insertions(+), 2 deletions(-)
create mode 100644 sqlite-android/.gitignore
create mode 100644 sqlite-android/build.gradle
create mode 100644 sqlite-android/proguard-rules.pro
create mode 100644 sqlite-android/src/main/AndroidManifest.xml
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/AbstractCursor.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/AbstractWindowedCursor.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/CursorWindow.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/CursorWindowAllocationException.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/DatabaseErrorHandler.java
create mode 100755 sqlite-android/src/main/java/io/requery/android/database/DefaultDatabaseErrorHandler.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/CloseGuard.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/RequerySQLiteOpenHelperFactory.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteClosable.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnectionPool.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursor.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursorDriver.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomExtension.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomFunction.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDebug.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDirectCursorDriver.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteFunction.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteGlobal.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteOpenHelper.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteProgram.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQuery.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQueryBuilder.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteSession.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatement.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementInfo.java
create mode 100644 sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementType.java
create mode 100644 sqlite-android/src/main/jni/Android.mk
create mode 100644 sqlite-android/src/main/jni/Application.mk
create mode 100644 sqlite-android/src/main/jni/sqlite/ALog-priv.h
create mode 100644 sqlite-android/src/main/jni/sqlite/Android.mk
create mode 100644 sqlite-android/src/main/jni/sqlite/CursorWindow.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/CursorWindow.h
create mode 100644 sqlite-android/src/main/jni/sqlite/Errors.h
create mode 100644 sqlite-android/src/main/jni/sqlite/JNIHelp.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/JNIHelp.h
create mode 100644 sqlite-android/src/main/jni/sqlite/JNIString.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/README
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_CursorWindow.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.h
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteDebug.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteFunction.cpp
create mode 100644 sqlite-android/src/main/jni/sqlite/android_database_SQLiteGlobal.cpp
diff --git a/app/build.gradle b/app/build.gradle
index e654f01339..4f0350c03f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -468,7 +468,8 @@ dependencies {
// https://www.sqlite.org/changes.html
// https://github.com/requery/sqlite-android/
// https://jitpack.io/#requery/sqlite-android
- implementation "com.github.requery:sqlite-android:$requery_version"
+ //implementation "com.github.requery:sqlite-android:$requery_version"
+ implementation project(':sqlite-android')
// https://mvnrepository.com/artifact/androidx.paging/paging-runtime
// https://developer.android.com/jetpack/androidx/releases/paging
diff --git a/settings.gradle b/settings.gradle
index fd78ac7fec..ab1f6a7344 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,3 @@
-include ':app', ':openpgp-api'
+include ':app', ':openpgp-api', ':sqlite-android'
project(':openpgp-api').projectDir = new File('openpgp-api')
+project(':sqlite-android').projectDir = new File('sqlite-android')
diff --git a/sqlite-android/.gitignore b/sqlite-android/.gitignore
new file mode 100644
index 0000000000..054fce48a8
--- /dev/null
+++ b/sqlite-android/.gitignore
@@ -0,0 +1,7 @@
+/build
+/src/main/jniLibs
+/src/main/jni/sqlite/sqlite3.h
+/src/main/jni/sqlite/sqlite3.c
+/src/main/jni/sqlite.zip
+/src/main/obj
+.externalNativeBuild/
diff --git a/sqlite-android/build.gradle b/sqlite-android/build.gradle
new file mode 100644
index 0000000000..7eca971ab5
--- /dev/null
+++ b/sqlite-android/build.gradle
@@ -0,0 +1,149 @@
+plugins {
+ id 'de.undercouch.download'
+ id 'com.android.library'
+ id 'maven-publish'
+}
+
+group = 'io.requery'
+version = '3.39.2'
+description = 'Android SQLite compatibility library'
+
+android {
+ compileSdkVersion 32
+ buildToolsVersion "33.0.0"
+ ndkVersion '25.0.8775105'
+
+ defaultConfig {
+ minSdkVersion 14
+ versionName project.version
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ consumerProguardFiles 'proguard-rules.pro'
+ ndk {
+ abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
+ }
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ lintOptions {
+ abortOnError false
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ externalNativeBuild {
+ ndkBuild {
+ path 'src/main/jni/Android.mk'
+ }
+ }
+
+ libraryVariants.all {
+ it.generateBuildConfigProvider.configure { enabled = false }
+ }
+}
+
+dependencies {
+ api 'androidx.sqlite:sqlite:2.2.0'
+ api 'androidx.core:core:1.8.0'
+ androidTestImplementation 'androidx.test:core:1.4.0'
+ androidTestImplementation 'androidx.test:runner:1.4.0'
+ androidTestImplementation 'androidx.test:rules:1.4.0'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+}
+
+ext {
+ sqliteDistributionUrl = 'https://www.sqlite.org/2022/sqlite-amalgamation-3390200.zip'
+}
+
+task downloadSqlite(type: Download) {
+ src project.sqliteDistributionUrl
+ dest 'src/main/jni/sqlite.zip'
+}
+
+task installSqlite(dependsOn: downloadSqlite, type: Copy) {
+ from zipTree(downloadSqlite.dest).matching {
+ include '*/sqlite3.*'
+ eachFile { it.setPath(it.getName()) }
+ }
+ into 'src/main/jni/sqlite'
+}
+
+preBuild.dependsOn installSqlite
+
+Properties properties = new Properties()
+File localProperties = project.rootProject.file('local.properties')
+if (localProperties.exists()) {
+ properties.load(localProperties.newDataInputStream())
+}
+
+task sourceJar(type: Jar) {
+ archiveClassifier.set('sources')
+ from android.sourceSets.main.java.srcDirs
+}
+
+task javadoc(type: Javadoc) {
+ source = android.sourceSets.main.java.srcDirs
+ classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
+ android.libraryVariants.all { variant ->
+ if (variant.name == 'release') {
+ owner.classpath += variant.javaCompileProvider.get().classpath
+ }
+ }
+ exclude '**/R.html', '**/R.*.html', '**/index.html'
+ if (JavaVersion.current().isJava9Compatible()) {
+ options.addBooleanOption('html5', true)
+ }
+
+ failOnError false
+}
+
+task javadocJar(type: Jar, dependsOn: javadoc) {
+ archiveClassifier.set('javadoc')
+ from javadoc.destinationDir
+}
+
+preBuild.dependsOn installSqlite
+// https://issuetracker.google.com/issues/207403732
+tasks.whenTaskAdded { task ->
+ if (task.name.startsWith("configureNdkBuildDebug")
+ || task.name.startsWith("configureNdkBuildRelease")) {
+ task.dependsOn preBuild
+ }
+}
+
+publishing {
+ publications {
+ maven(MavenPublication) {
+ groupId project.group
+ artifactId project.name
+ version project.version
+ pom {
+ name = project.name
+ description = project.description
+ url = 'https://github.com/requery/sqlite-android'
+ licenses {
+ license {
+ name = 'The Apache Software License, Version 2.0'
+ url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution = 'repo'
+ }
+ }
+ scm {
+ url = 'https://github.com/requery/sqlite-android.git'
+ connection = 'scm:git:git://github.com/requery/sqlite-android.git'
+ developerConnection = 'scm:git:git@github.com/requery/sqlite-android.git'
+ }
+ }
+ afterEvaluate {
+ artifact bundleReleaseAar
+ artifact sourceJar
+ artifact javadocJar
+ }
+ }
+ }
+}
diff --git a/sqlite-android/proguard-rules.pro b/sqlite-android/proguard-rules.pro
new file mode 100644
index 0000000000..02c5d5af92
--- /dev/null
+++ b/sqlite-android/proguard-rules.pro
@@ -0,0 +1,14 @@
+-keepclasseswithmembers class io.requery.android.database.** {
+ native ;
+ public (...);
+}
+-keepnames class io.requery.android.database.** { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteFunction { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteCustomFunction { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteCursor { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteDebug** { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteDatabase { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteOpenHelper { *; }
+-keep public class io.requery.android.database.sqlite.SQLiteStatement { *; }
+-keep public class io.requery.android.database.CursorWindow { *; }
+-keepattributes Exceptions,InnerClasses
diff --git a/sqlite-android/src/main/AndroidManifest.xml b/sqlite-android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..795975fcb4
--- /dev/null
+++ b/sqlite-android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/sqlite-android/src/main/java/io/requery/android/database/AbstractCursor.java b/sqlite-android/src/main/java/io/requery/android/database/AbstractCursor.java
new file mode 100644
index 0000000000..27a5e1c3c7
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/AbstractCursor.java
@@ -0,0 +1,421 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database;
+
+import android.content.ContentResolver;
+import android.database.CharArrayBuffer;
+import android.database.ContentObservable;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.CursorIndexOutOfBoundsException;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * 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 Cursor {
+
+ private static final String TAG = "Cursor";
+
+ protected int mPos;
+
+ protected boolean mClosed;
+
+ //@Deprecated // deprecated in AOSP but still used for non-deprecated methods
+ protected ContentResolver mContentResolver;
+
+ private Uri mNotifyUri;
+
+ private final Object mSelfObserverLock = new Object();
+ private ContentObserver mSelfObserver;
+ private boolean mSelfObserverRegistered;
+ private final DataSetObservable mDataSetObservable = new DataSetObservable();
+ private final ContentObservable mContentObservable = new ContentObservable();
+
+ private Bundle mExtras = Bundle.EMPTY;
+
+ @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 abstract int getType(int column);
+
+ @Override
+ public byte[] getBlob(int column) {
+ throw new UnsupportedOperationException("getBlob is not supported");
+ }
+
+ @Override
+ public int getColumnCount() {
+ return getColumnNames().length;
+ }
+
+ @Override
+ public void deactivate() {
+ onDeactivateOrClose();
+ }
+
+ /** @hide */
+ protected void onDeactivateOrClose() {
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ mSelfObserverRegistered = false;
+ }
+ mDataSetObservable.notifyInvalidated();
+ }
+
+ @Override
+ public boolean requery() {
+ if (mSelfObserver != null && !mSelfObserverRegistered) {
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ mDataSetObservable.notifyChanged();
+ return true;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return mClosed;
+ }
+
+ @Override
+ public void close() {
+ mClosed = true;
+ mContentObservable.unregisterAll();
+ onDeactivateOrClose();
+ }
+
+ @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;
+ }
+ }
+
+ public AbstractCursor() {
+ mPos = -1;
+ }
+
+ @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) {
+ mPos = -1;
+ } else {
+ mPos = position;
+ }
+
+ return result;
+ }
+
+ /**
+ * 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.
+ */
+ public abstract boolean onMove(int oldPosition, int newPosition);
+
+ @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() {
+ return getCount() == 0 || mPos == -1;
+ }
+
+ @Override
+ public final boolean isAfterLast() {
+ return getCount() == 0 || 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;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) {
+ final int index = getColumnIndex(columnName);
+ if (index < 0) {
+ throw new IllegalArgumentException("column '" + columnName + "' does not exist");
+ }
+ 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 value
+ */
+ @SuppressWarnings("deprecation")
+ protected void onChange(boolean selfChange) {
+ synchronized (mSelfObserverLock) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mContentObservable.dispatchChange(selfChange, null);
+ } else {
+ mContentObservable.dispatchChange(selfChange);
+ }
+ if (mNotifyUri != null && selfChange) {
+ mContentResolver.notifyChange(mNotifyUri, mSelfObserver);
+ }
+ }
+ }
+
+ /**
+ * Specifies a content URI to watch for changes.
+ *
+ * @param cr The content resolver from the caller's context.
+ * @param notifyUri The URI to watch for changes. This can be a
+ * specific row URI, or a base URI for a whole class of content.
+ */
+ @Override
+ public void setNotificationUri(ContentResolver cr, Uri notifyUri) {
+ synchronized (mSelfObserverLock) {
+ mNotifyUri = notifyUri;
+ mContentResolver = cr;
+ if (mSelfObserver != null) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ }
+ mSelfObserver = new SelfContentObserver(this);
+ mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver);
+ mSelfObserverRegistered = true;
+ }
+ }
+
+ @Override
+ public Uri getNotificationUri() {
+ synchronized (mSelfObserverLock) {
+ return mNotifyUri;
+ }
+ }
+
+ @Override
+ public boolean getWantsAllOnMoveCalls() {
+ return false;
+ }
+
+ @Override
+ public void setExtras(Bundle extras) {
+ mExtras = (extras == null) ? Bundle.EMPTY : extras;
+ }
+
+ @Override
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public Bundle respond(Bundle extras) {
+ return Bundle.EMPTY;
+ }
+
+ /**
+ * 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());
+ }
+ }
+
+ @SuppressWarnings("FinalizeDoesntCallSuperFinalize")
+ @Override
+ protected void finalize() {
+ if (mSelfObserver != null && mSelfObserverRegistered) {
+ mContentResolver.unregisterContentObserver(mSelfObserver);
+ }
+ try {
+ if (!mClosed) close();
+ } catch(Exception ignored) { }
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/AbstractWindowedCursor.java b/sqlite-android/src/main/java/io/requery/android/database/AbstractWindowedCursor.java
new file mode 100644
index 0000000000..1a850138c6
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/AbstractWindowedCursor.java
@@ -0,0 +1,177 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database;
+
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.database.StaleDataException;
+
+/**
+ * A base class for Cursors that store their data in {@link android.database.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.
+ *
+ */
+@SuppressWarnings("unused")
+public abstract class AbstractWindowedCursor extends AbstractCursor {
+ /**
+ * The cursor window owned by this cursor.
+ */
+ protected CursorWindow mWindow;
+
+ @Override
+ public byte[] getBlob(int columnIndex) {
+ checkPosition();
+ return mWindow.getBlob(mPos, columnIndex);
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ checkPosition();
+ return mWindow.getString(mPos, columnIndex);
+ }
+
+ @Override
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ mWindow.copyStringToBuffer(mPos, columnIndex, buffer);
+ }
+
+ @Override
+ public short getShort(int columnIndex) {
+ checkPosition();
+ return mWindow.getShort(mPos, columnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ checkPosition();
+ return mWindow.getInt(mPos, columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ checkPosition();
+ return mWindow.getLong(mPos, columnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex) {
+ checkPosition();
+ return mWindow.getFloat(mPos, columnIndex);
+ }
+
+ @Override
+ public double getDouble(int columnIndex) {
+ checkPosition();
+ return mWindow.getDouble(mPos, columnIndex);
+ }
+
+ @Override
+ public boolean isNull(int columnIndex) {
+ return mWindow.getType(mPos, columnIndex) == Cursor.FIELD_TYPE_NULL;
+ }
+
+ @Override
+ public int getType(int columnIndex) {
+ 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.");
+ }
+ }
+
+ 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();
+ }
+ }
+
+ @Override
+ protected void onDeactivateOrClose() {
+ super.onDeactivateOrClose();
+ closeWindow();
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/CursorWindow.java b/sqlite-android/src/main/java/io/requery/android/database/CursorWindow.java
new file mode 100644
index 0000000000..b7af4238fc
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/CursorWindow.java
@@ -0,0 +1,507 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database;
+
+import android.database.CharArrayBuffer;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import io.requery.android.database.sqlite.SQLiteClosable;
+
+/**
+ * A buffer containing multiple cursor rows.
+ */
+@SuppressWarnings("unused")
+public class CursorWindow extends SQLiteClosable {
+
+ private static final int WINDOW_SIZE_KB = 2048;
+
+ /** The cursor window size. resource xml file specifies the value in kB.
+ * convert it to bytes here by multiplying with 1024.
+ */
+ private static final int sDefaultCursorWindowSize =
+ WINDOW_SIZE_KB * 1024;
+ private final int mWindowSizeBytes;
+
+ /**
+ * The native CursorWindow object pointer. (FOR INTERNAL USE ONLY)
+ */
+ public long mWindowPtr;
+
+ private int mStartPos;
+ private final String mName;
+
+ private static native long nativeCreate(String name, int cursorWindowSize);
+ private static native void nativeDispose(long windowPtr);
+
+ private static native void nativeClear(long windowPtr);
+
+ private static native int nativeGetNumRows(long windowPtr);
+ private static native boolean nativeSetNumColumns(long windowPtr, int columnNum);
+ private static native boolean nativeAllocRow(long windowPtr);
+ private static native void nativeFreeLastRow(long windowPtr);
+
+ private static native int nativeGetType(long windowPtr, int row, int column);
+ private static native byte[] nativeGetBlob(long windowPtr, int row, int column);
+ private static native String nativeGetString(long windowPtr, int row, int column);
+ private static native long nativeGetLong(long windowPtr, int row, int column);
+ private static native double nativeGetDouble(long windowPtr, int row, int column);
+
+ private static native boolean nativePutBlob(long windowPtr, byte[] value, int row, int column);
+ private static native boolean nativePutString(long windowPtr, String value, int row, int column);
+ private static native boolean nativePutLong(long windowPtr, long value, int row, int column);
+ private static native boolean nativePutDouble(long windowPtr, double value, int row, int column);
+ private static native boolean nativePutNull(long windowPtr, int row, int column);
+
+ private static native String nativeGetName(long windowPtr);
+
+ /**
+ * Creates a new empty cursor with default cursor size (currently 2MB)
+ */
+ public CursorWindow(String name) {
+ this(name, sDefaultCursorWindowSize);
+ }
+
+
+ /**
+ * 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.
+ *
+ * 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. Value is a non-negative number of bytes.
+ */
+ public CursorWindow(String name, int windowSizeBytes) {
+ /* In
+ https://developer.android.com/reference/android/database/CursorWindow#CursorWindow(java.lang.String,%20long)
+ windowSizeBytes is long. However windowSizeBytes is
+ eventually transformed into a size_t in cpp, and I can not
+ guarantee that long->size_t would be possible. I thus keep
+ int. This means that we can create cursor of size up to 4GiB
+ while upstream can theoretically create cursor of size up to
+ 16 EiB. It is probably an acceptable restriction.*/
+ mStartPos = 0;
+ mWindowSizeBytes = windowSizeBytes;
+ mName = name != null && name.length() != 0 ? name : "";
+ mWindowPtr = nativeCreate(mName, windowSizeBytes);
+ if (mWindowPtr == 0) {
+ throw new CursorWindowAllocationException("Cursor window allocation of " +
+ (windowSizeBytes / 1024) + " kb failed. ");
+ }
+ }
+
+ @SuppressWarnings("ThrowFromFinallyBlock")
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ dispose();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ private void dispose() {
+ if (mWindowPtr != 0) {
+ nativeDispose(mWindowPtr);
+ mWindowPtr = 0;
+ }
+ }
+
+ /**
+ * Gets the name of this cursor window, never null.
+ */
+ 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() {
+ mStartPos = 0;
+ nativeClear(mWindowPtr);
+ }
+
+ /**
+ * Gets the start position of this cursor window.
+ *
+ * The start position is the zero-based index of the first row that this window contains
+ * relative to the entire result set of the {@link Cursor}.
+ *
+ *
+ * @return The zero-based start position.
+ */
+ public int getStartPosition() {
+ return mStartPos;
+ }
+
+ /**
+ * Sets the start position of this cursor window.
+ *
+ * The start position is the zero-based index of the first row that this window contains
+ * relative to the entire result set of the {@link Cursor}.
+ *
+ *
+ * @param pos The new zero-based start position.
+ */
+ public void setStartPosition(int pos) {
+ mStartPos = pos;
+ }
+
+ /**
+ * Gets the number of rows in this window.
+ *
+ * @return The number of rows in this cursor window.
+ */
+ public int getNumRows() {
+ return nativeGetNumRows(mWindowPtr);
+ }
+
+ /**
+ * Sets the number of columns in this window.
+ *
+ * This method must be called before any rows are added to the window, otherwise
+ * it will fail to set the number of columns if it differs from the current number
+ * of columns.
+ *
+ *
+ * @param columnNum The new number of columns.
+ * @return True if successful.
+ */
+ public boolean setNumColumns(int columnNum) {
+ return nativeSetNumColumns(mWindowPtr, columnNum);
+ }
+
+ /**
+ * 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(){
+ return nativeAllocRow(mWindowPtr);
+ }
+
+ /**
+ * Frees the last row in this cursor window.
+ */
+ public void freeLastRow(){
+ nativeFreeLastRow(mWindowPtr);
+ }
+
+ /**
+ * Returns the type of the field at the specified row and column index.
+ *
+ * The returned field types 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 row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The field type.
+ */
+ public int getType(int row, int column) {
+ return nativeGetType(mWindowPtr, row - mStartPos, column);
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as a byte array.
+ *
+ * The result is determined as follows:
+ *
+ * If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+ * is null
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then the result
+ * is the blob value.
+ * If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+ * is the array of bytes that make up the internal representation of the
+ * string value.
+ * If the field is of type {@link Cursor#FIELD_TYPE_INTEGER} or
+ * {@link Cursor#FIELD_TYPE_FLOAT}, then a {@link SQLiteException} is thrown.
+ *
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as a byte array.
+ */
+ public byte[] getBlob(int row, int column) {
+ return nativeGetBlob(mWindowPtr, row - mStartPos, 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(int row, int column) {
+ return nativeGetString(mWindowPtr, row - mStartPos, column);
+ }
+
+ /**
+ * Copies the text of the field at the specified row and column index into
+ * a {@link CharArrayBuffer}.
+ *
+ * The buffer is populated as follows:
+ *
+ * If the buffer is too small for the value to be copied, then it is
+ * automatically resized.
+ * If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the buffer
+ * is set to an empty string.
+ * If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the buffer
+ * is set to the contents of the string.
+ * If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the buffer
+ * is set to a string representation of the integer in decimal, obtained by formatting the
+ * value with the printf
family of functions using
+ * format specifier %lld
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the buffer is
+ * set to a string representation of the floating-point value in decimal, obtained by
+ * formatting the value with the printf
family of functions using
+ * format specifier %g
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+ * {@link SQLiteException} is thrown.
+ *
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @param buffer The {@link CharArrayBuffer} to hold the string. It is automatically
+ * resized if the requested string is larger than the buffer's current capacity.
+ */
+ public void copyStringToBuffer(int row, int column, CharArrayBuffer buffer) {
+ if (buffer == null) {
+ throw new IllegalArgumentException("CharArrayBuffer should not be null");
+ }
+ // TODO not as optimal as the original code
+ char[] chars = getString(row, column).toCharArray();
+ buffer.data = chars;
+ buffer.sizeCopied = chars.length;
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as a long
.
+ *
+ * The result is determined as follows:
+ *
+ * If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+ * is 0L
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+ * is the value obtained by parsing the string value with strtoll
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result
+ * is the long
value.
+ * If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result
+ * is the floating-point value converted to a long
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+ * {@link SQLiteException} is thrown.
+ *
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as a long
.
+ */
+ public long getLong(int row, int column) {
+ return nativeGetLong(mWindowPtr, row - mStartPos, column);
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as a
+ * double
.
+ *
+ * The result is determined as follows:
+ *
+ * If the field is of type {@link Cursor#FIELD_TYPE_NULL}, then the result
+ * is 0.0
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_STRING}, then the result
+ * is the value obtained by parsing the string value with strtod
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_INTEGER}, then the result
+ * is the integer value converted to a double
.
+ * If the field is of type {@link Cursor#FIELD_TYPE_FLOAT}, then the result
+ * is the double
value.
+ * If the field is of type {@link Cursor#FIELD_TYPE_BLOB}, then a
+ * {@link SQLiteException} is thrown.
+ *
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as a double
.
+ */
+ public double getDouble(int row, int column) {
+ return nativeGetDouble(mWindowPtr, row - mStartPos, column);
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as a
+ * short
.
+ *
+ * The result is determined by invoking {@link #getLong} and converting the
+ * result to short
.
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as a short
.
+ */
+ public short getShort(int row, int column) {
+ return (short) getLong(row, column);
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as an
+ * int
.
+ *
+ * The result is determined by invoking {@link #getLong} and converting the
+ * result to int
.
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as an int
.
+ */
+ public int getInt(int row, int column) {
+ return (int) getLong(row, column);
+ }
+
+ /**
+ * Gets the value of the field at the specified row and column index as a
+ * float
.
+ *
+ * The result is determined by invoking {@link #getDouble} and converting the
+ * result to float
.
+ *
+ *
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return The value of the field as an float
.
+ */
+ public float getFloat(int row, int column) {
+ return (float) getDouble(row, column);
+ }
+
+ /**
+ * Copies a byte array into the field at the specified row and column index.
+ *
+ * @param value The value to store.
+ * @param row The zero-based row index.
+ * @param column The zero-based column index.
+ * @return True if successful.
+ */
+ public boolean putBlob(byte[] value, int row, int column) {
+ return nativePutBlob(mWindowPtr, value, row - mStartPos, 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, int row, int column) {
+ return nativePutString(mWindowPtr, value, row - mStartPos, 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, int row, int column) {
+ return nativePutLong(mWindowPtr, value, row - mStartPos, 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, int row, int column) {
+ return nativePutDouble(mWindowPtr, value, row - mStartPos, 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(int row, int column) {
+ return nativePutNull(mWindowPtr, row - mStartPos, column);
+ }
+
+ @Override
+ protected void onAllReferencesReleased() {
+ dispose();
+ }
+
+ @Override
+ public String toString() {
+ return getName() + " {" + Long.toHexString(mWindowPtr) + "}";
+ }
+
+ public int getWindowSizeBytes() {
+ return mWindowSizeBytes;
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/CursorWindowAllocationException.java b/sqlite-android/src/main/java/io/requery/android/database/CursorWindowAllocationException.java
new file mode 100644
index 0000000000..3ac59bac02
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/CursorWindowAllocationException.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.
+ */
+
+package io.requery.android.database;
+
+/**
+ * This exception is thrown when a CursorWindow couldn't be allocated,
+ * most probably due to memory not being available.
+ *
+ * @hide
+ */
+public class CursorWindowAllocationException extends RuntimeException {
+ public CursorWindowAllocationException(String description) {
+ super(description);
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/DatabaseErrorHandler.java b/sqlite-android/src/main/java/io/requery/android/database/DatabaseErrorHandler.java
new file mode 100644
index 0000000000..e27fd9badd
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/DatabaseErrorHandler.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 from original source see README at the top level of this project
+
+package io.requery.android.database;
+
+import io.requery.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/sqlite-android/src/main/java/io/requery/android/database/DefaultDatabaseErrorHandler.java b/sqlite-android/src/main/java/io/requery/android/database/DefaultDatabaseErrorHandler.java
new file mode 100755
index 0000000000..5225d15ff2
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/DefaultDatabaseErrorHandler.java
@@ -0,0 +1,106 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database;
+
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+import io.requery.android.database.sqlite.SQLiteDatabase;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Default class used to define the actions to take when the database corruption is reported
+ * by sqlite.
+ *
+ * An application can specify an implementation of {@link DatabaseErrorHandler} on the
+ * following:
+ *
+ * {@link SQLiteDatabase#openOrCreateDatabase(String,
+ * SQLiteDatabase.CursorFactory, DatabaseErrorHandler)}
+ * {@link SQLiteDatabase#openDatabase(String,
+ * SQLiteDatabase.CursorFactory, int, DatabaseErrorHandler)}
+ *
+ * The specified {@link DatabaseErrorHandler} is used to handle database corruption errors, if they
+ * occur.
+ *
+ * If null is specified for DatabaeErrorHandler param in the above calls, then this class is used
+ * as the default {@link DatabaseErrorHandler}.
+ */
+public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler {
+
+ private static final String TAG = "DefaultDatabaseError";
+
+ @Override
+ public void onCorruption(SQLiteDatabase dbObj) {
+ Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+
+ // 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/CloseGuard.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/CloseGuard.java
new file mode 100644
index 0000000000..790ca594da
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/CloseGuard.java
@@ -0,0 +1,234 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.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
+ */
+@SuppressWarnings("unused")
+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 interface Reporter {
+ 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("SQLite", message, allocationSite);
+ }
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/RequerySQLiteOpenHelperFactory.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/RequerySQLiteOpenHelperFactory.java
new file mode 100644
index 0000000000..408a5da5ce
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/RequerySQLiteOpenHelperFactory.java
@@ -0,0 +1,95 @@
+package io.requery.android.database.sqlite;
+
+import android.content.Context;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import io.requery.android.database.DatabaseErrorHandler;
+
+import java.util.Collections;
+
+/**
+ * Implements {@link SupportSQLiteOpenHelper.Factory} using the SQLite implementation shipped in
+ * this library.
+ */
+@SuppressWarnings("unused")
+public final class RequerySQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+ private final Iterable configurationOptions;
+
+ @SuppressWarnings("WeakerAccess")
+ public RequerySQLiteOpenHelperFactory(Iterable configurationOptions) {
+ this.configurationOptions = configurationOptions;
+ }
+
+ public RequerySQLiteOpenHelperFactory() {
+ this(Collections.emptyList());
+ }
+
+ @Override
+ public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration config) {
+ return new CallbackSQLiteOpenHelper(config.context, config.name, config.callback, configurationOptions);
+ }
+
+ private static final class CallbackSQLiteOpenHelper extends SQLiteOpenHelper {
+
+ private final SupportSQLiteOpenHelper.Callback callback;
+ private final Iterable configurationOptions;
+
+ CallbackSQLiteOpenHelper(Context context, String name, SupportSQLiteOpenHelper.Callback cb, Iterable ops) {
+ super(context, name, null, cb.version, new CallbackDatabaseErrorHandler(cb));
+ this.callback = cb;
+ this.configurationOptions = ops;
+ }
+
+ @Override
+ public void onConfigure(SQLiteDatabase db) {
+ callback.onConfigure(db);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ callback.onCreate(db);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ callback.onUpgrade(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ callback.onDowngrade(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SQLiteDatabase db) {
+ callback.onOpen(db);
+ }
+
+ @Override protected SQLiteDatabaseConfiguration createConfiguration(String path, int openFlags) {
+ SQLiteDatabaseConfiguration config = super.createConfiguration(path, openFlags);
+
+ for (ConfigurationOptions option : configurationOptions) {
+ config = option.apply(config);
+ }
+
+ return config;
+ }
+ }
+
+ private static final class CallbackDatabaseErrorHandler implements DatabaseErrorHandler {
+
+ private final SupportSQLiteOpenHelper.Callback callback;
+
+ CallbackDatabaseErrorHandler(SupportSQLiteOpenHelper.Callback callback) {
+ this.callback = callback;
+ }
+
+ @Override
+ public void onCorruption(SQLiteDatabase db) {
+ callback.onCorruption(db);
+ }
+ }
+
+ public interface ConfigurationOptions {
+ SQLiteDatabaseConfiguration apply(SQLiteDatabaseConfiguration configuration);
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteClosable.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteClosable.java
new file mode 100644
index 0000000000..0edc17d4e0
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteClosable.java
@@ -0,0 +1,80 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.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();
+
+ /**
+ * 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;
+ synchronized(this) {
+ refCountIsZero = --mReferenceCount == 0;
+ }
+ if (refCountIsZero) {
+ onAllReferencesReleased();
+ }
+ }
+
+ /**
+ * 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java
new file mode 100644
index 0000000000..c408b86522
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnection.java
@@ -0,0 +1,1585 @@
+/*
+ * 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 from original source see README at the top level of this project
+/*
+** Modified to support SQLite extensions by the SQLite developers:
+** sqlite-dev@sqlite.org.
+*/
+
+package io.requery.android.database.sqlite;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteBindOrColumnIndexOutOfRangeException;
+import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteException;
+import android.os.Build;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.util.Printer;
+import androidx.collection.LruCache;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+import io.requery.android.database.CursorWindow;
+
+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
+ */
+@SuppressWarnings("TryFinallyCanBeTryWithResources")
+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 static final Pattern TRIM_SQL_PATTERN = Pattern.compile("[\\s]*\\n+[\\s]*");
+
+ 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 nativeRegisterFunction(long connectionPtr,
+ SQLiteFunction 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, long winPtr,
+ 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();
+ private static native void nativeLoadExtension(long connectionPtr, String file, String proc);
+
+ 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");
+ }
+
+ @SuppressWarnings("ThrowFromFinallyBlock")
+ @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,
+ // remove the wal flag as its a custom flag not supported by sqlite3_open_v2
+ mConfiguration.openFlags & ~SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING,
+ mConfiguration.label,
+ SQLiteDebug.DEBUG_SQL_STATEMENTS, SQLiteDebug.DEBUG_SQL_TIME);
+
+ setPageSize();
+ setForeignKeyModeFromConfiguration();
+ setJournalSizeLimit();
+ setAutoCheckpointInterval();
+ if (!nativeHasCodec()) {
+ setWalModeFromConfiguration();
+ setLocaleFromConfiguration();
+ }
+
+ // Register (deprecated) custom functions.
+ final int customFunctionCount = mConfiguration.customFunctions.size();
+ for (int i = 0; i < customFunctionCount; i++) {
+ SQLiteCustomFunction function = mConfiguration.customFunctions.get(i);
+ nativeRegisterCustomFunction(mConnectionPtr, function);
+ }
+
+ // Register functions
+ final int functionCount = mConfiguration.functions.size();
+ for (int i = 0; i < functionCount; i++) {
+ SQLiteFunction function = mConfiguration.functions.get(i);
+ nativeRegisterFunction(mConnectionPtr, function);
+ }
+
+ // Register custom extensions
+ for (SQLiteCustomExtension extension : mConfiguration.customExtensions) {
+ nativeLoadExtension(mConnectionPtr, extension.path, extension.entryPoint);
+ }
+ }
+
+ 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) {
+ switch (value) {
+ case "0":
+ return "OFF";
+ case "1":
+ return "NORMAL";
+ case "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 (SQLiteException 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.
+ if (!(ex instanceof SQLiteDatabaseLockedException)) {
+ throw ex;
+ }
+ }
+
+ // 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() {
+ // 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 + "'.");
+ }
+ }
+
+ public void enableLocalizedCollators() {
+ if (nativeHasCodec()) {
+ setLocaleFromConfiguration();
+ }
+ }
+
+ // Called by SQLiteConnectionPool only.
+ void reconfigure(SQLiteDatabaseConfiguration configuration) {
+ mOnlyAllowReadOnlyOperations = false;
+
+ // Register (deprecated) custom functions.
+ final int customFunctionCount = configuration.customFunctions.size();
+ for (int i = 0; i < customFunctionCount; i++) {
+ SQLiteCustomFunction function = configuration.customFunctions.get(i);
+ if (!mConfiguration.customFunctions.contains(function)) {
+ nativeRegisterCustomFunction(mConnectionPtr, function);
+ }
+ }
+
+ // Register Functions
+ final int functionCount = configuration.functions.size();
+ for (int i = 0; i < functionCount; i++) {
+ SQLiteFunction function = configuration.functions.get(i);
+ if (!mConfiguration.functions.contains(function)) {
+ nativeRegisterFunction(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.
+ /* 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;
+ }
+
+ /**
+ * 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);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
+ return fd >= 0 ? ParcelFileDescriptor.adoptFd(fd) : null;
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ } 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.mWindowPtr,
+ 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 = SQLiteStatementType.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);
+ }
+ }
+ }
+
+ @SuppressLint("Assert")
+ 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) {
+ String message = "Expected " + statement.mNumParameters + " bind arguments but "
+ + count + " were provided.";
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ throw new SQLiteBindOrColumnIndexOutOfRangeException(message);
+ } else {
+ throw new SQLiteException(message);
+ }
+ }
+ if (count == 0) {
+ return;
+ }
+
+ final long statementPtr = statement.mStatementPtr;
+ for (int i = 0; i < count; i++) {
+ final Object arg = bindArgs[i];
+ switch (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 ? 1 : 0);
+ } else {
+ nativeBindString(mConnectionPtr, statementPtr, i + 1, arg.toString());
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * 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
+ */
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ private 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;
+ }
+ }
+
+ 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) {
+ return statementType == SQLiteStatementType.STATEMENT_UPDATE
+ || statementType == SQLiteStatementType.STATEMENT_SELECT;
+ }
+
+ private void applyBlockGuardPolicy(PreparedStatement statement) {
+ if (!mConfiguration.isInMemoryDb() && SQLiteDebug.DEBUG_SQL_LOG) {
+ // don't have access to the policy, so just log
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ if (statement.mReadOnly) {
+ Log.w(TAG, "Reading from disk on main thread");
+ } else {
+ Log.w(TAG, "Writing to disk on main thread");
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 SQLiteDebug.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.
+ */
+ void collectDbStatsUnsafe(ArrayList dbStatsList) {
+ dbStatsList.add(getMainDbStatsUnsafe(0, 0, 0));
+ }
+
+ private SQLiteDebug.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 SQLiteDebug.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) {
+ return TRIM_SQL_PATTERN.matcher(sql).replaceAll(" ");
+ }
+
+ /**
+ * 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.mStartTime = System.currentTimeMillis();
+ operation.mKind = kind;
+ operation.mSql = sql;
+ if (bindArgs != null) {
+ if (operation.mBindArgs == null) {
+ operation.mBindArgs = new ArrayList<>();
+ } else {
+ operation.mBindArgs.clear();
+ }
+ for (final Object arg : bindArgs) {
+ 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 = System.currentTimeMillis();
+ 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);
+ if (operation == null) {
+ return;
+ }
+ 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 {
+ @SuppressLint("SimpleDateFormat")
+ private static final SimpleDateFormat sDateFormat =
+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+
+ public long mStartTime;
+ public long mEndTime;
+ 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() - mStartTime)
+ .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 getFormattedStartTime() {
+ return sDateFormat.format(new Date(mStartTime));
+ }
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnectionPool.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnectionPool.java
new file mode 100644
index 0000000000..fdfe5b455f
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteConnectionPool.java
@@ -0,0 +1,1084 @@
+/*
+ * 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 from original source see README at the top level of this project
+/*
+** Modified to support SQLite extensions by the SQLite developers:
+** sqlite-dev@sqlite.org.
+*/
+
+package io.requery.android.database.sqlite;
+
+import android.annotation.SuppressLint;
+import android.database.sqlite.SQLiteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Printer;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+
+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;
+
+ /**
+ * 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();
+ }
+
+ @SuppressWarnings("ThrowFromFinallyBlock")
+ @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.
+ */
+ @SuppressLint("Assert")
+ 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() {
+ for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
+ closeConnectionAndLogExceptionsLocked(connection);
+ }
+ 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());
+ }
+ }
+ for (SQLiteConnection key : keysToUpdate) {
+ mAcquiredConnections.put(key, 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 (!SQLiteDatabase.hasCodec()
+ && (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) {
+ 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(printer, verbose);
+ } else {
+ printer.println("");
+ }
+
+ printer.println(" Available non-primary connections:");
+ if (!mAvailableNonPrimaryConnections.isEmpty()) {
+ for (SQLiteConnection connection : mAvailableNonPrimaryConnections) {
+ connection.dump(printer, verbose);
+ }
+ } else {
+ printer.println("");
+ }
+
+ printer.println(" Acquired connections:");
+ if (!mAcquiredConnections.isEmpty()) {
+ for (Map.Entry entry :
+ mAcquiredConnections.entrySet()) {
+ final SQLiteConnection connection = entry.getKey();
+ connection.dumpUnsafe(printer, verbose);
+ printer.println(" Status: " + entry.getValue());
+ }
+ } else {
+ printer.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++) {
+ printer.println(i + ": waited for "
+ + ((now - waiter.mStartTime) * 0.001f)
+ + " ms - thread=" + waiter.mThread
+ + ", priority=" + waiter.mPriority
+ + ", sql='" + waiter.mSql + "'");
+ }
+ } else {
+ printer.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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursor.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursor.java
new file mode 100644
index 0000000000..226689eae5
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursor.java
@@ -0,0 +1,260 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.util.Log;
+import android.util.SparseIntArray;
+import io.requery.android.database.AbstractWindowedCursor;
+import io.requery.android.database.CursorWindow;
+
+import java.util.HashMap;
+
+/**
+ * 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 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 SparseIntArray mColumnNameArray;
+ private HashMap mColumnNameMap;
+
+ /** Used to find out where a cursor was allocated in case it never got released. */
+ private final CloseGuard mCloseGuard;
+
+ /**
+ * 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 not used, present only for compatibility with
+ * {@link android.database.sqlite.SQLiteCursor}
+ * @param query the {@link SQLiteQuery} object associated with this cursor object.
+ */
+ @SuppressWarnings("unused")
+ public SQLiteCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query) {
+ if (query == null) {
+ throw new IllegalArgumentException("query object cannot be null");
+ }
+ mDriver = driver;
+ mQuery = query;
+ mCloseGuard = CloseGuard.get();
+ 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;
+ }
+
+ public static int cursorPickFillWindowStartPosition(
+ int cursorPosition, int cursorWindowCapacity) {
+ return Math.max(cursorPosition - cursorWindowCapacity / 3, 0);
+ }
+
+ private void fillWindow(int requiredPos) {
+ clearOrCreateWindow(getDatabase().getPath());
+
+ try {
+ if (mCount == NO_COUNT) {
+ int startPos = 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 = 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.
+ setWindow(null);
+ throw ex;
+ }
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ // Create mColumnNameMap on demand
+ if (mColumnNameArray == null && mColumnNameMap == null) {
+ String[] columns = mColumns;
+ int columnCount = columns.length;
+ SparseIntArray map = new SparseIntArray(columnCount);
+ boolean collision = false;
+ for (int i = 0; i < columnCount; i++) {
+ int key = columns[i].hashCode();
+ // check for hashCode collision
+ if (map.get(key, -1) != -1) {
+ collision = true;
+ break;
+ }
+ map.put(key, i);
+ }
+
+ if (collision) {
+ mColumnNameMap = new HashMap<>();
+ for (int i = 0; i < columnCount; i++) {
+ mColumnNameMap.put(columns[i], i);
+ }
+ } else {
+ mColumnNameArray = 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);
+ }
+
+ if (mColumnNameMap != null) {
+ Integer i = mColumnNameMap.get(columnName);
+ return i == null ? -1 : i;
+ } else {
+ return mColumnNameArray.get(columnName.hashCode(), -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) {
+ mCloseGuard.warnIfOpen();
+ close();
+ }
+ } finally {
+ super.finalize();
+ }
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursorDriver.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursorDriver.java
new file mode 100644
index 0000000000..0a3c653cb3
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCursorDriver.java
@@ -0,0 +1,56 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.database.Cursor;
+
+/**
+ * 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(SQLiteDatabase.CursorFactory factory, Object[] 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
+ */
+ void setBindArguments(String[] bindArgs);
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomExtension.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomExtension.java
new file mode 100644
index 0000000000..86d8efaa77
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomExtension.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 requery.io
+ *
+ * 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 io.requery.android.database.sqlite;
+
+/**
+ * Describes a SQLite extension entry point.
+ */
+public final class SQLiteCustomExtension {
+
+ public final String path;
+ public final String entryPoint;
+
+ /**
+ * Creates a SQLite extension description.
+ *
+ * @param path path to the loadable extension shared library
+ * e.g. "/data/data/(package)/lib/libSqliteICU.so"
+ * @param entryPoint extension entry point (optional)
+ * e.g. "sqlite3_icu_init"
+ */
+ public SQLiteCustomExtension(String path, String entryPoint) {
+ if (path == null) {
+ throw new IllegalArgumentException("null path");
+ }
+ this.path = path;
+ this.entryPoint = entryPoint;
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomFunction.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomFunction.java
new file mode 100644
index 0000000000..6814875b49
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteCustomFunction.java
@@ -0,0 +1,54 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.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 String dispatchCallback(String[] args) {
+ return callback.callback(args);
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java
new file mode 100644
index 0000000000..aca107f4a6
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabase.java
@@ -0,0 +1,2603 @@
+/*
+ * 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 from original source see README at the top level of this project
+/*
+** Modified to support SQLite extensions by the SQLite developers:
+** sqlite-dev@sqlite.org.
+*/
+
+package io.requery.android.database.sqlite;
+
+import android.annotation.SuppressLint;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.Build;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Printer;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import io.requery.android.database.DatabaseErrorHandler;
+import io.requery.android.database.DefaultDatabaseErrorHandler;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+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.
+ *
+ */
+@SuppressWarnings({"unused", "JavaDoc", "TryFinallyCanBeTryWithResources"})
+@SuppressLint("ShiftFlags") // suppressed for readability with native code
+public final class SQLiteDatabase extends SQLiteClosable implements SupportSQLiteDatabase {
+
+ /**
+ * Name of the compiled native library.
+ */
+ public static final String LIBRARY_NAME = "sqlite3x";
+ static {
+ System.loadLibrary(LIBRARY_NAME);
+ }
+
+ 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 final 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;
+
+ /**
+ * 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;
+
+ /** Conflict options integer enumeration definition */
+ @IntDef({
+ CONFLICT_ABORT,
+ CONFLICT_FAIL,
+ CONFLICT_IGNORE,
+ CONFLICT_NONE,
+ CONFLICT_REPLACE,
+ CONFLICT_ROLLBACK})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConflictAlgorithm {
+ }
+
+ private static final String[] CONFLICT_VALUES = new String[]
+ {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
+
+ /** Open flag to open in the database in read only mode */
+ public static final int OPEN_READONLY = 0x00000001;
+
+ /** Open flag to open in the database in read/write mode */
+ public static final int OPEN_READWRITE = 0x00000002;
+
+ /** Open flag to create the database if it does not exist */
+ public static final int OPEN_CREATE = 0x00000004;
+
+ /** Open flag to support URI filenames */
+ public static final int OPEN_URI = 0x00000040;
+
+ /** Open flag opens the database in multi-thread threading mode */
+ public static final int OPEN_NOMUTEX = 0x00008000;
+
+ /** Open flag opens the database in serialized threading mode */
+ public static final int OPEN_FULLMUTEX = 0x00010000;
+
+ /** Open flag opens the database in shared cache mode */
+ public static final int OPEN_SHAREDCACHE = 0x00020000;
+
+ /** Open flag opens the database in private cache mode */
+ public static final int OPEN_PRIVATECACHE = 0x00040000;
+
+ /** Open flag equivalent to {@link #OPEN_READWRITE} | {@link #OPEN_CREATE} */
+ public static final int CREATE_IF_NECESSARY = OPEN_READWRITE | OPEN_CREATE;
+
+ /** Open flag to enable write-ahead logging */ // custom flag remove for sqlite3_open_v2
+ public static final int ENABLE_WRITE_AHEAD_LOGGING = 0x20000000;
+
+ /** Integer flag definition for the database open options */
+ @SuppressLint("UniqueConstants") // duplicate values provided for compatibility
+ @IntDef(flag = true, value = {
+ OPEN_READONLY,
+ OPEN_READWRITE,
+ OPEN_CREATE,
+ OPEN_URI,
+ OPEN_NOMUTEX,
+ OPEN_FULLMUTEX,
+ OPEN_SHAREDCACHE,
+ OPEN_PRIVATECACHE,
+ CREATE_IF_NECESSARY,
+ ENABLE_WRITE_AHEAD_LOGGING})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface OpenFlags {
+ }
+
+ /**
+ * 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(SQLiteDatabaseConfiguration configuration,
+ CursorFactory cursorFactory,
+ DatabaseErrorHandler errorHandler) {
+ mCursorFactory = cursorFactory;
+ mErrorHandler = errorHandler != null ? errorHandler : new DefaultDatabaseErrorHandler();
+ mConfigurationLocked = configuration;
+ }
+
+ @SuppressWarnings("ThrowFromFinallyBlock")
+ @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();
+ }
+
+ /**
+ * 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();
+ * }
+ *
+ */
+ @Override
+ public void beginTransaction() {
+ beginTransaction(null, SQLiteSession.TRANSACTION_MODE_EXCLUSIVE);
+ }
+
+ /**
+ * 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();
+ * }
+ *
+ */
+ @Override
+ public void beginTransactionNonExclusive() {
+ beginTransaction(null, SQLiteSession.TRANSACTION_MODE_IMMEDIATE);
+ }
+
+ /**
+ * Begins a transaction in DEFERRED mode.
+ */
+ public void beginTransactionDeferred() {
+ beginTransaction(null, SQLiteSession.TRANSACTION_MODE_DEFERRED);
+ }
+
+ /**
+ * Begins a transaction in DEFERRED mode.
+ *
+ * @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 beginTransactionWithListenerDeferred(
+ SQLiteTransactionListener transactionListener) {
+ beginTransaction(transactionListener, SQLiteSession.TRANSACTION_MODE_DEFERRED);
+ }
+
+ /**
+ * 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}.
+ */
+ @Override
+ public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
+ beginTransaction(transactionListener, SQLiteSession.TRANSACTION_MODE_EXCLUSIVE);
+ }
+
+ /**
+ * 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}.
+ */
+ @Override
+ public void beginTransactionWithListenerNonExclusive(
+ SQLiteTransactionListener transactionListener) {
+ beginTransaction(transactionListener, SQLiteSession.TRANSACTION_MODE_IMMEDIATE);
+ }
+
+ private void beginTransaction(SQLiteTransactionListener transactionListener, int mode) {
+ acquireReference();
+ try {
+ getThreadSession().beginTransaction(mode, 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.
+ */
+ @Override
+ 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.
+ */
+ @Override
+ 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.
+ */
+ @Override
+ 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.
+ */
+ @Override
+ public boolean isDbLockedByCurrentThread() {
+ acquireReference();
+ try {
+ return getThreadSession().hasConnection();
+ } finally {
+ releaseReference();
+ }
+ }
+
+ /**
+ * 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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();
+ }
+ }
+
+ /**
+ * Open the database according to the flags {@link OpenFlags}
+ *
+ * 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,
+ @OpenFlags int flags) {
+ return openDatabase(path, factory, flags, null);
+ }
+
+ /**
+ * Open the database according to the flags {@link OpenFlags}
+ *
+ * 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,
+ @OpenFlags int flags,
+ DatabaseErrorHandler errorHandler) {
+ SQLiteDatabaseConfiguration configuration = new SQLiteDatabaseConfiguration(path, flags);
+ SQLiteDatabase db = new SQLiteDatabase(configuration, factory, errorHandler);
+ db.open();
+ return db;
+ }
+
+ /**
+ * Open the database according to the given configuration.
+ *
+ * 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 configuration to database configuration to use
+ * @param factory an optional factory class that is called to instantiate a
+ * cursor when query is called, or null for default
+ * @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(SQLiteDatabaseConfiguration configuration,
+ CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ SQLiteDatabase db = new SQLiteDatabase(configuration, 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;
+ 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";
+ final FileFilter filter = new FileFilter() {
+ @Override
+ public boolean accept(File candidate) {
+ return candidate.getName().startsWith(prefix);
+ }
+ };
+ for (File masterJournal : dir.listFiles(filter)) {
+ 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_READONLY);
+ try {
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+ } catch (RuntimeException ex) {
+ mConfigurationLocked.openFlags = oldOpenFlags;
+ throw ex;
+ }
+ }
+ }
+
+ private void open() {
+ try {
+ if (!mConfigurationLocked.isInMemoryDb()
+ && (mConfigurationLocked.openFlags & OPEN_CREATE) != 0) {
+ ensureFile(mConfigurationLocked.path);
+ }
+ try {
+ openInner();
+ } catch (SQLiteDatabaseCorruptException ex) {
+ onCorruption();
+ openInner();
+ }
+ } catch (SQLiteException ex) {
+ Log.e(TAG, "Failed to open database '" + getLabel() + "'.", ex);
+ close();
+ throw ex;
+ }
+ }
+
+ private static void ensureFile(String path) {
+ File file = new File(path);
+ if (!file.exists()) {
+ try {
+ if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
+ // Fixes #103: Check parent directory's existence before
+ // attempting to create.
+ Log.e(TAG, "Couldn't mkdirs " + file);
+ }
+ if (!file.createNewFile()) {
+ Log.e(TAG, "Couldn't create " + file);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't ensure file " + file, e);
+ }
+ }
+ }
+
+ 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
+ */
+ @Deprecated
+ 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;
+ }
+ }
+ }
+
+ /**
+ * Registers a Function 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 addFunction(String name, int numArgs, Function function) {
+ addFunction(name, numArgs, function, 0);
+ }
+
+ /**
+ * Registers a Function 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
+ * @param flags
+ * @hide
+ */
+ public void addFunction(String name, int numArgs, Function function, int flags) {
+ // Create wrapper (also validates arguments).
+ SQLiteFunction wrapper = new SQLiteFunction(name, numArgs, function, flags);
+
+ synchronized (mLock) {
+ throwIfNotOpenLocked();
+
+ mConfigurationLocked.functions.add(wrapper);
+ try {
+ mConnectionPoolLocked.reconfigure(mConfigurationLocked);
+ } catch (RuntimeException ex) {
+ mConfigurationLocked.functions.remove(wrapper);
+ throw ex;
+ }
+ }
+ }
+
+ /**
+ * Gets the database version.
+ *
+ * @return the database version
+ */
+ @Override
+ public int getVersion() {
+ return ((Long) longForQuery("PRAGMA user_version;", null)).intValue();
+ }
+
+ /**
+ * Sets the database version.
+ *
+ * @param version the new database version
+ */
+ @Override
+ 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
+ */
+ @Override
+ public long getMaximumSize() {
+ long pageCount = longForQuery("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
+ */
+ @Override
+ 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 = longForQuery("PRAGMA max_page_count = " + numPages, null);
+ return newPageCount * pageSize;
+ }
+
+ /**
+ * Returns the current database page size, in bytes.
+ *
+ * @return the database page size, in bytes
+ */
+ @Override
+ public long getPageSize() {
+ return longForQuery("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
+ */
+ @Override
+ public void setPageSize(long numBytes) {
+ execSQL("PRAGMA page_size = " + numBytes);
+ }
+
+ /**
+ * 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.
+ */
+ @Override
+ 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ * @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,
+ Object[] 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.
+ * @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,
+ Object[] 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 query the SQL query. The SQL string must not be ; terminated
+ * @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.
+ */
+ @Override
+ public Cursor query(String query) {
+ return rawQueryWithFactory(null, query, null, null, null);
+ }
+
+ /**
+ * Runs the provided SQL and returns a {@link Cursor} over the result set.
+ *
+ * @param query 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.
+ * @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.
+ */
+ @Override
+ public Cursor query(String query, Object[] selectionArgs) {
+ return rawQueryWithFactory(null, query, selectionArgs, null, null);
+ }
+
+ /**
+ * Runs the provided SQL and returns a {@link Cursor} over the result set.
+ *
+ * @param supportQuery the SQL query.
+ * @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.
+ */
+ @Override
+ public Cursor query(final SupportSQLiteQuery supportQuery) {
+ return query(supportQuery, (CancellationSignal) null);
+ }
+
+ /**
+ * Runs the provided SQL and returns a {@link Cursor} over the result set.
+ *
+ * @param supportQuery the SQL query. The SQL string must not be ; terminated
+ * @param signal 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.
+ */
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public Cursor query(SupportSQLiteQuery supportQuery, android.os.CancellationSignal signal) {
+ if (signal != null) {
+ final CancellationSignal supportCancellationSignal = new CancellationSignal();
+ signal.setOnCancelListener(new android.os.CancellationSignal.OnCancelListener() {
+ @Override
+ public void onCancel() {
+ supportCancellationSignal.cancel();
+ }
+ });
+ return query(supportQuery, supportCancellationSignal);
+ } else {
+ return query(supportQuery, (CancellationSignal) null);
+ }
+ }
+
+ /**
+ * Runs the provided SQL and returns a {@link Cursor} over the result set.
+ *
+ * @param supportQuery the SQL query. The SQL string must not be ; terminated
+ * @param signal 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 query(final SupportSQLiteQuery supportQuery, CancellationSignal signal) {
+ return rawQueryWithFactory(new CursorFactory() {
+ @Override
+ public Cursor newCursor(SQLiteDatabase db, SQLiteCursorDriver masterQuery,
+ String editTable, SQLiteQuery query) {
+ supportQuery.bindTo(query);
+ if (mCursorFactory == null) {
+ return new SQLiteCursor(masterQuery, editTable, query);
+ } else {
+ return mCursorFactory.newCursor(db, masterQuery, editTable, query);
+ }
+ }
+ }, supportQuery.getSql(), new String[0], null, signal);
+ }
+
+ /**
+ * 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ * @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, Object[] 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.
+ *
+ * @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.
+ * @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.
+ *
+ * @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 key
+ * @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 conflictAlgorithm for insert conflict resolver
+ * @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 the primary key of the existing row if the input param 'conflictAlgorithm' =
+ * {@link #CONFLICT_IGNORE}
+ * OR -1 if any error
+ */
+ @Override
+ public long insert(String table, @ConflictAlgorithm int conflictAlgorithm,
+ ContentValues values) throws SQLException {
+ return insertWithOnConflict(table, null, values, conflictAlgorithm);
+ }
+
+ /**
+ * 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 the primary key of the existing row if the input param 'conflictAlgorithm' =
+ * {@link #CONFLICT_IGNORE}
+ * OR -1 if any error
+ */
+ @SuppressWarnings("StringConcatenationInsideStringBufferAppend")
+ public long insertWithOnConflict(String table, String nullColumnHack,
+ ContentValues initialValues, @ConflictAlgorithm 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 (Map.Entry entry : initialValues.valueSet()) {
+ sql.append((i > 0) ? "," : "");
+ sql.append(entry.getKey());
+ bindArgs[i++] = entry.getValue();
+ }
+ 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 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.
+ */
+ @Override
+ public int delete(String table, String whereClause, Object[] 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
+ */
+ @Override
+ public int update(String table, @ConflictAlgorithm int conflictAlgorithm, ContentValues values,
+ String whereClause, Object[] whereArgs) {
+ 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 (Map.Entry entry : values.valueSet()) {
+ sql.append((i > 0) ? "," : "");
+ sql.append(entry.getKey());
+ bindArgs[i++] = entry.getValue();
+ 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();
+ }
+ }
+
+ /**
+ * 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, @ConflictAlgorithm 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 (Map.Entry entry : values.valueSet()) {
+ sql.append((i > 0) ? "," : "");
+ sql.append(entry.getKey());
+ bindArgs[i++] = entry.getValue();
+ 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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 {
+ 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(@NonNull String sql, @Nullable CancellationSignal cancellationSignal) {
+ getThreadSession().prepare(sql,
+ getThreadDefaultConnectionFlags(true), cancellationSignal, null);
+ }
+
+ /**
+ * Returns true if the database is opened as read only.
+ *
+ * @return True if database is opened as read only.
+ */
+ @Override
+ public boolean isReadOnly() {
+ synchronized (mLock) {
+ return isReadOnlyLocked();
+ }
+ }
+
+ private boolean isReadOnlyLocked() {
+ return (mConfigurationLocked.openFlags & OPEN_READONLY) == 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).
+ */
+ @Override
+ 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.
+ */
+ @Override
+ public boolean needUpgrade(int newVersion) {
+ return newVersion > getVersion();
+ }
+
+ /**
+ * Gets the path to the database file.
+ *
+ * @return The path to the database file.
+ */
+ @Override
+ public final String getPath() {
+ synchronized (mLock) {
+ return mConfigurationLocked.path;
+ }
+ }
+
+ /**
+ * Sets the locale for this database.
+ *
+ * @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.
+ */
+ @Override
+ 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}.
+ */
+ @Override
+ 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.
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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;
+ }
+
+ 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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.
+ */
+ @Override
+ public List> getAttachedDbs() {
+ ArrayList> attachedDbs = new ArrayList<>();
+ synchronized (mLock) {
+ if (mConnectionPoolLocked == null) {
+ return null; // not open
+ }
+
+ 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.
+ */
+ @Override
+ public boolean isDatabaseIntegrityOk() {
+ acquireReference();
+ try {
+ List> attachedDbs;
+ 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 (Pair p : attachedDbs) {
+ 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)}.
+ */
+ 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.
+ *
+ * This interface is deprecated; new code should prefer {@link Function}
+ */
+ @Deprecated
+ public interface CustomFunction {
+ /**
+ * Invoked whenever the function is called.
+ * @param args function arguments
+ * @return String value of the result or null
+ */
+ String callback(String[] args);
+ }
+
+ /**
+ * A callback interface for a custom sqlite3 function. This can be used to create a function
+ * that can be called from sqlite3 database triggers, or used in queries.
+ */
+ public interface Function {
+ /**
+ * Flag that declares this function to be "deterministic,"
+ * which means it may be used with Indexes on Expressions.
+ */
+ public static final int FLAG_DETERMINISTIC = 0x800;
+
+ interface Args {
+ byte[] getBlob(int arg);
+ String getString(int arg);
+ double getDouble(int arg);
+ int getInt(int arg);
+ long getLong(int arg);
+ }
+
+ interface Result {
+ void set(byte[] value);
+ void set(double value);
+ void set(int value);
+ void set(long value);
+ void set(String value);
+ void setError(String error);
+ void setNull();
+ }
+
+ /**
+ * Invoked whenever the function is called.
+ * @param args function arguments
+ * @return String value of the result or null
+ */
+ void callback(Args args, Result result);
+ }
+
+ static boolean hasCodec() {
+ return SQLiteConnection.hasCodec();
+ }
+
+ void enableLocalizedCollators() {
+ mConnectionPoolLocked.enableLocalizedCollators();
+ }
+
+ /**
+ * Query the table for the number of rows in the table.
+ * @param table the name of the table to query
+ * @return the number of rows in the table
+ */
+ public long queryNumEntries(String table) {
+ return queryNumEntries(table, null, null);
+ }
+
+ /**
+ * Query the table for the number of rows in the table.
+ * @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 long queryNumEntries(String table, String selection) {
+ return queryNumEntries(table, selection, null);
+ }
+
+ /**
+ * Query the table for the number of rows in the table.
+ * @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 long queryNumEntries(String table, String selection, String[] selectionArgs) {
+ String s = (!TextUtils.isEmpty(selection)) ? " where " + selection : "";
+ return longForQuery("select count(*) from " + table + s, selectionArgs);
+ }
+
+ /**
+ * Utility method to run the query on the db and return the value in the
+ * first column of the first row.
+ */
+ public long longForQuery(String query, String[] selectionArgs) {
+ SQLiteStatement prog = 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.
+ */
+ private 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 String stringForQuery(String query, String[] selectionArgs) {
+ SQLiteStatement prog = 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 ParcelFileDescriptor blobFileDescriptorForQuery(String query, String[] selectionArgs) {
+ SQLiteStatement prog = 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();
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java
new file mode 100644
index 0000000000..2087f2bb8e
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDatabaseConfiguration.java
@@ -0,0 +1,203 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import java.util.ArrayList;
+import java.util.List;
+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 @SQLiteDatabase.OpenFlags 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.
+ *
+ * This interface is deprecated; see {@link SQLiteFunction}
+ */
+ @Deprecated
+ public final List customFunctions = new ArrayList<>();
+
+ /**
+ * The {@link SQLiteFunction}s to register.
+ */
+ public final List functions = new ArrayList<>();
+
+ /**
+ * The custom extensions to register.
+ */
+ public final List customExtensions = 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, @SQLiteDatabase.OpenFlags 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 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}.
+ * @param functions custom functions to use.
+ * @param extensions custom extensions to use.
+ */
+ public SQLiteDatabaseConfiguration(String path,
+ @SQLiteDatabase.OpenFlags int openFlags,
+ List customFunctions,
+ List functions,
+ List extensions) {
+ this(path, openFlags);
+ this.customFunctions.addAll(customFunctions);
+ this.customExtensions.addAll(extensions);
+ this.functions.addAll(functions);
+ }
+
+ /**
+ * Creates a database configuration as a copy of another configuration.
+ *
+ * @param other The other configuration.
+ */
+ 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.
+ */
+ 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);
+ customExtensions.clear();
+ customExtensions.addAll(other.customExtensions);
+ functions.clear();
+ functions.addAll(other.functions);
+ }
+
+ /**
+ * 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) {
+ if (path.indexOf('@') == -1) {
+ return path;
+ }
+ return EMAIL_IN_DB_PATTERN.matcher(path).replaceAll("XX@YY");
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDebug.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDebug.java
new file mode 100644
index 0000000000..7a398894bb
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDebug.java
@@ -0,0 +1,173 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.util.Log;
+import android.util.Printer;
+
+import java.util.ArrayList;
+
+/**
+ * Provides debugging info about all SQLite databases running in the current process.
+ *
+ * {@hide}
+ */
+@SuppressWarnings("unused")
+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 boolean shouldLogSlowQuery(long elapsedTimeMillis) {
+ int slowQueryMillis = Integer.parseInt(
+ System.getProperty("db.log.slow_query_threshold", "-1"));
+ 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDirectCursorDriver.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDirectCursorDriver.java
new file mode 100644
index 0000000000..1b69858c10
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteDirectCursorDriver.java
@@ -0,0 +1,85 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.database.Cursor;
+import androidx.core.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(SQLiteDatabase.CursorFactory factory, Object[] selectionArgs) {
+ SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, selectionArgs, mCancellationSignal);
+ final Cursor cursor;
+ try {
+ 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;
+ }
+
+ @Override
+ public void cursorClosed() {
+ // Do nothing
+ }
+
+ @Override
+ public void setBindArguments(String[] bindArgs) {
+ mQuery.bindAllArgsAsStrings(bindArgs);
+ }
+
+ @Override
+ public void cursorDeactivated() {
+ // Do nothing
+ }
+
+ @Override
+ public void cursorRequeried(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public String toString() {
+ return "SQLiteDirectCursorDriver: " + mSql;
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteFunction.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteFunction.java
new file mode 100644
index 0000000000..d9fbcb6b18
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteFunction.java
@@ -0,0 +1,185 @@
+package io.requery.android.database.sqlite;
+
+/**
+ * @author dhleong
+ */
+public class SQLiteFunction {
+ public final String name;
+ public final int numArgs;
+ public final SQLiteDatabase.Function callback;
+
+ // accessed from native code
+ final int flags;
+
+ // NOTE: from a single database connection, all calls to
+ // functions are serialized by SQLITE-internal mutexes,
+ // so we save on GC churn by reusing a single, shared instance
+ private final MyArgs args = new MyArgs();
+ private final MyResult result = new MyResult();
+
+ /**
+ * 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.
+ * @param flags Extra SQLITE flags to pass when creating the function
+ * in native code.
+ */
+ public SQLiteFunction(String name, int numArgs,
+ SQLiteDatabase.Function callback) {
+ this(name, numArgs, callback, 0);
+ }
+
+ /**
+ * 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.
+ * @param flags Extra SQLITE flags to pass when creating the function
+ * in native code.
+ */
+ public SQLiteFunction(String name, int numArgs,
+ SQLiteDatabase.Function callback,
+ int flags) {
+ if (name == null) {
+ throw new IllegalArgumentException("name must not be null.");
+ }
+
+ this.name = name;
+ this.numArgs = numArgs;
+ this.callback = callback;
+ this.flags = flags;
+ }
+
+ // Called from native.
+ @SuppressWarnings("unused")
+ private void dispatchCallback(long contextPtr, long argsPtr, int argsCount) {
+ result.contextPtr = contextPtr;
+ args.argsPtr = argsPtr;
+ args.argsCount = argsCount;
+
+ try {
+ callback.callback(args, result);
+
+ if (!result.isSet) {
+ result.setNull();
+ }
+
+ } finally {
+ result.contextPtr = 0;
+ result.isSet = false;
+ args.argsPtr = 0;
+ args.argsCount = 0;
+ }
+ }
+
+ static native byte[] nativeGetArgBlob(long argsPtr, int arg);
+ static native String nativeGetArgString(long argsPtr, int arg);
+ static native double nativeGetArgDouble(long argsPtr, int arg);
+ static native int nativeGetArgInt(long argsPtr, int arg);
+ static native long nativeGetArgLong(long argsPtr, int arg);
+
+ static native void nativeSetResultBlob(long contextPtr, byte[] result);
+ static native void nativeSetResultString(long contextPtr, String result);
+ static native void nativeSetResultDouble(long contextPtr, double result);
+ static native void nativeSetResultInt(long contextPtr, int result);
+ static native void nativeSetResultLong(long contextPtr, long result);
+ static native void nativeSetResultError(long contextPtr, String error);
+ static native void nativeSetResultNull(long contextPtr);
+
+ private static class MyArgs implements SQLiteDatabase.Function.Args {
+ long argsPtr;
+ int argsCount;
+
+ @Override
+ public byte[] getBlob(int arg) {
+ return nativeGetArgBlob(argsPtr, checkArg(arg));
+ }
+
+ @Override
+ public String getString(int arg) {
+ return nativeGetArgString(argsPtr, checkArg(arg));
+ }
+
+ @Override
+ public double getDouble(int arg) {
+ return nativeGetArgDouble(argsPtr, checkArg(arg));
+ }
+
+ @Override
+ public int getInt(int arg) {
+ return nativeGetArgInt(argsPtr, checkArg(arg));
+ }
+
+ @Override
+ public long getLong(int arg) {
+ return nativeGetArgLong(argsPtr, checkArg(arg));
+ }
+
+ private int checkArg(int arg) {
+ if (arg < 0 || arg >= argsCount) {
+ throw new IllegalArgumentException(
+ "Requested arg " + arg + " but had " + argsCount
+ );
+ }
+
+ return arg;
+ }
+ }
+
+ private static class MyResult implements SQLiteDatabase.Function.Result {
+ long contextPtr;
+ boolean isSet;
+
+ @Override
+ public void set(byte[] value) {
+ checkSet();
+ nativeSetResultBlob(contextPtr, value);
+ }
+
+ @Override
+ public void set(double value) {
+ checkSet();
+ nativeSetResultDouble(contextPtr, value);
+ }
+
+ @Override
+ public void set(int value) {
+ checkSet();
+ nativeSetResultInt(contextPtr, value);
+ }
+
+ @Override
+ public void set(long value) {
+ checkSet();
+ nativeSetResultLong(contextPtr, value);
+ }
+
+ @Override
+ public void set(String value) {
+ checkSet();
+ nativeSetResultString(contextPtr, value);
+ }
+
+ @Override
+ public void setError(String error) {
+ checkSet();
+ nativeSetResultError(contextPtr, error);
+ }
+
+ @Override
+ public void setNull() {
+ checkSet();
+ nativeSetResultNull(contextPtr);
+ }
+
+ private void checkSet() {
+ if (isSet) throw new IllegalStateException("Result is already set");
+ isSet = true;
+ }
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteGlobal.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteGlobal.java
new file mode 100644
index 0000000000..6e1281affe
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteGlobal.java
@@ -0,0 +1,116 @@
+/*
+ * 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 from original source see README at the top level of this project
+/*
+** Modified to support SQLite extensions by the SQLite developers:
+** sqlite-dev@sqlite.org.
+*/
+
+package io.requery.android.database.sqlite;
+
+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 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();
+ }
+
+ // values derived from:
+ // https://android.googlesource.com/platform/frameworks/base.git/+/master/core/res/res/values/config.xml
+
+ /**
+ * Gets the default page size to use when creating a database.
+ */
+ @SuppressWarnings("deprecation")
+ public static int getDefaultPageSize() {
+ synchronized (sLock) {
+ if (sDefaultPageSize == 0) {
+ sDefaultPageSize = new StatFs("/data").getBlockSize();
+ }
+ return 1024;
+ }
+ }
+
+ /**
+ * Gets the default journal mode when WAL is not in use.
+ */
+ public static String getDefaultJournalMode() {
+ return "TRUNCATE";
+ }
+
+ /**
+ * Gets the journal size limit in bytes.
+ */
+ public static int getJournalSizeLimit() {
+ return 524288;
+ }
+
+ /**
+ * Gets the default database synchronization mode when WAL is not in use.
+ */
+ public static String getDefaultSyncMode() {
+ return "FULL";
+ }
+
+ /**
+ * 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() {
+ return 1000;
+ }
+
+ /**
+ * Gets the connection pool size when in WAL mode.
+ */
+ public static int getWALConnectionPoolSize() {
+ return 10;
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteOpenHelper.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteOpenHelper.java
new file mode 100644
index 0000000000..dd5e4d0fcb
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteOpenHelper.java
@@ -0,0 +1,410 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import io.requery.android.database.DatabaseErrorHandler;
+
+/**
+ * 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.
+ */
+@SuppressWarnings("unused")
+public abstract class SQLiteOpenHelper implements SupportSQLiteOpenHelper {
+ 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 SQLiteDatabase.CursorFactory mFactory;
+ private final int mNewVersion;
+
+ 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,
+ SQLiteDatabase.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,
+ SQLiteDatabase.CursorFactory factory,
+ int version,
+ DatabaseErrorHandler errorHandler) {
+ if (version < 1) throw new IllegalArgumentException("Version must be >= 1, was " + version);
+
+ mContext = context;
+ mName = name;
+ mFactory = factory;
+ mNewVersion = version;
+ mErrorHandler = errorHandler;
+ }
+
+ /**
+ * Return the name of the SQLite database being opened, as given to
+ * the constructor.
+ */
+ @Override
+ 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()
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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.
+ */
+ @Override
+ 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 (db.isReadOnly()) {
+ db.reopenReadWrite();
+ }
+ } else if (mName == null) {
+ db = SQLiteDatabase.create(null);
+ } else {
+ try {
+ final String path = mContext.getDatabasePath(mName).getPath();
+ if (DEBUG_STRICT_READONLY && !writable) {
+ SQLiteDatabaseConfiguration configuration =
+ createConfiguration(path, SQLiteDatabase.OPEN_READONLY);
+ db = SQLiteDatabase.openDatabase(configuration, mFactory, mErrorHandler);
+ } else {
+ int flags = mEnableWriteAheadLogging ?
+ SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING : 0;
+ flags |= SQLiteDatabase.CREATE_IF_NECESSARY;
+ SQLiteDatabaseConfiguration configuration =
+ createConfiguration(path, flags);
+ db = SQLiteDatabase.openDatabase(configuration, mFactory, mErrorHandler);
+ }
+ } catch (SQLiteException ex) {
+ if (writable) {
+ throw ex;
+ }
+ Log.e(TAG, "Couldn't open " + mName
+ + " for writing (will try read-only):", ex);
+ final String path = mContext.getDatabasePath(mName).getPath();
+ SQLiteDatabaseConfiguration configuration =
+ createConfiguration(path, SQLiteDatabase.OPEN_READONLY);
+ db = SQLiteDatabase.openDatabase(configuration, mFactory, 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);
+ }
+
+ 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.
+ */
+ @Override
+ 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 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) {}
+
+ /**
+ * Called before the database is opened. Provides the {@link SQLiteDatabaseConfiguration}
+ * instance that is used to initialize the database. Override this to create a configuration
+ * that has custom functions or extensions.
+ *
+ * @param path to database file to open and/or create
+ * @param openFlags to control database access mode
+ * @return {@link SQLiteDatabaseConfiguration} instance, cannot be null.
+ */
+ protected SQLiteDatabaseConfiguration createConfiguration(String path,
+ @SQLiteDatabase.OpenFlags int openFlags) {
+ return new SQLiteDatabaseConfiguration(path, openFlags);
+ }
+}
diff --git a/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteProgram.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteProgram.java
new file mode 100644
index 0000000000..2399023ed5
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteProgram.java
@@ -0,0 +1,246 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import androidx.core.os.CancellationSignal;
+import androidx.sqlite.db.SupportSQLiteProgram;
+
+import java.util.Arrays;
+
+/**
+ * A base class for compiled SQLite programs.
+ *
+ * This class is not thread-safe.
+ *
+ */
+@SuppressWarnings("unused")
+public abstract class SQLiteProgram extends SQLiteClosable implements SupportSQLiteProgram {
+ 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 = SQLiteStatementType.getSqlStatementType(mSql);
+ switch (n) {
+ case SQLiteStatementType.STATEMENT_BEGIN:
+ case SQLiteStatementType.STATEMENT_COMMIT:
+ case SQLiteStatementType.STATEMENT_ABORT:
+ mReadOnly = false;
+ mColumnNames = EMPTY_STRING_ARRAY;
+ mNumParameters = 0;
+ break;
+
+ default:
+ boolean assumeReadOnly = (n == SQLiteStatementType.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();
+ }
+
+ /**
+ * 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ 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
+ */
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ if (value == null) {
+ throw new IllegalArgumentException("the bind value at index " + index + " is null");
+ }
+ bind(index, value);
+ }
+
+ /**
+ * 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 index the 1-based index to bind at
+ * @param value the value to bind
+ */
+ public void bindObject(int index, Object value) {
+ if (value == null) {
+ bindNull(index);
+ } else if (value instanceof Double || value instanceof Float) {
+ bindDouble(index, ((Number)value).doubleValue());
+ } else if (value instanceof Number) {
+ bindLong(index, ((Number)value).longValue());
+ } else if (value instanceof Boolean) {
+ Boolean bool = (Boolean)value;
+ if (bool) {
+ bindLong(index, 1);
+ } else {
+ bindLong(index, 0);
+ }
+ } else if (value instanceof byte[]){
+ bindBlob(index, (byte[]) value);
+ } else {
+ bindString(index, value.toString());
+ }
+ }
+
+ /**
+ * Clears all existing bindings. Unset bindings are treated as NULL.
+ */
+ @Override
+ 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQuery.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQuery.java
new file mode 100644
index 0000000000..3632ad10c0
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQuery.java
@@ -0,0 +1,86 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+import io.requery.android.database.CursorWindow;
+
+/**
+ * Represents a query that reads the resulting rows into a {@link SQLiteQuery}.
+ * This class is used by {@link SQLiteCursor} and isn't useful itself.
+ *
+ * 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, Object[] bindArgs,
+ CancellationSignal cancellationSignal) {
+ super(db, query, bindArgs, 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 {
+ return getSession().executeForCursorWindow(getSql(), getBindArgs(),
+ window, startPos, requiredPos, countAllRows, getConnectionFlags(),
+ mCancellationSignal);
+ } 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQueryBuilder.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQueryBuilder.java
new file mode 100644
index 0000000000..a22c413262
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteQueryBuilder.java
@@ -0,0 +1,612 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.os.OperationCanceledException;
+import android.provider.BaseColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import androidx.core.os.CancellationSignal;
+
+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.
+ */
+@SuppressWarnings("unused")
+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);
+ }
+
+ /**
+ * 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 */);
+ }
+
+ /**
+ * 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteSession.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteSession.java
new file mode 100644
index 0000000000..1f3201d3a2
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteSession.java
@@ -0,0 +1,975 @@
+/*
+ * 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 from original source see README at the top level of this project
+/*
+** Modified to support SQLite extensions by the SQLite developers:
+** sqlite-dev@sqlite.org.
+*/
+
+package io.requery.android.database.sqlite;
+
+import android.annotation.SuppressLint;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteTransactionListener;
+import android.os.ParcelFileDescriptor;
+import androidx.core.os.CancellationSignal;
+import androidx.core.os.OperationCanceledException;
+import io.requery.android.database.CursorWindow;
+
+/**
+ * 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 #endTransaction}. 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
+ */
+@SuppressWarnings({"unused", "JavaDoc"})
+@SuppressLint("Assert")
+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 = SQLiteStatementType.getSqlStatementType(sql);
+ switch (type) {
+ case SQLiteStatementType.STATEMENT_BEGIN:
+ beginTransaction(TRANSACTION_MODE_EXCLUSIVE, null, connectionFlags,
+ cancellationSignal);
+ return true;
+
+ case SQLiteStatementType.STATEMENT_COMMIT:
+ setTransactionSuccessful();
+ endTransaction(cancellationSignal);
+ return true;
+
+ case SQLiteStatementType.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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatement.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatement.java
new file mode 100644
index 0000000000..34edd8ec9c
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatement.java
@@ -0,0 +1,172 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDoneException;
+import android.os.ParcelFileDescriptor;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+/**
+ * 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.
+ *
+ */
+@SuppressWarnings("unused")
+public final class SQLiteStatement extends SQLiteProgram implements SupportSQLiteStatement {
+
+ 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 SQLException If the SQL string is invalid for some reason
+ */
+ @Override
+ 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 SQLException If the SQL string is invalid for some reason
+ */
+ @Override
+ 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 SQLException If the SQL string is invalid for some reason
+ */
+ @Override
+ 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 SQLiteDoneException if the query returns zero rows
+ */
+ @Override
+ 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 SQLiteDoneException if the query returns zero rows
+ */
+ @Override
+ 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 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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementInfo.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementInfo.java
new file mode 100644
index 0000000000..c8d50bc395
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementInfo.java
@@ -0,0 +1,40 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.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/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementType.java b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementType.java
new file mode 100644
index 0000000000..5cb412e751
--- /dev/null
+++ b/sqlite-android/src/main/java/io/requery/android/database/sqlite/SQLiteStatementType.java
@@ -0,0 +1,107 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+package io.requery.android.database.sqlite;
+
+import java.util.Locale;
+
+class SQLiteStatementType {
+
+ /** 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;
+
+ private SQLiteStatementType() {
+ }
+
+ /**
+ * Returns one of the following which represent the type of the given SQL statement.
+ *
+ * {@link #STATEMENT_SELECT}
+ * {@link #STATEMENT_UPDATE}
+ * {@link #STATEMENT_ATTACH}
+ * {@link #STATEMENT_BEGIN}
+ * {@link #STATEMENT_COMMIT}
+ * {@link #STATEMENT_ABORT}
+ * {@link #STATEMENT_OTHER}
+ *
+ * @param sql the SQL statement whose type is returned by this method
+ * @return one of the values listed above
+ */
+ public static int getSqlStatementType(String sql) {
+ sql = sql.trim();
+ if (sql.length() < 3) {
+ return STATEMENT_OTHER;
+ }
+ String prefixSql = sql.substring(0, 3);
+
+ if (prefixSql.equalsIgnoreCase("SEL")
+ || prefixSql.equalsIgnoreCase("WIT")) {
+ return STATEMENT_SELECT;
+ }
+ if (prefixSql.equalsIgnoreCase("INS")
+ || prefixSql.equalsIgnoreCase("UPD")
+ || prefixSql.equalsIgnoreCase("REP")
+ || prefixSql.equalsIgnoreCase("DEL")) {
+ return STATEMENT_UPDATE;
+ }
+ if (prefixSql.equalsIgnoreCase("ATT")) {
+ return STATEMENT_ATTACH;
+ }
+ if (prefixSql.equalsIgnoreCase("COM")
+ || prefixSql.equalsIgnoreCase("END")) {
+ return STATEMENT_COMMIT;
+ }
+ if (prefixSql.equalsIgnoreCase("ROL")) {
+ return STATEMENT_ABORT;
+ }
+ if (prefixSql.equalsIgnoreCase("BEG")) {
+ return STATEMENT_BEGIN;
+ }
+ if (prefixSql.equalsIgnoreCase("PRA")) {
+ return STATEMENT_PRAGMA;
+ }
+ if (prefixSql.equalsIgnoreCase("CRE")
+ || prefixSql.equalsIgnoreCase("DRO")
+ || prefixSql.equalsIgnoreCase("ALT")) {
+ return STATEMENT_DDL;
+ }
+
+ if (prefixSql.equalsIgnoreCase("ANA") || prefixSql.equalsIgnoreCase("DET")) {
+ return STATEMENT_UNPREPARED;
+ }
+
+ return STATEMENT_OTHER;
+ }
+}
diff --git a/sqlite-android/src/main/jni/Android.mk b/sqlite-android/src/main/jni/Android.mk
new file mode 100644
index 0000000000..e372509cbb
--- /dev/null
+++ b/sqlite-android/src/main/jni/Android.mk
@@ -0,0 +1,4 @@
+
+LOCAL_PATH:= $(call my-dir)
+include $(LOCAL_PATH)/sqlite/Android.mk
+
diff --git a/sqlite-android/src/main/jni/Application.mk b/sqlite-android/src/main/jni/Application.mk
new file mode 100644
index 0000000000..89c88ffbf3
--- /dev/null
+++ b/sqlite-android/src/main/jni/Application.mk
@@ -0,0 +1,5 @@
+APP_STL:=none
+APP_OPTIM := release
+APP_ABI := armeabi-v7a,arm64-v8a,x86,x86_64
+NDK_TOOLCHAIN_VERSION := clang
+NDK_APP_LIBS_OUT=../jniLibs
diff --git a/sqlite-android/src/main/jni/sqlite/ALog-priv.h b/sqlite-android/src/main/jni/sqlite/ALog-priv.h
new file mode 100644
index 0000000000..04a0abf398
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/ALog-priv.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright 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.
+ */
+
+#ifndef NATIVEHELPER_ALOGPRIV_H_
+#define NATIVEHELPER_ALOGPRIV_H_
+
+#include
+
+#ifndef LOG_NDEBUG
+#ifdef NDEBUG
+#define LOG_NDEBUG 1
+#else
+#define LOG_NDEBUG 0
+#endif
+#endif
+
+
+/*
+ * Basic log message macros intended to emulate the behavior of log/log.h
+ * in system core. This should be dependent only on ndk exposed logging
+ * functionality.
+ */
+
+#ifndef ALOG
+#define ALOG(priority, tag, fmt...) \
+ __android_log_print(ANDROID_##priority, tag, fmt)
+#endif
+
+#ifndef ALOGV
+#if LOG_NDEBUG
+#define ALOGV(...) ((void)0)
+#else
+#define ALOGV(...) ((void)ALOG(LOG_VERBOSE, LOG_TAG, __VA_ARGS__))
+#endif
+#endif
+
+#ifndef ALOGD
+#define ALOGD(...) ((void)ALOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef ALOGI
+#define ALOGI(...) ((void)ALOG(LOG_INFO, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef ALOGW
+#define ALOGW(...) ((void)ALOG(LOG_WARN, LOG_TAG, __VA_ARGS__))
+#endif
+
+#ifndef ALOGE
+#define ALOGE(...) ((void)ALOG(LOG_ERROR, LOG_TAG, __VA_ARGS__))
+#endif
+
+/*
+** Not quite the same as the core android LOG_FATAL_IF (which also
+** sends a SIGTRAP), but close enough.
+*/
+#define LOG_FATAL_IF(bCond, zErr) if( bCond ) ALOGE(zErr);
+
+#endif
diff --git a/sqlite-android/src/main/jni/sqlite/Android.mk b/sqlite-android/src/main/jni/sqlite/Android.mk
new file mode 100644
index 0000000000..861c9acde7
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/Android.mk
@@ -0,0 +1,67 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# NOTE the following flags,
+# SQLITE_TEMP_STORE=3 causes all TEMP files to go into RAM. and thats the behavior we want
+# SQLITE_ENABLE_FTS3 enables usage of FTS3 - NOT FTS1 or 2.
+# SQLITE_DEFAULT_AUTOVACUUM=1 causes the databases to be subject to auto-vacuum
+sqlite_flags := \
+ -DNDEBUG=1 \
+ -DHAVE_USLEEP=1 \
+ -DSQLITE_HAVE_ISNAN \
+ -DSQLITE_DEFAULT_JOURNAL_SIZE_LIMIT=1048576 \
+ -DSQLITE_THREADSAFE=2 \
+ -DSQLITE_TEMP_STORE=3 \
+ -DSQLITE_POWERSAFE_OVERWRITE=1 \
+ -DSQLITE_DEFAULT_FILE_FORMAT=4 \
+ -DSQLITE_DEFAULT_AUTOVACUUM=1 \
+ -DSQLITE_ENABLE_MEMORY_MANAGEMENT=1 \
+ -DSQLITE_ENABLE_FTS3 \
+ -DSQLITE_ENABLE_FTS3_PARENTHESIS \
+ -DSQLITE_ENABLE_FTS4 \
+ -DSQLITE_ENABLE_FTS4_PARENTHESIS \
+ -DSQLITE_ENABLE_FTS5 \
+ -DSQLITE_ENABLE_FTS5_PARENTHESIS \
+ -DSQLITE_ENABLE_JSON1 \
+ -DSQLITE_ENABLE_RTREE=1 \
+ -DSQLITE_UNTESTABLE \
+ -DSQLITE_OMIT_COMPILEOPTION_DIAGS \
+ -DSQLITE_DEFAULT_FILE_PERMISSIONS=0600 \
+ -DSQLITE_DEFAULT_MEMSTATUS=0 \
+ -DSQLITE_MAX_EXPR_DEPTH=0 \
+ -DSQLITE_USE_ALLOCA \
+ -DSQLITE_ENABLE_BATCH_ATOMIC_WRITE \
+ -O3
+
+LOCAL_CFLAGS += $(sqlite_flags)
+LOCAL_CFLAGS += -Wno-unused-parameter -Wno-int-to-pointer-cast
+LOCAL_CFLAGS += -Wno-uninitialized -Wno-parentheses
+LOCAL_CPPFLAGS += -Wno-conversion-null
+
+
+ifeq ($(TARGET_ARCH), arm)
+ LOCAL_CFLAGS += -DPACKED="__attribute__ ((packed))"
+else
+ LOCAL_CFLAGS += -DPACKED=""
+endif
+
+LOCAL_SRC_FILES:= \
+ android_database_SQLiteCommon.cpp \
+ android_database_SQLiteConnection.cpp \
+ android_database_SQLiteFunction.cpp \
+ android_database_SQLiteGlobal.cpp \
+ android_database_SQLiteDebug.cpp \
+ android_database_CursorWindow.cpp \
+ CursorWindow.cpp \
+ JNIHelp.cpp \
+ JNIString.cpp
+
+LOCAL_SRC_FILES += sqlite3.c
+
+LOCAL_C_INCLUDES += $(LOCAL_PATH)
+
+LOCAL_MODULE:= libsqlite3x
+LOCAL_LDLIBS += -ldl -llog -latomic
+
+include $(BUILD_SHARED_LIBRARY)
+
diff --git a/sqlite-android/src/main/jni/sqlite/CursorWindow.cpp b/sqlite-android/src/main/jni/sqlite/CursorWindow.cpp
new file mode 100644
index 0000000000..3e57543914
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/CursorWindow.cpp
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2006-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 from original source see README at the top level of this project
+
+#undef LOG_TAG
+#define LOG_TAG "CursorWindow"
+
+#include "CursorWindow.h"
+#include "ALog-priv.h"
+
+#include
+#include
+#include
+
+namespace android {
+
+CursorWindow::CursorWindow(const char* name, void* data, size_t size, bool readOnly) :
+ mData(data), mSize(size), mReadOnly(readOnly) {
+ mName = strdup(name);
+ mHeader = static_cast(mData);
+}
+
+CursorWindow::~CursorWindow() {
+ free(mName);
+ free(mData);
+}
+
+status_t CursorWindow::create(const char* name, size_t size, CursorWindow** outWindow) {
+ status_t result;
+ void* data = malloc(size);
+ if (!data) {
+ return NO_MEMORY;
+ }
+ CursorWindow* window = new CursorWindow(name, data, size, false);
+ result = window->clear();
+ if (!result) {
+ LOG_WINDOW("Created new CursorWindow: freeOffset=%d, "
+ "numRows=%d, numColumns=%d, mSize=%d, mData=%p",
+ window->mHeader->freeOffset,
+ window->mHeader->numRows,
+ window->mHeader->numColumns,
+ window->mSize, window->mData);
+ *outWindow = window;
+ return OK;
+ }
+ delete window;
+ return result;
+}
+
+status_t CursorWindow::clear() {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk);
+ mHeader->firstChunkOffset = sizeof(Header);
+ mHeader->numRows = 0;
+ mHeader->numColumns = 0;
+
+ RowSlotChunk* firstChunk = static_cast(offsetToPtr(mHeader->firstChunkOffset));
+ firstChunk->nextChunkOffset = 0;
+ return OK;
+}
+
+status_t CursorWindow::setNumColumns(uint32_t numColumns) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ uint32_t cur = mHeader->numColumns;
+ if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) {
+ ALOGE("Trying to go from %d columns to %d", cur, numColumns);
+ return INVALID_OPERATION;
+ }
+ mHeader->numColumns = numColumns;
+ return OK;
+}
+
+status_t CursorWindow::allocRow() {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ // Fill in the row slot
+ RowSlot* rowSlot = allocRowSlot();
+ if (rowSlot == NULL) {
+ return NO_MEMORY;
+ }
+
+ // Allocate the slots for the field directory
+ size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot);
+ uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/);
+ if (!fieldDirOffset) {
+ mHeader->numRows--;
+ LOG_WINDOW("The row failed, so back out the new row accounting "
+ "from allocRowSlot %d", mHeader->numRows);
+ return NO_MEMORY;
+ }
+ FieldSlot* fieldDir = static_cast(offsetToPtr(fieldDirOffset));
+ memset(fieldDir, 0, fieldDirSize);
+
+ //LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %d bytes at offset %u\n",
+ // mHeader->numRows - 1, offsetFromPtr(rowSlot), fieldDirSize, fieldDirOffset);
+ rowSlot->offset = fieldDirOffset;
+ return OK;
+}
+
+status_t CursorWindow::freeLastRow() {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ if (mHeader->numRows > 0) {
+ mHeader->numRows--;
+ }
+ return OK;
+}
+
+uint32_t CursorWindow::alloc(size_t size, bool aligned) {
+ uint32_t padding;
+ if (aligned) {
+ // 4 byte alignment
+ padding = (~mHeader->freeOffset + 1) & 3;
+ } else {
+ padding = 0;
+ }
+
+ uint32_t offset = mHeader->freeOffset + padding;
+ uint32_t nextFreeOffset = offset + size;
+ if (nextFreeOffset > mSize) {
+ ALOGW("Window is full: requested allocation %zu bytes, "
+ "free space %zu bytes, window size %zu bytes",
+ size, freeSpace(), mSize);
+ return 0;
+ }
+
+ mHeader->freeOffset = nextFreeOffset;
+ return offset;
+}
+
+CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) {
+ uint32_t chunkPos = row;
+ RowSlotChunk* chunk = static_cast(
+ offsetToPtr(mHeader->firstChunkOffset));
+ while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) {
+ chunk = static_cast(offsetToPtr(chunk->nextChunkOffset));
+ chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
+ }
+ return &chunk->slots[chunkPos];
+}
+
+CursorWindow::RowSlot* CursorWindow::allocRowSlot() {
+ uint32_t chunkPos = mHeader->numRows;
+ RowSlotChunk* chunk = static_cast(
+ offsetToPtr(mHeader->firstChunkOffset));
+ while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) {
+ chunk = static_cast(offsetToPtr(chunk->nextChunkOffset));
+ chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS;
+ }
+ if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) {
+ if (!chunk->nextChunkOffset) {
+ chunk->nextChunkOffset = alloc(sizeof(RowSlotChunk), true /*aligned*/);
+ if (!chunk->nextChunkOffset) {
+ return NULL;
+ }
+ }
+ chunk = static_cast(offsetToPtr(chunk->nextChunkOffset));
+ chunk->nextChunkOffset = 0;
+ chunkPos = 0;
+ }
+ mHeader->numRows += 1;
+ return &chunk->slots[chunkPos];
+}
+
+CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) {
+ if (row >= mHeader->numRows || column >= mHeader->numColumns) {
+ ALOGE("Failed to read row %d, column %d from a CursorWindow which "
+ "has %d rows, %d columns.",
+ row, column, mHeader->numRows, mHeader->numColumns);
+ return NULL;
+ }
+ RowSlot* rowSlot = getRowSlot(row);
+ if (!rowSlot) {
+ ALOGE("Failed to find rowSlot for row %d.", row);
+ return NULL;
+ }
+ FieldSlot* fieldDir = static_cast(offsetToPtr(rowSlot->offset));
+ return &fieldDir[column];
+}
+
+status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) {
+ return putBlobOrString(row, column, value, size, FIELD_TYPE_BLOB);
+}
+
+status_t CursorWindow::putString(uint32_t row, uint32_t column, const char* value,
+ size_t sizeIncludingNull) {
+ return putBlobOrString(row, column, value, sizeIncludingNull, FIELD_TYPE_STRING);
+}
+
+status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column,
+ const void* value, size_t size, int32_t type) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ FieldSlot* fieldSlot = getFieldSlot(row, column);
+ if (!fieldSlot) {
+ return BAD_VALUE;
+ }
+
+ uint32_t offset = alloc(size);
+ if (!offset) {
+ return NO_MEMORY;
+ }
+
+ memcpy(offsetToPtr(offset), value, size);
+
+ fieldSlot->type = type;
+ fieldSlot->data.buffer.offset = offset;
+ fieldSlot->data.buffer.size = size;
+ return OK;
+}
+
+status_t CursorWindow::putLong(uint32_t row, uint32_t column, int64_t value) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ FieldSlot* fieldSlot = getFieldSlot(row, column);
+ if (!fieldSlot) {
+ return BAD_VALUE;
+ }
+
+ fieldSlot->type = FIELD_TYPE_INTEGER;
+ fieldSlot->data.l = value;
+ return OK;
+}
+
+status_t CursorWindow::putDouble(uint32_t row, uint32_t column, double value) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ FieldSlot* fieldSlot = getFieldSlot(row, column);
+ if (!fieldSlot) {
+ return BAD_VALUE;
+ }
+
+ fieldSlot->type = FIELD_TYPE_FLOAT;
+ fieldSlot->data.d = value;
+ return OK;
+}
+
+status_t CursorWindow::putNull(uint32_t row, uint32_t column) {
+ if (mReadOnly) {
+ return INVALID_OPERATION;
+ }
+
+ FieldSlot* fieldSlot = getFieldSlot(row, column);
+ if (!fieldSlot) {
+ return BAD_VALUE;
+ }
+
+ fieldSlot->type = FIELD_TYPE_NULL;
+ fieldSlot->data.buffer.offset = 0;
+ fieldSlot->data.buffer.size = 0;
+ return OK;
+}
+
+}; // namespace android
diff --git a/sqlite-android/src/main/jni/sqlite/CursorWindow.h b/sqlite-android/src/main/jni/sqlite/CursorWindow.h
new file mode 100644
index 0000000000..aceeb6347e
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/CursorWindow.h
@@ -0,0 +1,188 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#ifndef _ANDROID__DATABASE_WINDOW_H
+#define _ANDROID__DATABASE_WINDOW_H
+
+#include "ALog-priv.h"
+#include
+#include
+
+#include "Errors.h"
+
+#if LOG_NDEBUG
+
+#define IF_LOG_WINDOW() if (false)
+#define LOG_WINDOW(...)
+
+#else
+
+#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow")
+#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__)
+
+#endif
+
+namespace android {
+
+/**
+ * This class stores a set of rows from a database in a buffer. The beginning of the
+ * window has first chunk of RowSlots, which are offsets to the row directory, followed by
+ * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case
+ * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a
+ * FieldSlot per column, which has the size, offset, and type of the data for that field.
+ * Note that the data types come from sqlite3.h.
+ *
+ * Strings are stored in UTF-8.
+ */
+class CursorWindow {
+ CursorWindow(const char* name, void* data, size_t size, bool readOnly);
+
+public:
+ /* Field types. */
+ enum {
+ FIELD_TYPE_NULL = 0,
+ FIELD_TYPE_INTEGER = 1,
+ FIELD_TYPE_FLOAT = 2,
+ FIELD_TYPE_STRING = 3,
+ FIELD_TYPE_BLOB = 4,
+ };
+
+ /* Opaque type that describes a field slot. */
+ struct FieldSlot {
+ private:
+ int32_t type;
+ union {
+ double d;
+ int64_t l;
+ struct {
+ uint32_t offset;
+ uint32_t size;
+ } buffer;
+ } data;
+
+ friend class CursorWindow;
+ } __attribute((packed));
+
+ ~CursorWindow();
+
+ static status_t create(const char* name, size_t size, CursorWindow** outCursorWindow);
+
+ inline const char* name() { return mName; }
+ inline size_t size() { return mSize; }
+ inline size_t freeSpace() { return mSize - mHeader->freeOffset; }
+ inline uint32_t getNumRows() { return mHeader->numRows; }
+ inline uint32_t getNumColumns() { return mHeader->numColumns; }
+
+ status_t clear();
+ status_t setNumColumns(uint32_t numColumns);
+
+ /**
+ * Allocate a row slot and its directory.
+ * The row is initialized will null entries for each field.
+ */
+ status_t allocRow();
+ status_t freeLastRow();
+
+ status_t putBlob(uint32_t row, uint32_t column, const void* value, size_t size);
+ status_t putString(uint32_t row, uint32_t column, const char* value, size_t sizeIncludingNull);
+ status_t putLong(uint32_t row, uint32_t column, int64_t value);
+ status_t putDouble(uint32_t row, uint32_t column, double value);
+ status_t putNull(uint32_t row, uint32_t column);
+
+ /**
+ * Gets the field slot at the specified row and column.
+ * Returns null if the requested row or column is not in the window.
+ */
+ FieldSlot* getFieldSlot(uint32_t row, uint32_t column);
+
+ inline int32_t getFieldSlotType(FieldSlot* fieldSlot) {
+ return fieldSlot->type;
+ }
+
+ inline int64_t getFieldSlotValueLong(FieldSlot* fieldSlot) {
+ return fieldSlot->data.l;
+ }
+
+ inline double getFieldSlotValueDouble(FieldSlot* fieldSlot) {
+ return fieldSlot->data.d;
+ }
+
+ inline const char* getFieldSlotValueString(FieldSlot* fieldSlot,
+ size_t* outSizeIncludingNull) {
+ *outSizeIncludingNull = fieldSlot->data.buffer.size;
+ return static_cast(offsetToPtr(fieldSlot->data.buffer.offset));
+ }
+
+ inline const void* getFieldSlotValueBlob(FieldSlot* fieldSlot, size_t* outSize) {
+ *outSize = fieldSlot->data.buffer.size;
+ return offsetToPtr(fieldSlot->data.buffer.offset);
+ }
+
+private:
+ static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100;
+
+ struct Header {
+ // Offset of the lowest unused byte in the window.
+ uint32_t freeOffset;
+
+ // Offset of the first row slot chunk.
+ uint32_t firstChunkOffset;
+
+ uint32_t numRows;
+ uint32_t numColumns;
+ };
+
+ struct RowSlot {
+ uint32_t offset;
+ };
+
+ struct RowSlotChunk {
+ RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS];
+ uint32_t nextChunkOffset;
+ };
+
+ char* mName;
+ void* mData;
+ size_t mSize;
+ bool mReadOnly;
+ Header* mHeader;
+
+ inline void* offsetToPtr(uint32_t offset) {
+ return static_cast(mData) + offset;
+ }
+
+ inline uint32_t offsetFromPtr(void* ptr) {
+ return static_cast(ptr) - static_cast(mData);
+ }
+
+ /**
+ * Allocate a portion of the window. Returns the offset
+ * of the allocation, or 0 if there isn't enough space.
+ * If aligned is true, the allocation gets 4 byte alignment.
+ */
+ uint32_t alloc(size_t size, bool aligned = false);
+
+ RowSlot* getRowSlot(uint32_t row);
+ RowSlot* allocRowSlot();
+
+ status_t putBlobOrString(uint32_t row, uint32_t column,
+ const void* value, size_t size, int32_t type);
+};
+
+}; // namespace android
+
+#endif
diff --git a/sqlite-android/src/main/jni/sqlite/Errors.h b/sqlite-android/src/main/jni/sqlite/Errors.h
new file mode 100644
index 0000000000..0b75b1926c
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/Errors.h
@@ -0,0 +1,88 @@
+/*
+ * 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.
+ */
+
+#ifndef ANDROID_ERRORS_H
+#define ANDROID_ERRORS_H
+
+#include
+#include
+
+namespace android {
+
+// use this type to return error codes
+#ifdef HAVE_MS_C_RUNTIME
+typedef int status_t;
+#else
+typedef int32_t status_t;
+#endif
+
+/* the MS C runtime lacks a few error codes */
+
+/*
+ * Error codes.
+ * All error codes are negative values.
+ */
+
+// Win32 #defines NO_ERROR as well. It has the same value, so there's no
+// real conflict, though it's a bit awkward.
+#ifdef _WIN32
+# undef NO_ERROR
+#endif
+
+enum {
+ OK = 0, // Everything's swell.
+ NO_ERROR = 0, // No errors.
+
+ UNKNOWN_ERROR = 0x80000000,
+
+ NO_MEMORY = -ENOMEM,
+ INVALID_OPERATION = -ENOSYS,
+ BAD_VALUE = -EINVAL,
+ BAD_TYPE = 0x80000001,
+ NAME_NOT_FOUND = -ENOENT,
+ PERMISSION_DENIED = -EPERM,
+ NO_INIT = -ENODEV,
+ ALREADY_EXISTS = -EEXIST,
+ DEAD_OBJECT = -EPIPE,
+ FAILED_TRANSACTION = 0x80000002,
+ JPARKS_BROKE_IT = -EPIPE,
+#if !defined(HAVE_MS_C_RUNTIME)
+ BAD_INDEX = -EOVERFLOW,
+ NOT_ENOUGH_DATA = -ENODATA,
+ WOULD_BLOCK = -EWOULDBLOCK,
+ TIMED_OUT = -ETIMEDOUT,
+ UNKNOWN_TRANSACTION = -EBADMSG,
+#else
+ BAD_INDEX = -E2BIG,
+ NOT_ENOUGH_DATA = 0x80000003,
+ WOULD_BLOCK = 0x80000004,
+ TIMED_OUT = 0x80000005,
+ UNKNOWN_TRANSACTION = 0x80000006,
+#endif
+ FDS_NOT_ALLOWED = 0x80000007,
+};
+
+// Restore define; enumeration is in "android" namespace, so the value defined
+// there won't work for Win32 code in a different namespace.
+#ifdef _WIN32
+# define NO_ERROR 0L
+#endif
+
+}; // namespace android
+
+// ---------------------------------------------------------------------------
+
+#endif // ANDROID_ERRORS_H
diff --git a/sqlite-android/src/main/jni/sqlite/JNIHelp.cpp b/sqlite-android/src/main/jni/sqlite/JNIHelp.cpp
new file mode 100644
index 0000000000..c153429616
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/JNIHelp.cpp
@@ -0,0 +1,217 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "JNIHelp"
+
+#include "JNIHelp.h"
+#include "ALog-priv.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ * Equivalent to ScopedLocalRef, but for C_JNIEnv instead. (And slightly more powerful.)
+ */
+template
+class scoped_local_ref {
+public:
+ scoped_local_ref(C_JNIEnv* env, T localRef = NULL)
+ : mEnv(env), mLocalRef(localRef)
+ {
+ }
+
+ ~scoped_local_ref() {
+ reset();
+ }
+
+ void reset(T localRef = NULL) {
+ if (mLocalRef != NULL) {
+ (*mEnv)->DeleteLocalRef(reinterpret_cast(mEnv), mLocalRef);
+ mLocalRef = localRef;
+ }
+ }
+
+ T get() const {
+ return mLocalRef;
+ }
+
+private:
+ C_JNIEnv* mEnv;
+ T mLocalRef;
+
+ // Disallow copy and assignment.
+ scoped_local_ref(const scoped_local_ref&);
+ void operator=(const scoped_local_ref&);
+};
+
+static jclass findClass(C_JNIEnv* env, const char* className) {
+ JNIEnv* e = reinterpret_cast(env);
+ return (*env)->FindClass(e, className);
+}
+
+extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
+ const JNINativeMethod* gMethods, int numMethods)
+{
+ JNIEnv* e = reinterpret_cast(env);
+
+ ALOGV("Registering %s's %d native methods...", className, numMethods);
+
+ scoped_local_ref c(env, findClass(env, className));
+ if (c.get() == NULL) {
+ char* msg;
+ asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
+ e->FatalError(msg);
+ }
+
+ if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
+ char* msg;
+ asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
+ e->FatalError(msg);
+ }
+
+ return 0;
+}
+
+/*
+ * Returns a human-readable summary of an exception object. The buffer will
+ * be populated with the "binary" class name and, if present, the
+ * exception message.
+ */
+static bool logExceptionSummary(C_JNIEnv *env, jthrowable exception,
+ const char* exceptionClassName) {
+ JNIEnv* e = reinterpret_cast(env);
+
+ /* get the name of the exception's class */
+ scoped_local_ref exceptionClass(env, (*env)->GetObjectClass(e, exception)); // can't fail
+ scoped_local_ref classClass(env,
+ (*env)->GetObjectClass(e, exceptionClass.get())); // java.lang.Class, can't fail
+ jmethodID classGetNameMethod =
+ (*env)->GetMethodID(e, classClass.get(), "getName", "()Ljava/lang/String;");
+ scoped_local_ref classNameStr(env,
+ (jstring) (*env)->CallObjectMethod(e, exceptionClass.get(), classGetNameMethod));
+ if (classNameStr.get() == NULL) {
+ (*env)->ExceptionClear(e);
+ ALOGW("Discarding pending exception (%s) to throw %s", "",
+ exceptionClassName);
+ return false;
+ }
+ const char* classNameChars = (*env)->GetStringUTFChars(e, classNameStr.get(), NULL);
+ if (classNameChars == NULL) {
+ (*env)->ExceptionClear(e);
+ ALOGW("Discarding pending exception (%s) to throw %s", "",
+ exceptionClassName);
+ return false;
+ }
+ (*env)->ReleaseStringUTFChars(e, classNameStr.get(), classNameChars);
+
+ /* if the exception has a detail message, get that */
+ jmethodID getMessage =
+ (*env)->GetMethodID(e, exceptionClass.get(), "getMessage", "()Ljava/lang/String;");
+ scoped_local_ref messageStr(env,
+ (jstring) (*env)->CallObjectMethod(e, exception, getMessage));
+ if (messageStr.get() == NULL) {
+ return true;
+ }
+
+ const char* messageChars = (*env)->GetStringUTFChars(e, messageStr.get(), NULL);
+ if (messageChars != NULL) {
+ ALOGW("Discarding pending exception (%s: %s) to throw %s",
+ classNameChars,
+ messageChars,
+ exceptionClassName);
+ (*env)->ReleaseStringUTFChars(e, messageStr.get(), messageChars);
+ } else {
+ ALOGW("Discarding pending exception (%s: ) to throw %s",
+ classNameChars,
+ exceptionClassName);
+ (*env)->ExceptionClear(e); // clear OOM
+ }
+
+ return true;
+}
+
+extern "C" int jniThrowException(C_JNIEnv* env, const char* className, const char* msg) {
+ JNIEnv* e = reinterpret_cast(env);
+
+ if ((*env)->ExceptionCheck(e)) {
+ /* TODO: consider creating the new exception with this as "cause" */
+ scoped_local_ref exception(env, (*env)->ExceptionOccurred(e));
+ (*env)->ExceptionClear(e);
+
+ if (exception.get() != NULL) {
+ logExceptionSummary(env, exception.get(), className);
+ }
+ }
+
+ scoped_local_ref exceptionClass(env, findClass(env, className));
+ if (exceptionClass.get() == NULL) {
+ ALOGE("Unable to find exception class %s", className);
+ /* ClassNotFoundException now pending */
+ return -1;
+ }
+
+ if ((*env)->ThrowNew(e, exceptionClass.get(), msg) != JNI_OK) {
+ ALOGE("Failed throwing '%s' '%s'", className, msg);
+ /* an exception, most likely OOM, will now be pending */
+ return -1;
+ }
+
+ return 0;
+}
+
+int jniThrowExceptionFmt(C_JNIEnv* env, const char* className, const char* fmt, va_list args) {
+ char msgBuf[512];
+ vsnprintf(msgBuf, sizeof(msgBuf), fmt, args);
+ return jniThrowException(env, className, msgBuf);
+}
+
+int jniThrowNullPointerException(C_JNIEnv* env, const char* msg) {
+ return jniThrowException(env, "java/lang/NullPointerException", msg);
+}
+
+int jniThrowRuntimeException(C_JNIEnv* env, const char* msg) {
+ return jniThrowException(env, "java/lang/RuntimeException", msg);
+}
+
+int jniThrowIOException(C_JNIEnv* env, int errnum) {
+ char buffer[80];
+ const char* message = jniStrError(errnum, buffer, sizeof(buffer));
+ return jniThrowException(env, "java/io/IOException", message);
+}
+
+const char* jniStrError(int errnum, char* buf, size_t buflen) {
+#if __GLIBC__
+ // Note: glibc has a nonstandard strerror_r that returns char* rather than POSIX's int.
+ // char *strerror_r(int errnum, char *buf, size_t n);
+ return strerror_r(errnum, buf, buflen);
+#else
+ int rc = strerror_r(errnum, buf, buflen);
+ if (rc != 0) {
+ // (POSIX only guarantees a value other than 0. The safest
+ // way to implement this function is to use C++ and overload on the
+ // type of strerror_r to accurately distinguish GNU from POSIX.)
+ snprintf(buf, buflen, "errno %d", errnum);
+ }
+ return buf;
+#endif
+}
+
+void* operator new (size_t size) { return malloc(size); }
+void* operator new [] (size_t size) { return malloc(size); }
+void operator delete (void* pointer) { free(pointer); }
+void operator delete [] (void* pointer) { free(pointer); }
diff --git a/sqlite-android/src/main/jni/sqlite/JNIHelp.h b/sqlite-android/src/main/jni/sqlite/JNIHelp.h
new file mode 100644
index 0000000000..33a836fe83
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/JNIHelp.h
@@ -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.
+ */
+
+/*
+ * JNI helper functions.
+ *
+ * This file may be included by C or C++ code, which is trouble because jni.h
+ * uses different typedefs for JNIEnv in each language.
+ *
+ * TODO: remove C support.
+ */
+#ifndef NATIVEHELPER_JNIHELP_H_
+#define NATIVEHELPER_JNIHELP_H_
+
+#include "jni.h"
+#include
+
+#ifndef NELEM
+# define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * Register one or more native methods with a particular class.
+ * "className" looks like "java/lang/String". Aborts on failure.
+ * TODO: fix all callers and change the return type to void.
+ */
+int jniRegisterNativeMethods(C_JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods);
+
+/*
+ * Throw an exception with the specified class and an optional message.
+ *
+ * The "className" argument will be passed directly to FindClass, which
+ * takes strings with slashes (e.g. "java/lang/Object").
+ *
+ * If an exception is currently pending, we log a warning message and
+ * clear it.
+ *
+ * Returns 0 on success, nonzero if something failed (e.g. the exception
+ * class couldn't be found, so *an* exception will still be pending).
+ *
+ * Currently aborts the VM if it can't throw the exception.
+ */
+int jniThrowException(C_JNIEnv* env, const char* className, const char* msg);
+
+/*
+ * Throw a java.lang.NullPointerException, with an optional message.
+ */
+int jniThrowNullPointerException(C_JNIEnv* env, const char* msg);
+
+/*
+ * Throw a java.lang.RuntimeException, with an optional message.
+ */
+int jniThrowRuntimeException(C_JNIEnv* env, const char* msg);
+
+/*
+ * Throw a java.io.IOException, generating the message from errno.
+ */
+int jniThrowIOException(C_JNIEnv* env, int errnum);
+
+/*
+ * Return a pointer to a locale-dependent error string explaining errno
+ * value 'errnum'. The returned pointer may or may not be equal to 'buf'.
+ * This function is thread-safe (unlike strerror) and portable (unlike
+ * strerror_r).
+ */
+const char* jniStrError(int errnum, char* buf, size_t buflen);
+
+/*
+ * Returns a new java.io.FileDescriptor for the given int fd.
+ */
+jobject jniCreateFileDescriptor(C_JNIEnv* env, int fd);
+
+/*
+ * Returns the int fd from a java.io.FileDescriptor.
+ */
+int jniGetFDFromFileDescriptor(C_JNIEnv* env, jobject fileDescriptor);
+
+/*
+ * Sets the int fd in a java.io.FileDescriptor.
+ */
+void jniSetFileDescriptorOfFD(C_JNIEnv* env, jobject fileDescriptor, int value);
+
+/*
+ * Returns the reference from a java.lang.ref.Reference.
+ */
+jobject jniGetReferent(C_JNIEnv* env, jobject ref);
+
+#ifdef __cplusplus
+}
+#endif
+
+
+/*
+ * For C++ code, we provide inlines that map to the C functions. g++ always
+ * inlines these, even on non-optimized builds.
+ */
+#if defined(__cplusplus)
+inline int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods) {
+ return jniRegisterNativeMethods(&env->functions, className, gMethods, numMethods);
+}
+
+inline int jniThrowException(JNIEnv* env, const char* className, const char* msg) {
+ return jniThrowException(&env->functions, className, msg);
+}
+
+extern "C" int jniThrowExceptionFmt(C_JNIEnv* env, const char* className, const char* fmt, va_list args);
+
+/*
+ * Equivalent to jniThrowException but with a printf-like format string and
+ * variable-length argument list. This is only available in C++.
+ */
+inline int jniThrowExceptionFmt(JNIEnv* env, const char* className, const char* fmt, ...) {
+ va_list args;
+ va_start(args, fmt);
+ return jniThrowExceptionFmt(&env->functions, className, fmt, args);
+ va_end(args);
+}
+
+inline int jniThrowNullPointerException(JNIEnv* env, const char* msg) {
+ return jniThrowNullPointerException(&env->functions, msg);
+}
+
+inline int jniThrowRuntimeException(JNIEnv* env, const char* msg) {
+ return jniThrowRuntimeException(&env->functions, msg);
+}
+
+inline int jniThrowIOException(JNIEnv* env, int errnum) {
+ return jniThrowIOException(&env->functions, errnum);
+}
+
+inline jobject jniCreateFileDescriptor(JNIEnv* env, int fd) {
+ return jniCreateFileDescriptor(&env->functions, fd);
+}
+
+inline int jniGetFDFromFileDescriptor(JNIEnv* env, jobject fileDescriptor) {
+ return jniGetFDFromFileDescriptor(&env->functions, fileDescriptor);
+}
+
+inline void jniSetFileDescriptorOfFD(JNIEnv* env, jobject fileDescriptor, int value) {
+ jniSetFileDescriptorOfFD(&env->functions, fileDescriptor, value);
+}
+
+inline jobject jniGetReferent(JNIEnv* env, jobject ref) {
+ return jniGetReferent(&env->functions, ref);
+}
+
+#endif
+
+#define FIND_CLASS(var, className) \
+ var = env->FindClass(className); \
+ LOG_FATAL_IF(! var, "Unable to find class " className);
+
+#define GET_METHOD_ID(var, clazz, methodName, fieldDescriptor) \
+ var = env->GetMethodID(clazz, methodName, fieldDescriptor); \
+ LOG_FATAL_IF(! var, "Unable to find method" methodName);
+
+#define GET_FIELD_ID(var, clazz, fieldName, fieldDescriptor) \
+ var = env->GetFieldID(clazz, fieldName, fieldDescriptor); \
+ LOG_FATAL_IF(! var, "Unable to find field " fieldName);
+
+#endif /* NATIVEHELPER_JNIHELP_H_ */
diff --git a/sqlite-android/src/main/jni/sqlite/JNIString.cpp b/sqlite-android/src/main/jni/sqlite/JNIString.cpp
new file mode 100644
index 0000000000..ca758b1e96
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/JNIString.cpp
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Note this code is adapted from AOSP implementation of String, now located at
+// https://android.googlesource.com/platform/libcore/+/master/libart/src/main/java/java/lang/StringFactory.java
+
+#include
+
+#define REPLACEMENT_CHAR 0xfffd;
+
+namespace android {
+
+jsize utf8ToJavaCharArray(const char* d, jchar v[], jint byteCount) {
+ jint idx = 0;
+ jint last = byteCount;
+ jint s = 0;
+outer:
+ while (idx < last) {
+ jbyte b0 = d[idx++];
+ if ((b0 & 0x80) == 0) {
+ // 0xxxxxxx
+ // Range: U-00000000 - U-0000007F
+ jint val = b0 & 0xff;
+ v[s++] = (jchar) val;
+ } else if (((b0 & 0xe0) == 0xc0) || ((b0 & 0xf0) == 0xe0) ||
+ ((b0 & 0xf8) == 0xf0) || ((b0 & 0xfc) == 0xf8) || ((b0 & 0xfe) == 0xfc)) {
+ jint utfCount = 1;
+ if ((b0 & 0xf0) == 0xe0) utfCount = 2;
+ else if ((b0 & 0xf8) == 0xf0) utfCount = 3;
+ else if ((b0 & 0xfc) == 0xf8) utfCount = 4;
+ else if ((b0 & 0xfe) == 0xfc) utfCount = 5;
+
+ // 110xxxxx (10xxxxxx)+
+ // Range: U-00000080 - U-000007FF (count == 1)
+ // Range: U-00000800 - U-0000FFFF (count == 2)
+ // Range: U-00010000 - U-001FFFFF (count == 3)
+ // Range: U-00200000 - U-03FFFFFF (count == 4)
+ // Range: U-04000000 - U-7FFFFFFF (count == 5)
+
+ if (idx + utfCount > last) {
+ v[s++] = REPLACEMENT_CHAR;
+ continue;
+ }
+
+ // Extract usable bits from b0
+ jint val = b0 & (0x1f >> (utfCount - 1));
+ for (int i = 0; i < utfCount; ++i) {
+ jbyte b = d[idx++];
+ if ((b & 0xc0) != 0x80) {
+ v[s++] = REPLACEMENT_CHAR;
+ idx--; // Put the input char back
+ goto outer;
+ }
+ // Push new bits in from the right side
+ val <<= 6;
+ val |= b & 0x3f;
+ }
+
+ // Note: Java allows overlong char
+ // specifications To disallow, check that val
+ // is greater than or equal to the minimum
+ // value for each count:
+ //
+ // count min value
+ // ----- ----------
+ // 1 0x80
+ // 2 0x800
+ // 3 0x10000
+ // 4 0x200000
+ // 5 0x4000000
+
+ // Allow surrogate values (0xD800 - 0xDFFF) to
+ // be specified using 3-byte UTF values only
+ if ((utfCount != 2) && (val >= 0xD800) && (val <= 0xDFFF)) {
+ v[s++] = REPLACEMENT_CHAR;
+ continue;
+ }
+
+ // Reject chars greater than the Unicode maximum of U+10FFFF.
+ if (val > 0x10FFFF) {
+ v[s++] = REPLACEMENT_CHAR;
+ continue;
+ }
+
+ // Encode chars from U+10000 up as surrogate pairs
+ if (val < 0x10000) {
+ v[s++] = (jchar) val;
+ } else {
+ int x = val & 0xffff;
+ int u = (val >> 16) & 0x1f;
+ int w = (u - 1) & 0xffff;
+ int hi = 0xd800 | (w << 6) | (x >> 10);
+ int lo = 0xdc00 | (x & 0x3ff);
+ v[s++] = (jchar) hi;
+ v[s++] = (jchar) lo;
+ }
+ } else {
+ // Illegal values 0x8*, 0x9*, 0xa*, 0xb*, 0xfd-0xff
+ v[s++] = REPLACEMENT_CHAR;
+ }
+ }
+ return s;
+}
+}
\ No newline at end of file
diff --git a/sqlite-android/src/main/jni/sqlite/README b/sqlite-android/src/main/jni/sqlite/README
new file mode 100644
index 0000000000..da09588fda
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/README
@@ -0,0 +1,32 @@
+
+All the files in this directory are copied from stock android. The following
+files:
+
+JNIHelp.cpp
+ALog-priv.h
+
+are copied in from Android's libnativehelper module (altogether less than 1000
+lines of code). The remainder are from the core framework (directory
+/frameworks/base/core/jni).
+
+Notes on changes:
+
+The ashmem_XXX() interfaces are used for the various "xxxForBlobDescriptor()"
+API functions. The code in libcutils for this seems to be platform
+dependent - some platforms have kernel support, others have a user space
+implementation. So these functions are not supported for now.
+
+The original SQLiteConnection.cpp uses AndroidRuntime::genJNIEnv() to obtain a
+pointer to the current threads environment. Changed to store a pointer to the
+process JavaVM (Android allows only one) as a global variable. Then retrieve
+the JNIEnv as needed using GetEnv().
+
+Replaced uses of class String8 with std::string in SQLiteConnection.cpp and a
+few other places.
+
+The "LOCALIZED" collation and some miscellaneous user-functions added by the
+sqlite3_android.cpp module are not included. A collation called LOCALIZED
+that is equivalent to BINARY is added instead to keep various things working.
+This should not cause serious problems - class SQLiteConnection always
+runs "REINDEX LOCALIZED" immediately after opening a connection.
+
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_CursorWindow.cpp b/sqlite-android/src/main/jni/sqlite/android_database_CursorWindow.cpp
new file mode 100644
index 0000000000..cda6093037
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_CursorWindow.cpp
@@ -0,0 +1,412 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#undef LOG_TAG
+#define LOG_TAG "CursorWindow"
+#define __STDC_FORMAT_MACROS
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "CursorWindow.h"
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+static struct {
+ jfieldID data;
+ jfieldID sizeCopied;
+} gCharArrayBufferClassInfo;
+
+static jstring gEmptyString = NULL;
+
+static void throwExceptionWithRowCol(JNIEnv* env, jint row, jint column) {
+ char buf[64];
+ snprintf(buf, sizeof(buf), "Couldn't read row %d column %d", row, column);
+ jniThrowException(env, "java/lang/IllegalStateException", buf);
+}
+
+static void throwUnknownTypeException(JNIEnv * env, jint type) {
+ char buf[32];
+ snprintf(buf, sizeof(buf), "UNKNOWN type %d", type);
+ jniThrowException(env, "java/lang/IllegalStateException", buf);
+}
+
+static jlong nativeCreate(JNIEnv* env, jclass clazz, jstring nameObj, jint cursorWindowSize) {
+ CursorWindow* window;
+ const char* nameStr = env->GetStringUTFChars(nameObj, NULL);
+ status_t status = CursorWindow::create(nameStr, cursorWindowSize, &window);
+ env->ReleaseStringUTFChars(nameObj, nameStr);
+
+ if (status || !window) {
+ ALOGE("Could not allocate CursorWindow of size %d due to error %d.",
+ cursorWindowSize, status);
+ return 0;
+ }
+
+ LOG_WINDOW("nativeInitializeEmpty: window = %p", window);
+ return reinterpret_cast(window);
+}
+
+static void nativeDispose(JNIEnv* env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ if (window) {
+ LOG_WINDOW("Closing window %p", window);
+ delete window;
+ }
+}
+
+static jstring nativeGetName(JNIEnv* env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ return env->NewStringUTF(window->name());
+}
+
+static void nativeClear(JNIEnv * env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ LOG_WINDOW("Clearing window %p", window);
+ status_t status = window->clear();
+ if (status) {
+ LOG_WINDOW("Could not clear window. error=%d", status);
+ }
+}
+
+static jint nativeGetNumRows(JNIEnv* env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ return window->getNumRows();
+}
+
+static jboolean nativeSetNumColumns(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint columnNum) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ status_t status = window->setNumColumns(columnNum);
+ return status == OK;
+}
+
+static jboolean nativeAllocRow(JNIEnv* env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ status_t status = window->allocRow();
+ return status == OK;
+}
+
+static void nativeFreeLastRow(JNIEnv* env, jclass clazz, jlong windowPtr) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ window->freeLastRow();
+}
+
+static jint nativeGetType(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ LOG_WINDOW("returning column type affinity for %d,%d from %p", row, column, window);
+
+ CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
+ if (!fieldSlot) {
+ return CursorWindow::FIELD_TYPE_NULL;
+ }
+ return window->getFieldSlotType(fieldSlot);
+}
+
+static jbyteArray nativeGetBlob(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ //LOG_WINDOW("Getting blob for %d,%d from %p", row, column, window);
+
+ CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
+ if (!fieldSlot) {
+ throwExceptionWithRowCol(env, row, column);
+ return NULL;
+ }
+
+ int32_t type = window->getFieldSlotType(fieldSlot);
+ if (type == CursorWindow::FIELD_TYPE_BLOB || type == CursorWindow::FIELD_TYPE_STRING) {
+ size_t size;
+ const void* value = window->getFieldSlotValueBlob(fieldSlot, &size);
+ jbyteArray byteArray = env->NewByteArray(size);
+ if (!byteArray) {
+ env->ExceptionClear();
+ throw_sqlite3_exception(env, "Native could not create new byte[]");
+ return NULL;
+ }
+ env->SetByteArrayRegion(byteArray, 0, size, static_cast(value));
+ return byteArray;
+ } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
+ throw_sqlite3_exception(env, "INTEGER data in nativeGetBlob ");
+ } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
+ throw_sqlite3_exception(env, "FLOAT data in nativeGetBlob ");
+ } else if (type == CursorWindow::FIELD_TYPE_NULL) {
+ // do nothing
+ } else {
+ throwUnknownTypeException(env, type);
+ }
+ return NULL;
+}
+
+extern int utf8ToJavaCharArray(const char* d, jchar v[], jint byteCount);
+
+static jstring nativeGetString(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ //LOG_WINDOW("Getting string for %d,%d from %p", row, column, window);
+
+ CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
+ if (!fieldSlot) {
+ throwExceptionWithRowCol(env, row, column);
+ return NULL;
+ }
+
+ int32_t type = window->getFieldSlotType(fieldSlot);
+ if (type == CursorWindow::FIELD_TYPE_STRING) {
+ size_t sizeIncludingNull;
+ const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
+ if (sizeIncludingNull <= 1) {
+ return gEmptyString;
+ }
+ const size_t MaxStackStringSize = 65536; // max size for a stack char array
+ if (sizeIncludingNull > MaxStackStringSize) {
+ jchar* chars = new jchar[sizeIncludingNull - 1];
+ jint size = utf8ToJavaCharArray(value, chars, sizeIncludingNull - 1);
+ jstring string = env->NewString(chars, size);
+ delete[] chars;
+ return string;
+ } else {
+ jchar chars[sizeIncludingNull - 1];
+ jint size = utf8ToJavaCharArray(value, chars, sizeIncludingNull - 1);
+ return env->NewString(chars, size);
+ }
+ } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
+ int64_t value = window->getFieldSlotValueLong(fieldSlot);
+ char buf[32];
+ snprintf(buf, sizeof(buf), "%" PRId64, value);
+ return env->NewStringUTF(buf);
+ } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
+ double value = window->getFieldSlotValueDouble(fieldSlot);
+ char buf[32];
+ snprintf(buf, sizeof(buf), "%g", value);
+ return env->NewStringUTF(buf);
+ } else if (type == CursorWindow::FIELD_TYPE_NULL) {
+ return NULL;
+ } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
+ throw_sqlite3_exception(env, "Unable to convert BLOB to string");
+ return NULL;
+ } else {
+ throwUnknownTypeException(env, type);
+ return NULL;
+ }
+}
+
+static jlong nativeGetLong(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ //LOG_WINDOW("Getting long for %d,%d from %p", row, column, window);
+
+ CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
+ if (!fieldSlot) {
+ throwExceptionWithRowCol(env, row, column);
+ return 0;
+ }
+
+ int32_t type = window->getFieldSlotType(fieldSlot);
+ if (type == CursorWindow::FIELD_TYPE_INTEGER) {
+ return window->getFieldSlotValueLong(fieldSlot);
+ } else if (type == CursorWindow::FIELD_TYPE_STRING) {
+ size_t sizeIncludingNull;
+ const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
+ return sizeIncludingNull > 1 ? strtoll(value, NULL, 0) : 0L;
+ } else if (type == CursorWindow::FIELD_TYPE_FLOAT) {
+ return jlong(window->getFieldSlotValueDouble(fieldSlot));
+ } else if (type == CursorWindow::FIELD_TYPE_NULL) {
+ return 0;
+ } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
+ throw_sqlite3_exception(env, "Unable to convert BLOB to long");
+ return 0;
+ } else {
+ throwUnknownTypeException(env, type);
+ return 0;
+ }
+}
+
+static jdouble nativeGetDouble(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ //LOG_WINDOW("Getting double for %d,%d from %p", row, column, window);
+
+ CursorWindow::FieldSlot* fieldSlot = window->getFieldSlot(row, column);
+ if (!fieldSlot) {
+ throwExceptionWithRowCol(env, row, column);
+ return 0.0;
+ }
+
+ int32_t type = window->getFieldSlotType(fieldSlot);
+ if (type == CursorWindow::FIELD_TYPE_FLOAT) {
+ return window->getFieldSlotValueDouble(fieldSlot);
+ } else if (type == CursorWindow::FIELD_TYPE_STRING) {
+ size_t sizeIncludingNull;
+ const char* value = window->getFieldSlotValueString(fieldSlot, &sizeIncludingNull);
+ return sizeIncludingNull > 1 ? strtod(value, NULL) : 0.0;
+ } else if (type == CursorWindow::FIELD_TYPE_INTEGER) {
+ return jdouble(window->getFieldSlotValueLong(fieldSlot));
+ } else if (type == CursorWindow::FIELD_TYPE_NULL) {
+ return 0.0;
+ } else if (type == CursorWindow::FIELD_TYPE_BLOB) {
+ throw_sqlite3_exception(env, "Unable to convert BLOB to double");
+ return 0.0;
+ } else {
+ throwUnknownTypeException(env, type);
+ return 0.0;
+ }
+}
+
+static jboolean nativePutBlob(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jbyteArray valueObj, jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ jsize len = env->GetArrayLength(valueObj);
+
+ void* value = env->GetPrimitiveArrayCritical(valueObj, NULL);
+ status_t status = window->putBlob(row, column, value, len);
+ env->ReleasePrimitiveArrayCritical(valueObj, value, JNI_ABORT);
+
+ if (status) {
+ LOG_WINDOW("Failed to put blob. error=%d", status);
+ return false;
+ }
+
+ LOG_WINDOW("%d,%d is BLOB with %u bytes", row, column, len);
+ return true;
+}
+
+static jboolean nativePutString(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jstring valueObj, jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+
+ size_t sizeIncludingNull = env->GetStringUTFLength(valueObj) + 1;
+ const char* valueStr = env->GetStringUTFChars(valueObj, NULL);
+ if (!valueStr) {
+ LOG_WINDOW("value can't be transferred to UTFChars");
+ return false;
+ }
+ status_t status = window->putString(row, column, valueStr, sizeIncludingNull);
+ env->ReleaseStringUTFChars(valueObj, valueStr);
+
+ if (status) {
+ LOG_WINDOW("Failed to put string. error=%d", status);
+ return false;
+ }
+
+ LOG_WINDOW("%d,%d is TEXT with %u bytes", row, column, sizeIncludingNull);
+ return true;
+}
+
+static jboolean nativePutLong(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jlong value, jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ status_t status = window->putLong(row, column, value);
+
+ if (status) {
+ LOG_WINDOW("Failed to put long. error=%d", status);
+ return false;
+ }
+
+ LOG_WINDOW("%d,%d is INTEGER 0x%016llx", row, column, value);
+ return true;
+}
+
+static jboolean nativePutDouble(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jdouble value, jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ status_t status = window->putDouble(row, column, value);
+
+ if (status) {
+ LOG_WINDOW("Failed to put double. error=%d", status);
+ return false;
+ }
+
+ LOG_WINDOW("%d,%d is FLOAT %lf", row, column, value);
+ return true;
+}
+
+static jboolean nativePutNull(JNIEnv* env, jclass clazz, jlong windowPtr,
+ jint row, jint column) {
+ CursorWindow* window = reinterpret_cast(windowPtr);
+ status_t status = window->putNull(row, column);
+
+ if (status) {
+ LOG_WINDOW("Failed to put null. error=%d", status);
+ return false;
+ }
+
+ LOG_WINDOW("%d,%d is NULL", row, column);
+ return true;
+}
+
+static const JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeCreate", "(Ljava/lang/String;I)J",
+ (void*)nativeCreate },
+ { "nativeDispose", "(J)V",
+ (void*)nativeDispose },
+ { "nativeGetName", "(J)Ljava/lang/String;",
+ (void*)nativeGetName },
+ { "nativeClear", "(J)V",
+ (void*)nativeClear },
+ { "nativeGetNumRows", "(J)I",
+ (void*)nativeGetNumRows },
+ { "nativeSetNumColumns", "(JI)Z",
+ (void*)nativeSetNumColumns },
+ { "nativeAllocRow", "(J)Z",
+ (void*)nativeAllocRow },
+ { "nativeFreeLastRow", "(J)V",
+ (void*)nativeFreeLastRow },
+ { "nativeGetType", "(JII)I",
+ (void*)nativeGetType },
+ { "nativeGetBlob", "(JII)[B",
+ (void*)nativeGetBlob },
+ { "nativeGetString", "(JII)Ljava/lang/String;",
+ (void*)nativeGetString },
+ { "nativeGetLong", "(JII)J",
+ (void*)nativeGetLong },
+ { "nativeGetDouble", "(JII)D",
+ (void*)nativeGetDouble },
+ { "nativePutBlob", "(J[BII)Z",
+ (void*)nativePutBlob },
+ { "nativePutString", "(JLjava/lang/String;II)Z",
+ (void*)nativePutString },
+ { "nativePutLong", "(JJII)Z",
+ (void*)nativePutLong },
+ { "nativePutDouble", "(JDII)Z",
+ (void*)nativePutDouble },
+ { "nativePutNull", "(JII)Z",
+ (void*)nativePutNull },
+};
+
+int register_android_database_CursorWindow(JNIEnv* env)
+{
+ jclass clazz;
+ FIND_CLASS(clazz, "android/database/CharArrayBuffer");
+
+ GET_FIELD_ID(gCharArrayBufferClassInfo.data, clazz, "data", "[C");
+ GET_FIELD_ID(gCharArrayBufferClassInfo.sizeCopied, clazz, "sizeCopied", "I");
+
+ gEmptyString = static_cast(env->NewGlobalRef(env->NewStringUTF("")));
+ return jniRegisterNativeMethods(env,
+ "io/requery/android/database/CursorWindow", sMethods, NELEM(sMethods));
+}
+
+} // namespace android
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.cpp
new file mode 100644
index 0000000000..705343efb8
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.cpp
@@ -0,0 +1,140 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+/* throw a SQLiteException with a message appropriate for the error in handle */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle) {
+ throw_sqlite3_exception(env, handle, NULL);
+}
+
+/* throw a SQLiteException with the given message */
+void throw_sqlite3_exception(JNIEnv* env, const char* message) {
+ throw_sqlite3_exception(env, NULL, message);
+}
+
+/* throw a SQLiteException with a message appropriate for the error in handle
+ concatenated with the given message
+ */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message) {
+ if (handle) {
+ // get the error code and message from the SQLite connection
+ // the error message may contain more information than the error code
+ // because it is based on the extended error code rather than the simplified
+ // error code that SQLite normally returns.
+ throw_sqlite3_exception(env, sqlite3_extended_errcode(handle),
+ sqlite3_errmsg(handle), message);
+ } else {
+ // we use SQLITE_OK so that a generic SQLiteException is thrown;
+ // any code not specified in the switch statement below would do.
+ throw_sqlite3_exception(env, SQLITE_OK, "unknown error", message);
+ }
+}
+
+/* throw a SQLiteException for a given error code
+ * should only be used when the database connection is not available because the
+ * error information will not be quite as rich */
+void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message) {
+ throw_sqlite3_exception(env, errcode, "unknown error", message);
+}
+
+/* throw a SQLiteException for a given error code, sqlite3message, and
+ user message
+ */
+void throw_sqlite3_exception(JNIEnv* env, int errcode,
+ const char* sqlite3Message, const char* message) {
+ const char* exceptionClass;
+ switch (errcode & 0xff) { /* mask off extended error code */
+ case SQLITE_IOERR:
+ exceptionClass = "android/database/sqlite/SQLiteDiskIOException";
+ break;
+ case SQLITE_CORRUPT:
+ case SQLITE_NOTADB: // treat "unsupported file format" error as corruption also
+ exceptionClass = "android/database/sqlite/SQLiteDatabaseCorruptException";
+ break;
+ case SQLITE_CONSTRAINT:
+ exceptionClass = "android/database/sqlite/SQLiteConstraintException";
+ break;
+ case SQLITE_ABORT:
+ exceptionClass = "android/database/sqlite/SQLiteAbortException";
+ break;
+ case SQLITE_DONE:
+ exceptionClass = "android/database/sqlite/SQLiteDoneException";
+ sqlite3Message = NULL; // SQLite error message is irrelevant in this case
+ break;
+ case SQLITE_FULL:
+ exceptionClass = "android/database/sqlite/SQLiteFullException";
+ break;
+ case SQLITE_MISUSE:
+ exceptionClass = "android/database/sqlite/SQLiteMisuseException";
+ break;
+ case SQLITE_PERM:
+ exceptionClass = "android/database/sqlite/SQLiteAccessPermException";
+ break;
+ case SQLITE_BUSY:
+ exceptionClass = "android/database/sqlite/SQLiteDatabaseLockedException";
+ break;
+ case SQLITE_LOCKED:
+ exceptionClass = "android/database/sqlite/SQLiteTableLockedException";
+ break;
+ case SQLITE_READONLY:
+ exceptionClass = "android/database/sqlite/SQLiteReadOnlyDatabaseException";
+ break;
+ case SQLITE_CANTOPEN:
+ exceptionClass = "android/database/sqlite/SQLiteCantOpenDatabaseException";
+ break;
+ case SQLITE_TOOBIG:
+ exceptionClass = "android/database/sqlite/SQLiteBlobTooBigException";
+ break;
+ case SQLITE_RANGE:
+ exceptionClass = "android/database/sqlite/SQLiteBindOrColumnIndexOutOfRangeException";
+ break;
+ case SQLITE_NOMEM:
+ exceptionClass = "android/database/sqlite/SQLiteOutOfMemoryException";
+ break;
+ case SQLITE_MISMATCH:
+ exceptionClass = "android/database/sqlite/SQLiteDatatypeMismatchException";
+ break;
+ case SQLITE_INTERRUPT:
+ exceptionClass = "androidx/core/os/OperationCanceledException";
+ break;
+ default:
+ exceptionClass = "android/database/sqlite/SQLiteException";
+ break;
+ }
+
+ // check this exception class exists otherwise just default to SQLiteException
+ if (env->FindClass(exceptionClass) == NULL) {
+ exceptionClass = "android/database/sqlite/SQLiteException";
+ }
+
+ if (sqlite3Message) {
+ char *zFullmsg = sqlite3_mprintf(
+ "%s (code %d)%s%s", sqlite3Message, errcode,
+ (message ? ": " : ""), (message ? message : "")
+ );
+ jniThrowException(env, exceptionClass, zFullmsg);
+ sqlite3_free(zFullmsg);
+ } else {
+ jniThrowException(env, exceptionClass, message);
+ }
+}
+
+
+} // namespace android
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.h b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.h
new file mode 100644
index 0000000000..8c684dec49
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteCommon.h
@@ -0,0 +1,52 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#ifndef _ANDROID_DATABASE_SQLITE_COMMON_H
+#define _ANDROID_DATABASE_SQLITE_COMMON_H
+
+#include
+#include
+
+#include
+
+// Special log tags defined in SQLiteDebug.java.
+#define SQLITE_LOG_TAG "SQLiteLog"
+#define SQLITE_TRACE_TAG "SQLiteStatements"
+#define SQLITE_PROFILE_TAG "SQLiteTime"
+
+namespace android {
+
+/* throw a SQLiteException with a message appropriate for the error in handle */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle);
+
+/* throw a SQLiteException with the given message */
+void throw_sqlite3_exception(JNIEnv* env, const char* message);
+
+/* throw a SQLiteException with a message appropriate for the error in handle
+ concatenated with the given message
+ */
+void throw_sqlite3_exception(JNIEnv* env, sqlite3* handle, const char* message);
+
+/* throw a SQLiteException for a given error code */
+void throw_sqlite3_exception_errcode(JNIEnv* env, int errcode, const char* message);
+
+void throw_sqlite3_exception(JNIEnv* env, int errcode,
+ const char* sqlite3Message, const char* message);
+
+}
+
+#endif // _ANDROID_DATABASE_SQLITE_COMMON_H
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp
new file mode 100644
index 0000000000..bd7ff7b8e9
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteConnection.cpp
@@ -0,0 +1,1048 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#define LOG_TAG "SQLiteConnection"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "sqlite3.h"
+#include "JNIHelp.h"
+#include "ALog-priv.h"
+#include "android_database_SQLiteCommon.h"
+#include "CursorWindow.h"
+
+// Set to 1 to use UTF16 storage for localized indexes.
+#define UTF16_STORAGE 0
+
+namespace android {
+
+/* Busy timeout in milliseconds.
+ * If another connection (possibly in another process) has the database locked for
+ * longer than this amount of time then SQLite will generate a SQLITE_BUSY error.
+ * The SQLITE_BUSY error is then raised as a SQLiteDatabaseLockedException.
+ *
+ * In ordinary usage, busy timeouts are quite rare. Most databases only ever
+ * have a single open connection at a time unless they are using WAL. When using
+ * WAL, a timeout could occur if one connection is busy performing an auto-checkpoint
+ * operation. The busy timeout needs to be long enough to tolerate slow I/O write
+ * operations but not so long as to cause the application to hang indefinitely if
+ * there is a problem acquiring a database lock.
+ */
+static const int BUSY_TIMEOUT_MS = 2500;
+
+static JavaVM *gpJavaVM = 0;
+
+static struct {
+ jfieldID name;
+ jfieldID numArgs;
+ jmethodID dispatchCallback;
+} gSQLiteCustomFunctionClassInfo;
+
+static struct {
+ jfieldID name;
+ jfieldID numArgs;
+ jfieldID flags;
+ jmethodID dispatchCallback;
+} gSQLiteFunctionClassInfo;
+
+static struct {
+ jclass clazz;
+} gStringClassInfo;
+
+struct SQLiteConnection {
+ sqlite3* const db;
+ const int openFlags;
+ char* path;
+ char* label;
+
+ volatile bool canceled;
+
+ SQLiteConnection(sqlite3* db, int openFlags, const char* path_, const char* label_) :
+ db(db), openFlags(openFlags), canceled(false) {
+ path = strdup(path_);
+ label = strdup(label_);
+ }
+
+ ~SQLiteConnection() {
+ free(path);
+ free(label);
+ }
+};
+
+// Called each time a statement begins execution, when tracing is enabled.
+static void sqliteTraceCallback(void *data, const char *sql) {
+ SQLiteConnection* connection = static_cast(data);
+ ALOG(LOG_VERBOSE, SQLITE_TRACE_TAG, "%s: \"%s\"\n",
+ connection->label, sql);
+}
+
+// Called each time a statement finishes execution, when profiling is enabled.
+static void sqliteProfileCallback(void *data, const char *sql, sqlite3_uint64 tm) {
+ SQLiteConnection* connection = static_cast(data);
+ ALOG(LOG_VERBOSE, SQLITE_PROFILE_TAG, "%s: \"%s\" took %0.3f ms\n",
+ connection->label, sql, tm * 0.000001f);
+}
+
+// Called after each SQLite VM instruction when cancelation is enabled.
+static int sqliteProgressHandlerCallback(void* data) {
+ SQLiteConnection* connection = static_cast(data);
+ return connection->canceled;
+}
+
+/*
+** This function is a collation sequence callback equivalent to the built-in
+** BINARY sequence.
+**
+** Stock Android uses a modified version of sqlite3.c that calls out to a module
+** named "sqlite3_android" to add extra built-in collations and functions to
+** all database handles. Specifically, collation sequence "LOCALIZED". For now,
+** this module does not include sqlite3_android (since it is difficult to build
+** with the NDK only). Instead, this function is registered as "LOCALIZED" for all
+** new database handles.
+*/
+static int coll_localized(
+ void *not_used,
+ int nKey1, const void *pKey1,
+ int nKey2, const void *pKey2
+){
+ int rc, n;
+ n = nKey1GetStringUTFChars(pathStr, NULL);
+ const char* labelChars = env->GetStringUTFChars(labelStr, NULL);
+
+ sqlite3* db;
+ int err = sqlite3_open_v2(pathChars, &db, openFlags, NULL);
+ if (err != SQLITE_OK) {
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+ throw_sqlite3_exception_errcode(env, err, "Could not open database");
+ return 0;
+ }
+ err = sqlite3_create_collation(db, "localized", SQLITE_UTF8, 0, coll_localized);
+ if (err != SQLITE_OK) {
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+ throw_sqlite3_exception_errcode(env, err, "Could not register collation");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Check that the database is really read/write when that is what we asked for.
+ if ((openFlags & SQLITE_OPEN_READWRITE) && sqlite3_db_readonly(db, NULL)) {
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+ throw_sqlite3_exception(env, db, "Could not open the database in read/write mode.");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Set the default busy handler to retry automatically before returning SQLITE_BUSY.
+ err = sqlite3_busy_timeout(db, BUSY_TIMEOUT_MS);
+ if (err != SQLITE_OK) {
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+ throw_sqlite3_exception(env, db, "Could not set busy timeout");
+ sqlite3_close(db);
+ return 0;
+ }
+
+ // Register custom Android functions.
+#if 0
+ err = register_android_functions(db, UTF16_STORAGE);
+ if (err) {
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+ throw_sqlite3_exception(env, db, "Could not register Android SQL functions.");
+ sqlite3_close(db);
+ return 0;
+ }
+#endif
+
+ // Create wrapper object.
+ SQLiteConnection* connection = new SQLiteConnection(db, openFlags, pathChars, labelChars);
+ ALOGV("Opened connection %p with label '%s'", db, labelChars);
+ env->ReleaseStringUTFChars(pathStr, pathChars);
+ env->ReleaseStringUTFChars(labelStr, labelChars);
+
+ // Enable tracing and profiling if requested.
+ if (enableTrace) {
+ sqlite3_trace(db, &sqliteTraceCallback, connection);
+ }
+ if (enableProfile) {
+ sqlite3_profile(db, &sqliteProfileCallback, connection);
+ }
+
+ return reinterpret_cast(connection);
+}
+
+static void nativeClose(JNIEnv* env, jclass clazz, jlong connectionPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+
+ if (connection) {
+ ALOGV("Closing connection %p", connection->db);
+ int err = sqlite3_close(connection->db);
+ if (err != SQLITE_OK) {
+ // This can happen if sub-objects aren't closed first. Make sure the caller knows.
+ ALOGE("sqlite3_close(%p) failed: %d", connection->db, err);
+ throw_sqlite3_exception(env, connection->db, "Count not close db.");
+ return;
+ }
+
+ delete connection;
+ }
+}
+
+// Called each time a custom function is evaluated.
+static void sqliteCustomFunctionCallback(sqlite3_context *context,
+ int argc, sqlite3_value **argv) {
+
+ JNIEnv* env = 0;
+ gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4);
+
+ // Get the callback function object.
+ // Create a new local reference to it in case the callback tries to do something
+ // dumb like unregister the function (thereby destroying the global ref) while it is running.
+ jobject functionObjGlobal = reinterpret_cast(sqlite3_user_data(context));
+ jobject functionObj = env->NewLocalRef(functionObjGlobal);
+
+ jobjectArray argsArray = env->NewObjectArray(argc, gStringClassInfo.clazz, NULL);
+ if (argsArray) {
+ for (int i = 0; i < argc; i++) {
+ const jchar* arg = static_cast(sqlite3_value_text16(argv[i]));
+ if (!arg) {
+ ALOGW("NULL argument in custom_function_callback. This should not happen.");
+ } else {
+ size_t argLen = sqlite3_value_bytes16(argv[i]) / sizeof(jchar);
+ jstring argStr = env->NewString(arg, argLen);
+ if (!argStr) {
+ goto error; // out of memory error
+ }
+ env->SetObjectArrayElement(argsArray, i, argStr);
+ env->DeleteLocalRef(argStr);
+ }
+ }
+
+ {
+ jobject result = env->CallObjectMethod(functionObj,
+ gSQLiteCustomFunctionClassInfo.dispatchCallback, argsArray);
+ if (env->ExceptionCheck()) {
+ sqlite3_result_error(context, "Custom function exception", -1);
+ } else if (result == NULL) {
+ sqlite3_result_null(context);
+ } else {
+ jstring str = static_cast(result);
+ const char* chars = env->GetStringUTFChars(str, NULL);
+ sqlite3_result_text(context, chars, -1, SQLITE_TRANSIENT);
+ env->ReleaseStringUTFChars(str, chars);
+ }
+ env->DeleteLocalRef(result);
+ }
+error:
+ env->DeleteLocalRef(argsArray);
+ }
+
+ env->DeleteLocalRef(functionObj);
+
+ if (env->ExceptionCheck()) {
+ ALOGE("An exception was thrown by custom SQLite function.");
+ /* LOGE_EX(env); */
+ env->ExceptionClear();
+ }
+}
+
+// Called each time a Function is evaluated.
+static void sqliteFunctionCallback(sqlite3_context *context,
+ int argc, sqlite3_value **argv) {
+
+ JNIEnv* env = 0;
+ gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4);
+
+ // Get the callback function object.
+ // Create a new local reference to it in case the callback tries to do something
+ // dumb like unregister the function (thereby destroying the global ref) while it is running.
+ jobject functionObjGlobal = reinterpret_cast(sqlite3_user_data(context));
+ jobject functionObj = env->NewLocalRef(functionObjGlobal);
+
+ jlong contextPtr = jlong(context);
+ jlong argsPtr = jlong(argv);
+ jint argsCount = jint(argc);
+
+ env->CallVoidMethod(functionObj,
+ gSQLiteFunctionClassInfo.dispatchCallback,
+ contextPtr,
+ argsPtr,
+ argsCount
+ );
+ if (env->ExceptionCheck()) {
+ sqlite3_result_error(context, "Custom function exception", -1);
+ }
+
+ env->DeleteLocalRef(functionObj);
+
+ if (env->ExceptionCheck()) {
+ ALOGE("An exception was thrown by custom SQLite function.");
+ /* LOGE_EX(env); */
+ env->ExceptionClear();
+ }
+}
+
+// Called when a custom function is destroyed.
+static void sqliteCustomFunctionDestructor(void* data) {
+ jobject functionObjGlobal = reinterpret_cast(data);
+ JNIEnv* env = 0;
+ gpJavaVM->GetEnv((void**)&env, JNI_VERSION_1_4);
+ env->DeleteGlobalRef(functionObjGlobal);
+}
+
+static void nativeRegisterCustomFunction(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jobject functionObj) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+
+ jstring nameStr = jstring(env->GetObjectField(
+ functionObj, gSQLiteCustomFunctionClassInfo.name));
+ jint numArgs = env->GetIntField(functionObj, gSQLiteCustomFunctionClassInfo.numArgs);
+
+ jobject functionObjGlobal = env->NewGlobalRef(functionObj);
+
+ const char* name = env->GetStringUTFChars(nameStr, NULL);
+ int err = sqlite3_create_function_v2(connection->db, name, numArgs, SQLITE_UTF16,
+ reinterpret_cast(functionObjGlobal),
+ &sqliteCustomFunctionCallback, NULL, NULL, &sqliteCustomFunctionDestructor);
+ env->ReleaseStringUTFChars(nameStr, name);
+
+ if (err != SQLITE_OK) {
+ ALOGE("sqlite3_create_function returned %d", err);
+ env->DeleteGlobalRef(functionObjGlobal);
+ throw_sqlite3_exception(env, connection->db);
+ return;
+ }
+}
+
+static void nativeRegisterFunction(JNIEnv *env, jclass clazz, jlong connectionPtr,
+ jobject functionObj) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+
+ jstring nameStr = jstring(env->GetObjectField(
+ functionObj, gSQLiteFunctionClassInfo.name));
+ jint numArgs = env->GetIntField(functionObj, gSQLiteFunctionClassInfo.numArgs);
+ jint flags = env->GetIntField(functionObj, gSQLiteFunctionClassInfo.flags);
+
+ jobject functionObjGlobal = env->NewGlobalRef(functionObj);
+
+ const char* name = env->GetStringUTFChars(nameStr, NULL);
+ int err = sqlite3_create_function_v2(connection->db, name, numArgs,
+ SQLITE_UTF16 | flags,
+ reinterpret_cast(functionObjGlobal),
+ &sqliteFunctionCallback, NULL, NULL, &sqliteCustomFunctionDestructor);
+ env->ReleaseStringUTFChars(nameStr, name);
+
+ if (err != SQLITE_OK) {
+ ALOGE("sqlite3_create_function returned %d", err);
+ env->DeleteGlobalRef(functionObjGlobal);
+ throw_sqlite3_exception(env, connection->db);
+ return;
+ }
+}
+
+static void nativeRegisterLocalizedCollators(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jstring localeStr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+#if 0
+ const char* locale = env->GetStringUTFChars(localeStr, NULL);
+
+ int err = register_localized_collators(connection->db, locale, UTF16_STORAGE);
+ env->ReleaseStringUTFChars(localeStr, locale);
+
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db);
+ }
+#endif
+}
+
+static jlong nativePrepareStatement(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jstring sqlString) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+
+ jsize sqlLength = env->GetStringLength(sqlString);
+ const jchar* sql = env->GetStringCritical(sqlString, NULL);
+ sqlite3_stmt* statement;
+ int err = sqlite3_prepare16_v2(connection->db,
+ sql, sqlLength * sizeof(jchar), &statement, NULL);
+ env->ReleaseStringCritical(sqlString, sql);
+
+ if (err != SQLITE_OK) {
+ // Error messages like 'near ")": syntax error' are not
+ // always helpful enough, so construct an error string that
+ // includes the query itself.
+ const char *query = env->GetStringUTFChars(sqlString, NULL);
+ char *message = (char*) malloc(strlen(query) + 50);
+ if (message) {
+ strcpy(message, ", while compiling: "); // less than 50 chars
+ strcat(message, query);
+ }
+ env->ReleaseStringUTFChars(sqlString, query);
+ throw_sqlite3_exception(env, connection->db, message);
+ free(message);
+ return 0;
+ }
+
+ ALOGV("Prepared statement %p on connection %p", statement, connection->db);
+ return reinterpret_cast(statement);
+}
+
+static void nativeFinalizeStatement(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ // We ignore the result of sqlite3_finalize because it is really telling us about
+ // whether any errors occurred while executing the statement. The statement itself
+ // is always finalized regardless.
+ ALOGV("Finalized statement %p on connection %p", statement, connection->db);
+ sqlite3_finalize(statement);
+}
+
+static jint nativeGetParameterCount(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ return sqlite3_bind_parameter_count(statement);
+}
+
+static jboolean nativeIsReadOnly(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ return sqlite3_stmt_readonly(statement) != 0;
+}
+
+static jint nativeGetColumnCount(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ return sqlite3_column_count(statement);
+}
+
+static jstring nativeGetColumnName(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ const jchar* name = static_cast(sqlite3_column_name16(statement, index));
+ if (name) {
+ size_t length = 0;
+ while (name[length]) {
+ length += 1;
+ }
+ return env->NewString(name, length);
+ }
+ return NULL;
+}
+
+static void nativeBindNull(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = sqlite3_bind_null(statement, index);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindLong(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index, jlong value) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = sqlite3_bind_int64(statement, index, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindDouble(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index, jdouble value) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = sqlite3_bind_double(statement, index, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindString(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index, jstring valueString) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ jsize valueLength = env->GetStringLength(valueString);
+ const jchar* value = env->GetStringCritical(valueString, NULL);
+ int err = sqlite3_bind_text16(statement, index, value, valueLength * sizeof(jchar),
+ SQLITE_TRANSIENT);
+ env->ReleaseStringCritical(valueString, value);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeBindBlob(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr, jint index, jbyteArray valueArray) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ jsize valueLength = env->GetArrayLength(valueArray);
+ jbyte* value = static_cast(env->GetPrimitiveArrayCritical(valueArray, NULL));
+ int err = sqlite3_bind_blob(statement, index, value, valueLength, SQLITE_TRANSIENT);
+ env->ReleasePrimitiveArrayCritical(valueArray, value, JNI_ABORT);
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static void nativeResetStatementAndClearBindings(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = sqlite3_reset(statement);
+ if (err == SQLITE_OK) {
+ err = sqlite3_clear_bindings(statement);
+ }
+ if (err != SQLITE_OK) {
+ throw_sqlite3_exception(env, connection->db, NULL);
+ }
+}
+
+static int executeNonQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3_stmt* statement) {
+ int err = sqlite3_step(statement);
+ if (err == SQLITE_ROW) {
+ throw_sqlite3_exception(env,
+ "Queries can be performed using SQLiteDatabase query or rawQuery methods only.");
+ } else if (err != SQLITE_DONE) {
+ throw_sqlite3_exception(env, connection->db);
+ }
+ return err;
+}
+
+static void nativeExecute(JNIEnv* env, jclass clazz, jlong connectionPtr,
+ jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ executeNonQuery(env, connection, statement);
+}
+
+static jint nativeExecuteForChangedRowCount(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = executeNonQuery(env, connection, statement);
+ return err == SQLITE_DONE ? sqlite3_changes(connection->db) : -1;
+}
+
+static jlong nativeExecuteForLastInsertedRowId(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = executeNonQuery(env, connection, statement);
+ return err == SQLITE_DONE && sqlite3_changes(connection->db) > 0
+ ? sqlite3_last_insert_rowid(connection->db) : -1;
+}
+
+static int executeOneRowQuery(JNIEnv* env, SQLiteConnection* connection, sqlite3_stmt* statement) {
+ int err = sqlite3_step(statement);
+ if (err != SQLITE_ROW) {
+ throw_sqlite3_exception(env, connection->db);
+ }
+ return err;
+}
+
+static jlong nativeExecuteForLong(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ return sqlite3_column_int64(statement, 0);
+ }
+ return -1;
+}
+
+static jstring nativeExecuteForString(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ const jchar* text = static_cast(sqlite3_column_text16(statement, 0));
+ if (text) {
+ size_t length = sqlite3_column_bytes16(statement, 0) / sizeof(jchar);
+ return env->NewString(text, length);
+ }
+ }
+ return NULL;
+}
+
+static int createAshmemRegionWithData(JNIEnv* env, const void* data, size_t length) {
+#if 0
+ int error = 0;
+ int fd = ashmem_create_region(NULL, length);
+ if (fd < 0) {
+ error = errno;
+ ALOGE("ashmem_create_region failed: %s", strerror(error));
+ } else {
+ if (length > 0) {
+ void* ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ if (ptr == MAP_FAILED) {
+ error = errno;
+ ALOGE("mmap failed: %s", strerror(error));
+ } else {
+ memcpy(ptr, data, length);
+ munmap(ptr, length);
+ }
+ }
+
+ if (!error) {
+ if (ashmem_set_prot_region(fd, PROT_READ) < 0) {
+ error = errno;
+ ALOGE("ashmem_set_prot_region failed: %s", strerror(errno));
+ } else {
+ return fd;
+ }
+ }
+
+ close(fd);
+ }
+
+#endif
+ jniThrowIOException(env, -1);
+ return -1;
+}
+
+static jint nativeExecuteForBlobFileDescriptor(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+
+ int err = executeOneRowQuery(env, connection, statement);
+ if (err == SQLITE_ROW && sqlite3_column_count(statement) >= 1) {
+ const void* blob = sqlite3_column_blob(statement, 0);
+ if (blob) {
+ int length = sqlite3_column_bytes(statement, 0);
+ if (length >= 0) {
+ return createAshmemRegionWithData(env, blob, length);
+ }
+ }
+ }
+ return -1;
+}
+
+enum CopyRowResult {
+ CPR_OK,
+ CPR_FULL,
+ CPR_ERROR,
+};
+
+static CopyRowResult copyRow(JNIEnv* env, CursorWindow* window,
+ sqlite3_stmt* statement, int numColumns, int startPos, int addedRows) {
+ // Allocate a new field directory for the row.
+ status_t status = window->allocRow();
+ if (status) {
+ LOG_WINDOW("Failed allocating fieldDir at startPos %d row %d, error=%d",
+ startPos, addedRows, status);
+ return CPR_FULL;
+ }
+
+ // Pack the row into the window.
+ CopyRowResult result = CPR_OK;
+ for (int i = 0; i < numColumns; i++) {
+ int type = sqlite3_column_type(statement, i);
+ if (type == SQLITE_TEXT) {
+ // TEXT data
+ const char* text = reinterpret_cast(
+ sqlite3_column_text(statement, i));
+ // SQLite does not include the NULL terminator in size, but does
+ // ensure all strings are NULL terminated, so increase size by
+ // one to make sure we store the terminator.
+ size_t sizeIncludingNull = sqlite3_column_bytes(statement, i) + 1;
+ status = window->putString(addedRows, i, text, sizeIncludingNull);
+ if (status) {
+ LOG_WINDOW("Failed allocating %u bytes for text at %d,%d, error=%d",
+ sizeIncludingNull, startPos + addedRows, i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is TEXT with %u bytes",
+ startPos + addedRows, i, sizeIncludingNull);
+ } else if (type == SQLITE_INTEGER) {
+ // INTEGER data
+ int64_t value = sqlite3_column_int64(statement, i);
+ status = window->putLong(addedRows, i, value);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a long in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is INTEGER 0x%016llx", startPos + addedRows, i, value);
+ } else if (type == SQLITE_FLOAT) {
+ // FLOAT data
+ double value = sqlite3_column_double(statement, i);
+ status = window->putDouble(addedRows, i, value);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a double in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is FLOAT %lf", startPos + addedRows, i, value);
+ } else if (type == SQLITE_BLOB) {
+ // BLOB data
+ const void* blob = sqlite3_column_blob(statement, i);
+ size_t size = sqlite3_column_bytes(statement, i);
+ status = window->putBlob(addedRows, i, blob, size);
+ if (status) {
+ LOG_WINDOW("Failed allocating %u bytes for blob at %d,%d, error=%d",
+ size, startPos + addedRows, i, status);
+ result = CPR_FULL;
+ break;
+ }
+ LOG_WINDOW("%d,%d is Blob with %u bytes",
+ startPos + addedRows, i, size);
+ } else if (type == SQLITE_NULL) {
+ // NULL field
+ status = window->putNull(addedRows, i);
+ if (status) {
+ LOG_WINDOW("Failed allocating space for a null in column %d, error=%d",
+ i, status);
+ result = CPR_FULL;
+ break;
+ }
+
+ LOG_WINDOW("%d,%d is NULL", startPos + addedRows, i);
+ } else {
+ // Unknown data
+ ALOGE("Unknown column type when filling database window");
+ throw_sqlite3_exception(env, "Unknown column type when filling window");
+ result = CPR_ERROR;
+ break;
+ }
+ }
+
+ // Free the last row if if was not successfully copied.
+ if (result != CPR_OK) {
+ window->freeLastRow();
+ }
+ return result;
+}
+
+static jlong nativeExecuteForCursorWindow(JNIEnv* env, jclass clazz,
+ jlong connectionPtr, jlong statementPtr, jlong windowPtr,
+ jint startPos, jint requiredPos, jboolean countAllRows) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ sqlite3_stmt* statement = reinterpret_cast(statementPtr);
+ CursorWindow* window = reinterpret_cast(windowPtr);
+
+ status_t status = window->clear();
+ if (status) {
+ throw_sqlite3_exception(env, connection->db, "Failed to clear the cursor window");
+ return 0;
+ }
+
+ int numColumns = sqlite3_column_count(statement);
+ status = window->setNumColumns(numColumns);
+ if (status) {
+ throw_sqlite3_exception(env, connection->db, "Failed to set the cursor window column count");
+ return 0;
+ }
+
+ int retryCount = 0;
+ int totalRows = 0;
+ int addedRows = 0;
+ bool windowFull = false;
+ bool gotException = false;
+ while (!gotException && (!windowFull || countAllRows)) {
+ int err = sqlite3_step(statement);
+ if (err == SQLITE_ROW) {
+ LOG_WINDOW("Stepped statement %p to row %d", statement, totalRows);
+ retryCount = 0;
+ totalRows += 1;
+
+ // Skip the row if the window is full or we haven't reached the start position yet.
+ if (startPos >= totalRows || windowFull) {
+ continue;
+ }
+
+ CopyRowResult cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
+ if (cpr == CPR_FULL && addedRows && startPos + addedRows <= requiredPos) {
+ // We filled the window before we got to the one row that we really wanted.
+ // Clear the window and start filling it again from here.
+ // TODO: Would be nicer if we could progressively replace earlier rows.
+ window->clear();
+ window->setNumColumns(numColumns);
+ startPos += addedRows;
+ addedRows = 0;
+ cpr = copyRow(env, window, statement, numColumns, startPos, addedRows);
+ }
+
+ if (cpr == CPR_OK) {
+ addedRows += 1;
+ } else if (cpr == CPR_FULL) {
+ windowFull = true;
+ } else {
+ gotException = true;
+ }
+ } else if (err == SQLITE_DONE) {
+ // All rows processed, bail
+ LOG_WINDOW("Processed all rows");
+ break;
+ } else if (err == SQLITE_LOCKED || err == SQLITE_BUSY) {
+ // The table is locked, retry
+ LOG_WINDOW("Database locked, retrying");
+ if (retryCount > 50) {
+ ALOGE("Bailing on database busy retry");
+ throw_sqlite3_exception(env, connection->db, "retrycount exceeded");
+ gotException = true;
+ } else {
+ // Sleep to give the thread holding the lock a chance to finish
+ usleep(1000);
+ retryCount++;
+ }
+ } else {
+ throw_sqlite3_exception(env, connection->db);
+ gotException = true;
+ }
+ }
+
+ LOG_WINDOW("Resetting statement %p after fetching %d rows and adding %d rows"
+ "to the window in %d bytes",
+ statement, totalRows, addedRows, window->size() - window->freeSpace());
+ sqlite3_reset(statement);
+
+ // Report the total number of rows on request.
+ if (startPos > totalRows) {
+ ALOGE("startPos %d > actual rows %d", startPos, totalRows);
+ }
+ jlong result = jlong(startPos) << 32 | jlong(totalRows);
+ return result;
+}
+
+static jint nativeGetDbLookaside(JNIEnv* env, jobject clazz, jlong connectionPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+
+ int cur = -1;
+ int unused;
+ sqlite3_db_status(connection->db, SQLITE_DBSTATUS_LOOKASIDE_USED, &cur, &unused, 0);
+ return cur;
+}
+
+static void nativeCancel(JNIEnv* env, jobject clazz, jlong connectionPtr) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ connection->canceled = true;
+}
+
+static void nativeResetCancel(JNIEnv* env, jobject clazz, jlong connectionPtr,
+ jboolean cancelable) {
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ connection->canceled = false;
+
+ if (cancelable) {
+ sqlite3_progress_handler(connection->db, 4, sqliteProgressHandlerCallback,
+ connection);
+ } else {
+ sqlite3_progress_handler(connection->db, 0, NULL, NULL);
+ }
+}
+
+static jboolean nativeHasCodec(JNIEnv* env, jobject clazz){
+#ifdef SQLITE_HAS_CODEC
+ return true;
+#else
+ return false;
+#endif
+}
+
+static void nativeLoadExtension(JNIEnv* env, jobject clazz,
+ jlong connectionPtr, jstring file, jstring proc) {
+ char* errorMessage;
+
+ SQLiteConnection* connection = reinterpret_cast(connectionPtr);
+ int result = sqlite3_enable_load_extension(connection->db, 1);
+ if (result == SQLITE_OK) {
+ const char* fileChars = env->GetStringUTFChars(file, NULL);
+ const char* procChars = NULL;
+ if (proc) {
+ procChars = env->GetStringUTFChars(proc, NULL);
+ }
+ result = sqlite3_load_extension(connection->db, fileChars, procChars, &errorMessage);
+ env->ReleaseStringUTFChars(file, fileChars);
+ if (proc) {
+ env->ReleaseStringUTFChars(proc, procChars);
+ }
+ }
+ if (result != SQLITE_OK) {
+ char* formattedError = sqlite3_mprintf("Could not register extension: %s", errorMessage);
+ sqlite3_free(errorMessage);
+
+ throw_sqlite3_exception_errcode(env, result, formattedError);
+ sqlite3_free(formattedError);
+ }
+}
+
+static JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeOpen", "(Ljava/lang/String;ILjava/lang/String;ZZ)J",
+ (void*)nativeOpen },
+ { "nativeClose", "(J)V",
+ (void*)nativeClose },
+ { "nativeRegisterCustomFunction", "(JLio/requery/android/database/sqlite/SQLiteCustomFunction;)V",
+ (void*)nativeRegisterCustomFunction },
+ { "nativeRegisterFunction", "(JLio/requery/android/database/sqlite/SQLiteFunction;)V",
+ (void*)nativeRegisterFunction },
+ { "nativeRegisterLocalizedCollators", "(JLjava/lang/String;)V",
+ (void*)nativeRegisterLocalizedCollators },
+ { "nativePrepareStatement", "(JLjava/lang/String;)J",
+ (void*)nativePrepareStatement },
+ { "nativeFinalizeStatement", "(JJ)V",
+ (void*)nativeFinalizeStatement },
+ { "nativeGetParameterCount", "(JJ)I",
+ (void*)nativeGetParameterCount },
+ { "nativeIsReadOnly", "(JJ)Z",
+ (void*)nativeIsReadOnly },
+ { "nativeGetColumnCount", "(JJ)I",
+ (void*)nativeGetColumnCount },
+ { "nativeGetColumnName", "(JJI)Ljava/lang/String;",
+ (void*)nativeGetColumnName },
+ { "nativeBindNull", "(JJI)V",
+ (void*)nativeBindNull },
+ { "nativeBindLong", "(JJIJ)V",
+ (void*)nativeBindLong },
+ { "nativeBindDouble", "(JJID)V",
+ (void*)nativeBindDouble },
+ { "nativeBindString", "(JJILjava/lang/String;)V",
+ (void*)nativeBindString },
+ { "nativeBindBlob", "(JJI[B)V",
+ (void*)nativeBindBlob },
+ { "nativeResetStatementAndClearBindings", "(JJ)V",
+ (void*)nativeResetStatementAndClearBindings },
+ { "nativeExecute", "(JJ)V",
+ (void*)nativeExecute },
+ { "nativeExecuteForLong", "(JJ)J",
+ (void*)nativeExecuteForLong },
+ { "nativeExecuteForString", "(JJ)Ljava/lang/String;",
+ (void*)nativeExecuteForString },
+ { "nativeExecuteForBlobFileDescriptor", "(JJ)I",
+ (void*)nativeExecuteForBlobFileDescriptor },
+ { "nativeExecuteForChangedRowCount", "(JJ)I",
+ (void*)nativeExecuteForChangedRowCount },
+ { "nativeExecuteForLastInsertedRowId", "(JJ)J",
+ (void*)nativeExecuteForLastInsertedRowId },
+ { "nativeExecuteForCursorWindow", "(JJJIIZ)J",
+ (void*)nativeExecuteForCursorWindow },
+ { "nativeGetDbLookaside", "(J)I",
+ (void*)nativeGetDbLookaside },
+ { "nativeCancel", "(J)V",
+ (void*)nativeCancel },
+ { "nativeResetCancel", "(JZ)V",
+ (void*)nativeResetCancel },
+ { "nativeHasCodec", "()Z",
+ (void*)nativeHasCodec },
+ { "nativeLoadExtension", "(JLjava/lang/String;Ljava/lang/String;)V",
+ (void*)nativeLoadExtension },
+};
+
+int register_android_database_SQLiteConnection(JNIEnv *env)
+{
+ jclass clazz;
+ FIND_CLASS(clazz, "io/requery/android/database/sqlite/SQLiteCustomFunction");
+
+ GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.name, clazz,
+ "name", "Ljava/lang/String;");
+ GET_FIELD_ID(gSQLiteCustomFunctionClassInfo.numArgs, clazz,
+ "numArgs", "I");
+ GET_METHOD_ID(gSQLiteCustomFunctionClassInfo.dispatchCallback,
+ clazz, "dispatchCallback", "([Ljava/lang/String;)Ljava/lang/String;");
+
+ FIND_CLASS(clazz, "io/requery/android/database/sqlite/SQLiteFunction");
+
+ GET_FIELD_ID(gSQLiteFunctionClassInfo.name, clazz,
+ "name", "Ljava/lang/String;");
+ GET_FIELD_ID(gSQLiteFunctionClassInfo.numArgs, clazz,
+ "numArgs", "I");
+ GET_FIELD_ID(gSQLiteFunctionClassInfo.flags, clazz,
+ "flags", "I");
+ GET_METHOD_ID(gSQLiteFunctionClassInfo.dispatchCallback,
+ clazz, "dispatchCallback", "(JJI)V");
+
+ FIND_CLASS(clazz, "java/lang/String");
+ gStringClassInfo.clazz = jclass(env->NewGlobalRef(clazz));
+
+ return jniRegisterNativeMethods(env,
+ "io/requery/android/database/sqlite/SQLiteConnection",
+ sMethods, NELEM(sMethods)
+ );
+}
+
+extern int register_android_database_SQLiteGlobal(JNIEnv *env);
+extern int register_android_database_SQLiteDebug(JNIEnv *env);
+extern int register_android_database_SQLiteFunction(JNIEnv *env);
+extern int register_android_database_CursorWindow(JNIEnv *env);
+
+} // namespace android
+
+extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
+ JNIEnv *env = 0;
+
+ android::gpJavaVM = vm;
+ vm->GetEnv((void**)&env, JNI_VERSION_1_4);
+
+ android::register_android_database_SQLiteConnection(env);
+ android::register_android_database_SQLiteDebug(env);
+ android::register_android_database_SQLiteGlobal(env);
+ android::register_android_database_CursorWindow(env);
+ android::register_android_database_SQLiteFunction(env);
+
+ return JNI_VERSION_1_4;
+}
+
+
+
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteDebug.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteDebug.cpp
new file mode 100644
index 0000000000..371b321f02
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteDebug.cpp
@@ -0,0 +1,81 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#define LOG_TAG "SQLiteDebug"
+
+#include
+#include "JNIHelp.h"
+#include "ALog-priv.h"
+
+#include
+#include
+#include
+#include
+
+#include
+
+namespace android {
+
+static struct {
+ jfieldID memoryUsed;
+ jfieldID pageCacheOverflow;
+ jfieldID largestMemAlloc;
+} gSQLiteDebugPagerStatsClassInfo;
+
+static void nativeGetPagerStats(JNIEnv *env, jobject clazz, jobject statsObj)
+{
+ int memoryUsed;
+ int pageCacheOverflow;
+ int largestMemAlloc;
+ int unused;
+
+ sqlite3_status(SQLITE_STATUS_MEMORY_USED, &memoryUsed, &unused, 0);
+ sqlite3_status(SQLITE_STATUS_MALLOC_SIZE, &unused, &largestMemAlloc, 0);
+ sqlite3_status(SQLITE_STATUS_PAGECACHE_OVERFLOW, &pageCacheOverflow, &unused, 0);
+ env->SetIntField(statsObj, gSQLiteDebugPagerStatsClassInfo.memoryUsed, memoryUsed);
+ env->SetIntField(statsObj, gSQLiteDebugPagerStatsClassInfo.pageCacheOverflow,
+ pageCacheOverflow);
+ env->SetIntField(statsObj, gSQLiteDebugPagerStatsClassInfo.largestMemAlloc, largestMemAlloc);
+}
+
+/*
+ * JNI registration.
+ */
+
+static JNINativeMethod gMethods[] =
+{
+ { "nativeGetPagerStats", "(Lio/requery/android/database/sqlite/SQLiteDebug$PagerStats;)V",
+ (void*) nativeGetPagerStats },
+};
+
+int register_android_database_SQLiteDebug(JNIEnv *env)
+{
+ jclass clazz;
+ FIND_CLASS(clazz, "io/requery/android/database/sqlite/SQLiteDebug$PagerStats");
+
+ GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.memoryUsed, clazz,
+ "memoryUsed", "I");
+ GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.largestMemAlloc, clazz,
+ "largestMemAlloc", "I");
+ GET_FIELD_ID(gSQLiteDebugPagerStatsClassInfo.pageCacheOverflow, clazz,
+ "pageCacheOverflow", "I");
+
+ return jniRegisterNativeMethods(env, "io/requery/android/database/sqlite/SQLiteDebug",
+ gMethods, NELEM(gMethods));
+}
+
+} // namespace android
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteFunction.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteFunction.cpp
new file mode 100644
index 0000000000..f376593dda
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteFunction.cpp
@@ -0,0 +1,229 @@
+#define LOG_TAG "SQLiteFunction"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "sqlite3.h"
+#include "JNIHelp.h"
+#include "ALog-priv.h"
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+/* Returns the sqlite3_value for the given arg of the given function.
+ * If 0 is returned, an exception has been thrown to report the reason. */
+static sqlite3_value *tovalue(JNIEnv *env, jlong argsPtr, jint arg) {
+ if (arg < 0) {
+ throw_sqlite3_exception(env, "Invalid arg index");
+ return 0;
+ }
+ if (!argsPtr) {
+ throw_sqlite3_exception(env, "Invalid argsPtr");
+ return 0;
+ }
+
+ sqlite3_value **args = reinterpret_cast(argsPtr);
+ return args[arg];
+}
+
+static sqlite3_context *tocontext(JNIEnv *env, jlong contextPtr) {
+ if (!contextPtr) {
+ throw_sqlite3_exception(env, "Invalid contextPtr");
+ return 0;
+ }
+
+ return reinterpret_cast(contextPtr);
+}
+
+/*
+ * Getters
+ */
+
+static jbyteArray nativeGetArgBlob(JNIEnv* env, jclass clazz, jlong argsPtr,
+ jint arg) {
+ int length;
+ jbyteArray byteArray;
+ const void *blob;
+
+ sqlite3_value *value = tovalue(env, argsPtr, arg);
+ if (!value) return NULL;
+
+ blob = sqlite3_value_blob(value);
+ if (!blob) return NULL;
+
+ length = sqlite3_value_bytes(value);
+ byteArray = env->NewByteArray(length);
+ if (!byteArray) {
+ env->ExceptionClear();
+ throw_sqlite3_exception(env, "Native could not create new byte[]");
+ return NULL;
+ }
+
+ env->SetByteArrayRegion(byteArray, 0, length, static_cast(blob));
+ return byteArray;
+}
+
+static jstring nativeGetArgString(JNIEnv* env, jclass clazz, jlong argsPtr,
+ jint arg) {
+ sqlite3_value *value = tovalue(env, argsPtr, arg);
+ if (!value) return NULL;
+
+ const jchar* chars = static_cast(sqlite3_value_text16(value));
+ if (!chars) return NULL;
+
+ size_t len = sqlite3_value_bytes16(value) / sizeof(jchar);
+ jstring str = env->NewString(chars, len);
+ if (!str) {
+ env->ExceptionClear();
+ throw_sqlite3_exception(env, "Native could not allocate string");
+ return NULL;
+ }
+
+ return str;
+}
+
+static jlong nativeGetArgLong(JNIEnv* env, jclass clazz, jlong argsPtr,
+ jint arg) {
+ sqlite3_value *value = tovalue(env, argsPtr, arg);
+ return value ? sqlite3_value_int64(value) : 0;
+}
+
+static jdouble nativeGetArgDouble(JNIEnv* env, jclass clazz, jlong argsPtr,
+ jint arg) {
+ sqlite3_value *value = tovalue(env, argsPtr, arg);
+ return value ? sqlite3_value_double(value) : 0;
+}
+
+static jint nativeGetArgInt(JNIEnv* env, jclass clazz, jlong argsPtr,
+ jint arg) {
+ sqlite3_value *value = tovalue(env, argsPtr, arg);
+ return value ? sqlite3_value_int(value) : 0;
+}
+
+/*
+ * Setters
+ */
+
+static void nativeSetResultBlob(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jbyteArray result) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (!context) return;
+ if (result == NULL) {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ jsize len = env->GetArrayLength(result);
+ void *bytes = env->GetPrimitiveArrayCritical(result, NULL);
+ if (!bytes) {
+ env->ExceptionClear();
+ throw_sqlite3_exception(env, "Out of memory accepting blob");
+ return;
+ }
+
+ sqlite3_result_blob(context, bytes, len, SQLITE_TRANSIENT);
+ env->ReleasePrimitiveArrayCritical(result, bytes, JNI_ABORT);
+}
+
+static void nativeSetResultString(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jstring result) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (result == NULL) {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ const char* chars = env->GetStringUTFChars(result, NULL);
+ if (!chars) {
+ ALOGE("result value can't be transferred to UTFChars");
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+
+ sqlite3_result_text(context, chars, -1, SQLITE_TRANSIENT);
+ env->ReleaseStringUTFChars(result, chars);
+}
+
+static void nativeSetResultLong(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jlong result) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (context) sqlite3_result_int64(context, result);
+}
+
+static void nativeSetResultDouble(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jdouble result) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (context) sqlite3_result_double(context, result);
+}
+
+static void nativeSetResultInt(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jint result) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (context) sqlite3_result_int(context, result);
+}
+
+static void nativeSetResultError(JNIEnv* env, jclass clazz,
+ jlong contextPtr, jstring error) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (error == NULL) {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ const char* chars = env->GetStringUTFChars(error, NULL);
+ if (!chars) {
+ ALOGE("result value can't be transferred to UTFChars");
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+
+ sqlite3_result_error(context, chars, -1);
+ env->ReleaseStringUTFChars(error, chars);
+}
+
+static void nativeSetResultNull(JNIEnv* env, jclass clazz, jlong contextPtr) {
+ sqlite3_context *context = tocontext(env, contextPtr);
+ if (context) sqlite3_result_null(context);
+}
+
+
+static const JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeGetArgBlob", "(JI)[B",
+ (void*)nativeGetArgBlob },
+ { "nativeGetArgString", "(JI)Ljava/lang/String;",
+ (void*)nativeGetArgString },
+ { "nativeGetArgLong", "(JI)J",
+ (void*)nativeGetArgLong },
+ { "nativeGetArgDouble", "(JI)D",
+ (void*)nativeGetArgDouble },
+ { "nativeGetArgInt", "(JI)I",
+ (void*)nativeGetArgInt },
+
+ { "nativeSetResultBlob", "(J[B)V",
+ (void*)nativeSetResultBlob },
+ { "nativeSetResultString", "(JLjava/lang/String;)V",
+ (void*)nativeSetResultString },
+ { "nativeSetResultLong", "(JJ)V",
+ (void*)nativeSetResultLong },
+ { "nativeSetResultDouble", "(JD)V",
+ (void*)nativeSetResultDouble },
+ { "nativeSetResultInt", "(JI)V",
+ (void*)nativeSetResultInt },
+ { "nativeSetResultError", "(JLjava/lang/String;)V",
+ (void*)nativeSetResultError },
+ { "nativeSetResultNull", "(J)V",
+ (void*)nativeSetResultNull },
+};
+
+int register_android_database_SQLiteFunction(JNIEnv* env)
+{
+ return jniRegisterNativeMethods(env,
+ "io/requery/android/database/sqlite/SQLiteFunction", sMethods, NELEM(sMethods));
+}
+
+} // namespace android
diff --git a/sqlite-android/src/main/jni/sqlite/android_database_SQLiteGlobal.cpp b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteGlobal.cpp
new file mode 100644
index 0000000000..a07b48e272
--- /dev/null
+++ b/sqlite-android/src/main/jni/sqlite/android_database_SQLiteGlobal.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 from original source see README at the top level of this project
+
+#define LOG_TAG "SQLiteGlobal"
+
+#include
+#include
+#include "ALog-priv.h"
+
+#include
+//#include
+
+#include "android_database_SQLiteCommon.h"
+
+namespace android {
+
+// Limit heap to 8MB for now. This is 4 times the maximum cursor window
+// size, as has been used by the original code in SQLiteDatabase for
+// a long time.
+static const int SOFT_HEAP_LIMIT = 8 * 1024 * 1024;
+
+
+// Called each time a message is logged.
+static void sqliteLogCallback(void* data, int iErrCode, const char* zMsg) {
+ bool verboseLog = !!data;
+ if (iErrCode == 0 || iErrCode == SQLITE_CONSTRAINT || iErrCode == SQLITE_SCHEMA) {
+ if (verboseLog) {
+ ALOG(LOG_VERBOSE, SQLITE_LOG_TAG, "(%d) %s\n", iErrCode, zMsg);
+ }
+ } else {
+ ALOG(LOG_ERROR, SQLITE_LOG_TAG, "(%d) %s\n", iErrCode, zMsg);
+ }
+}
+
+// Sets the global SQLite configuration.
+// This must be called before any other SQLite functions are called.
+static void sqliteInitialize() {
+ // Enable multi-threaded mode. In this mode, SQLite is safe to use by multiple
+ // threads as long as no two threads use the same database connection at the same
+ // time (which we guarantee in the SQLite database wrappers).
+ sqlite3_config(SQLITE_CONFIG_MULTITHREAD);
+
+ // Redirect SQLite log messages to the Android log.
+#if 0
+ bool verboseLog = android_util_Log_isVerboseLogEnabled(SQLITE_LOG_TAG);
+#endif
+ bool verboseLog = false;
+ sqlite3_config(SQLITE_CONFIG_LOG, &sqliteLogCallback, verboseLog ? (void*)1 : NULL);
+
+ // The soft heap limit prevents the page cache allocations from growing
+ // beyond the given limit, no matter what the max page cache sizes are
+ // set to. The limit does not, as of 3.5.0, affect any other allocations.
+ sqlite3_soft_heap_limit(SOFT_HEAP_LIMIT);
+
+ // Initialize SQLite.
+ sqlite3_initialize();
+}
+
+static jint nativeReleaseMemory(JNIEnv* env, jclass clazz) {
+ return sqlite3_release_memory(SOFT_HEAP_LIMIT);
+}
+
+static JNINativeMethod sMethods[] =
+{
+ /* name, signature, funcPtr */
+ { "nativeReleaseMemory", "()I",
+ (void*)nativeReleaseMemory },
+};
+
+int register_android_database_SQLiteGlobal(JNIEnv *env)
+{
+ sqliteInitialize();
+
+ return jniRegisterNativeMethods(env, "io/requery/android/database/sqlite/SQLiteGlobal",
+ sMethods, NELEM(sMethods));
+}
+
+} // namespace android