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. + *
    + *
  1. {@link #STATEMENT_SELECT}
  2. + *
  3. {@link #STATEMENT_UPDATE}
  4. + *
  5. {@link #STATEMENT_ATTACH}
  6. + *
  7. {@link #STATEMENT_BEGIN}
  8. + *
  9. {@link #STATEMENT_COMMIT}
  10. + *
  11. {@link #STATEMENT_ABORT}
  12. + *
  13. {@link #STATEMENT_OTHER}
  14. + *
+ * @param sql the SQL statement whose type is returned by this method + * @return one of the values listed above + */ + public static int getSqlStatementType(String sql) { + sql = sql.trim(); + if (sql.length() < 3) { + return STATEMENT_OTHER; + } + String prefixSql = sql.substring(0, 3); + + 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