Compile sqlite framework inline

pull/214/head
M66B 1 year ago
parent bf9e353754
commit 2c4eb99d6a

@ -452,6 +452,8 @@ configurations.all {
// Support @69c481c39a17d4e1e44a4eb298bb81c48f226eef
exclude group: "androidx.room", module: "room-runtime"
exclude group: "androidx.sqlite", module: "sqlite-framework"
// Workaround https://issuetracker.google.com/issues/134685570
exclude group: "androidx.lifecycle", module: "lifecycle-livedata"
exclude group: "androidx.lifecycle", module: "lifecycle-livedata-core"
@ -624,7 +626,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-common:$room_version" // because of exclude
// https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework
implementation "androidx.sqlite:sqlite-framework:$sqlite_version" // because of exclude
//implementation "androidx.sqlite:sqlite-framework:$sqlite_version" // because of exclude
annotationProcessor "androidx.room:room-compiler:$room_version"
// https://www.sqlite.org/changes.html

@ -489,6 +489,15 @@ final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, Delega
public void close() throws IOException {
mAutoCloser.closeDatabaseIfOpen();
}
@Override
public boolean isExecPerConnectionSQLSupported() {
return false;
}
@Override
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
}
}
/**

@ -25,6 +25,7 @@ import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
@ -299,4 +300,13 @@ final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
public void close() throws IOException {
mDelegate.close();
}
@Override
public boolean isExecPerConnectionSQLSupported() {
return false;
}
@Override
public void execPerConnectionSQL(@NonNull String sql, @Nullable Object[] bindArgs) {
}
}

@ -0,0 +1,109 @@
/*
* Copyright (C) 2017 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 androidx.sqlite.db
/**
* A basic implementation of [SupportSQLiteQuery] which receives a query and its args and
* binds args based on the passed in Object type.
*
* @constructor Creates an SQL query with the sql string and the bind arguments.
*
* @param query The query string, can include bind arguments (.e.g ?).
* @param bindArgs The bind argument value that will replace the placeholders in the query.
*/
class SimpleSQLiteQuery(
private val query: String,
@Suppress("ArrayReturn") // Due to legacy API
private val bindArgs: Array<out Any?>?
) : SupportSQLiteQuery {
/**
* Creates an SQL query without any bind arguments.
*
* @param query The SQL query to execute. Cannot include bind parameters.
*/
constructor(query: String) : this(query, null)
override val sql: String
get() = this.query
/**
* Creates an SQL query without any bind arguments.
*
* @param [statement] The SQL query to execute. Cannot include bind parameters.
*/
override fun bindTo(statement: SupportSQLiteProgram) {
bind(statement, bindArgs)
}
override val argCount: Int
get() = bindArgs?.size ?: 0
companion object {
/**
* Binds the given arguments into the given sqlite statement.
*
* @param [statement] The sqlite statement
* @param [bindArgs] The list of bind arguments
*/
@JvmStatic
fun bind(
statement: SupportSQLiteProgram,
@Suppress("ArrayReturn") // Due to legacy API
bindArgs: Array<out Any?>?
) {
if (bindArgs == null) {
return
}
val limit = bindArgs.size
for (i in 0 until limit) {
val arg = bindArgs[i]
bind(statement, i + 1, arg)
}
}
private fun bind(statement: SupportSQLiteProgram, index: Int, arg: Any?) {
// extracted from android.database.sqlite.SQLiteConnection
if (arg == null) {
statement.bindNull(index)
} else if (arg is ByteArray) {
statement.bindBlob(index, arg)
} else if (arg is Float) {
statement.bindDouble(index, arg.toDouble())
} else if (arg is Double) {
statement.bindDouble(index, arg)
} else if (arg is Long) {
statement.bindLong(index, arg)
} else if (arg is Int) {
statement.bindLong(index, arg.toLong())
} else if (arg is Short) {
statement.bindLong(index, arg.toLong())
} else if (arg is Byte) {
statement.bindLong(index, arg.toLong())
} else if (arg is String) {
statement.bindString(index, arg)
} else if (arg is Boolean) {
statement.bindLong(index, if (arg) 1 else 0)
} else {
throw IllegalArgumentException(
"Cannot bind $arg at index $index Supported types: Null, ByteArray, " +
"Float, Double, Long, Int, Short, Byte, String"
)
}
}
}
}

@ -0,0 +1,295 @@
/*
* Copyright 2021 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 androidx.sqlite.db
import android.app.ActivityManager
import android.content.ContentResolver
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteDatabase.CursorFactory
import android.database.sqlite.SQLiteOpenHelper
import android.net.Uri
import android.os.Bundle
import android.os.CancellationSignal
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import java.io.File
/**
* Helper for accessing features in [SupportSQLiteOpenHelper].
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class SupportSQLiteCompat private constructor() {
/**
* Class for accessing functions that require SDK version 16 and higher.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(16)
object Api16Impl {
/**
* Cancels the operation and signals the cancellation listener. If the operation has not yet
* started, then it will be canceled as soon as it does.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun cancel(cancellationSignal: CancellationSignal) {
cancellationSignal.cancel()
}
/**
* Creates a cancellation signal, initially not canceled.
*
* @return a new cancellation signal
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun createCancellationSignal(): CancellationSignal {
return CancellationSignal()
}
/**
* 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.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun deleteDatabase(file: File): Boolean {
return SQLiteDatabase.deleteDatabase(file)
}
/**
* Runs the provided SQL and returns a cursor over the result set.
*
* @param sql the SQL query. The SQL string must not be ; terminated
* @param selectionArgs You may include ?s in where clause in the query,
* which will be replaced by the values from selectionArgs. The
* values will be bound as Strings.
* @param 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 [OperationCanceledException] will be thrown
* when the query is executed.
* @param cursorFactory the cursor factory to use, or null for the default factory
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun rawQueryWithFactory(
sQLiteDatabase: SQLiteDatabase,
sql: String,
selectionArgs: Array<out String?>,
editTable: String?,
cancellationSignal: CancellationSignal,
cursorFactory: CursorFactory
): Cursor {
return sQLiteDatabase.rawQueryWithFactory(
cursorFactory, sql, selectionArgs, editTable,
cancellationSignal
)
}
/**
* Sets whether foreign key constraints are enabled for the database.
*
* @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.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun setForeignKeyConstraintsEnabled(
sQLiteDatabase: SQLiteDatabase,
enable: Boolean
) {
sQLiteDatabase.setForeignKeyConstraintsEnabled(enable)
}
/**
* This method disables the features enabled by
* [SQLiteDatabase.enableWriteAheadLogging].
*
* @throws - 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.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun disableWriteAheadLogging(sQLiteDatabase: SQLiteDatabase) {
sQLiteDatabase.disableWriteAheadLogging()
}
/**
* Returns true if [SQLiteDatabase.enableWriteAheadLogging] logging has been enabled for
* this database.
*
* For details, see [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING].
*
* @return True if write-ahead logging has been enabled for this database.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun isWriteAheadLoggingEnabled(sQLiteDatabase: SQLiteDatabase): Boolean {
return sQLiteDatabase.isWriteAheadLoggingEnabled
}
/**
* Sets [SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING] flag if `enabled` is `true`, unsets
* otherwise.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun setWriteAheadLoggingEnabled(
sQLiteOpenHelper: SQLiteOpenHelper,
enabled: Boolean
) {
sQLiteOpenHelper.setWriteAheadLoggingEnabled(enabled)
}
}
/**
* Helper for accessing functions that require SDK version 19 and higher.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(19)
object Api19Impl {
/**
* Return the URI at which notifications of changes in this Cursor's data
* will be delivered.
*
* @return Returns a URI that can be used with [ContentResolver.registerContentObserver] to
* find out about changes to this Cursor's data. May be null if no notification URI has been
* set.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun getNotificationUri(cursor: Cursor): Uri {
return cursor.notificationUri
}
/**
* Returns true if this is a low-RAM device. Exactly whether a device is low-RAM
* is ultimately up to the device configuration, but currently it generally means
* something with 1GB or less of RAM. This is mostly intended to be used by apps
* to determine whether they should turn off certain features that require more RAM.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun isLowRamDevice(activityManager: ActivityManager): Boolean {
return activityManager.isLowRamDevice
}
}
/**
* Helper for accessing functions that require SDK version 21 and higher.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(21)
object Api21Impl {
/**
* Returns the absolute path to the directory on the filesystem.
*
* @return The path of the directory holding application files that will not be
* automatically backed up to remote storage.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun getNoBackupFilesDir(context: Context): File {
return context.noBackupFilesDir
}
}
/**
* Helper for accessing functions that require SDK version 23 and higher.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(23)
object Api23Impl {
/**
* Sets a [Bundle] that will be returned by [Cursor.getExtras].
*
* @param extras [Bundle] to set, or null to set an empty bundle.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun setExtras(cursor: Cursor, extras: Bundle) {
cursor.extras = extras
}
}
/**
* Helper for accessing functions that require SDK version 29 and higher.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@RequiresApi(29)
object Api29Impl {
/**
* Similar to [Cursor.setNotificationUri], except this version
* allows to watch multiple content URIs for changes.
*
* @param cr The content resolver from the caller's context. The listener attached to
* this resolver will be notified.
* @param uris The content URIs to watch.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun setNotificationUris(
cursor: Cursor,
cr: ContentResolver,
uris: List<Uri?>
) {
cursor.setNotificationUris(cr, uris)
}
/**
* Return the URIs at which notifications of changes in this Cursor's data
* will be delivered, as previously set by [setNotificationUris].
*
* @return Returns URIs that can be used with [ContentResolver.registerContentObserver]
* to find out about changes to this Cursor's data. May be null if no notification URI has
* been set.
*
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@JvmStatic
fun getNotificationUris(cursor: Cursor): List<Uri> {
return cursor.notificationUris!!
}
}
}

@ -0,0 +1,598 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db
import android.annotation.SuppressLint
import android.content.ContentValues
import android.database.Cursor
import android.database.SQLException
import android.database.sqlite.SQLiteTransactionListener
import android.os.Build
import android.os.CancellationSignal
import android.util.Pair
import androidx.annotation.RequiresApi
import java.io.Closeable
import java.util.Locale
/**
* A database abstraction which removes the framework dependency and allows swapping underlying
* sql versions. It mimics the behavior of [android.database.sqlite.SQLiteDatabase]
*/
interface SupportSQLiteDatabase : Closeable {
/**
* Compiles the given SQL statement.
*
* @param sql The sql query.
* @return Compiled statement.
*/
fun compileStatement(sql: String): SupportSQLiteStatement
/**
* 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()
* }
* ```
*/
fun beginTransaction()
/**
* 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()
* }
* ```
*/
fun beginTransactionNonExclusive()
/**
* 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
* [yieldIfContendedSafely].
*/
fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener)
/**
* 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 [yieldIfContendedSafely].
*/
fun beginTransactionWithListenerNonExclusive(
transactionListener: SQLiteTransactionListener
)
/**
* End a transaction. See beginTransaction for notes about how to use this and when transactions
* are committed and rolled back.
*/
fun endTransaction()
/**
* 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.
*/
fun setTransactionSuccessful()
/**
* Returns true if the current thread has a transaction pending.
*
* @return True if the current thread is in a transaction.
*/
fun inTransaction(): Boolean
/**
* 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.
*/
val isDbLockedByCurrentThread: Boolean
/**
* 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
*/
fun yieldIfContendedSafely(): Boolean
/**
* 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 sleepAfterYieldDelayMillis 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
*/
fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean
/**
* Is true if [execPerConnectionSQL] is supported by the implementation.
*/
@get:Suppress("AcronymName") // To keep consistency with framework method name.
val isExecPerConnectionSQLSupported: Boolean
get() = false
/**
* Execute the given SQL statement on all connections to this database.
*
* This statement will be immediately executed on all existing connections,
* and will be automatically executed on all future connections.
*
* Some example usages are changes like `PRAGMA trusted_schema=OFF` or
* functions like `SELECT icu_load_collation()`. If you execute these
* statements using [execSQL] then they will only apply to a single
* database connection; using this method will ensure that they are
* uniformly applied to all current and future connections.
*
* An implementation of [SupportSQLiteDatabase] might not support this operation. Use
* [isExecPerConnectionSQLSupported] to check if this operation is supported before
* calling this method.
*
* @param sql The SQL statement to be executed. Multiple statements
* separated by semicolons are not supported.
* @param bindArgs The arguments that should be bound to the SQL statement.
* @throws UnsupportedOperationException if this operation is not supported. To check if it
* supported use [isExecPerConnectionSQLSupported]
*/
@Suppress("AcronymName") // To keep consistency with framework method name.
fun execPerConnectionSQL(
sql: String,
@SuppressLint("ArrayReturn") bindArgs: Array<out Any?>?
) {
throw UnsupportedOperationException()
}
/**
* The database version.
*/
var version: Int
/**
* The maximum size the database may grow to.
*/
val maximumSize: Long
/**
* 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
*/
fun setMaximumSize(numBytes: Long): Long
/**
* The current database page size, in bytes.
*
* 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.
*/
var pageSize: Long
/**
* Runs the given query on the database. If you would like to have typed bind arguments,
* use [query].
*
* @param query The SQL query that includes the query and can bind into a given compiled
* program.
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*/
fun query(query: String): Cursor
/**
* Runs the given query on the database. If you would like to have bind arguments,
* use [query].
*
* @param query The SQL query that includes the query and can bind into a given compiled
* program.
* @param bindArgs The query arguments to bind.
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*/
fun query(query: String, bindArgs: Array<out Any?>): Cursor
/**
* Runs the given query on the database.
*
* This class allows using type safe sql program bindings while running queries.
*
* @param query The [SimpleSQLiteQuery] query that includes the query and can bind into a
* given compiled program.
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*/
fun query(query: SupportSQLiteQuery): Cursor
/**
* Runs the given query on the database.
*
* This class allows using type safe sql program bindings while running queries.
*
* @param query The SQL query that includes the query and can bind into a given compiled
* program.
* @param cancellationSignal A signal to cancel the operation in progress, or null if none.
* If the operation is canceled, then [androidx.core.os.OperationCanceledException] will be
* thrown when the query is executed.
* @return A [Cursor] object, which is positioned before the first entry. Note that
* [Cursor]s are not synchronized, see the documentation for more details.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
fun query(
query: SupportSQLiteQuery,
cancellationSignal: CancellationSignal?
): Cursor
/**
* Convenience method for inserting a row into the database.
*
* @param table the table to insert the row into
* @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
* @param conflictAlgorithm for insert conflict resolver. One of
* [android.database.sqlite.SQLiteDatabase.CONFLICT_NONE],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_ROLLBACK],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_ABORT],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE].
* @return the row ID of the newly inserted row, or -1 if an error occurred
* @throws SQLException If the insert fails
*/
@Throws(SQLException::class)
fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long
/**
* 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.
*/
fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int
/**
* Convenience method for updating rows in the database.
*
* @param table the table to update in
* @param conflictAlgorithm for update conflict resolver. One of
* [android.database.sqlite.SQLiteDatabase.CONFLICT_NONE],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_ROLLBACK],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_ABORT],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE],
* [android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE].
* @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
*/
fun update(
table: String,
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
whereArgs: Array<out Any?>?
): Int
/**
* Execute a single SQL statement that does not return any data.
*
* When using [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
* [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
*/
@Throws(SQLException::class)
fun execSQL(sql: String)
/**
* Execute a single SQL statement that does not return any data.
*
* When using [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
* [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 selectionArgs.
* @throws SQLException if the SQL string is invalid
*/
@Throws(SQLException::class)
fun execSQL(sql: String, bindArgs: Array<out Any?>)
/**
* Is true if the database is opened as read only.
*/
val isReadOnly: Boolean
/**
* Is true if the database is currently open.
*/
val isOpen: Boolean
/**
* 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.
*/
fun needUpgrade(newVersion: Int): Boolean
/**
* The path to the database file.
*/
val path: String?
/**
* Sets the locale for this database. Does nothing if this database has
* the [android.database.sqlite.SQLiteDatabase.NO_LOCALIZED_COLLATORS] flag set or was opened
* read only.
*
* @param locale The new locale.
* @throws SQLException if the locale could not be set. The most common reason
* for this is that there is no collator available for the locale you
* requested.
* In this case the database remains unchanged.
*/
fun setLocale(locale: Locale)
/**
* 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
* [android.database.sqlite.SQLiteDatabase.MAX_SQL_CACHE_SIZE])
* @throws IllegalStateException if input cacheSize is over the max.
* [android.database.sqlite.SQLiteDatabase.MAX_SQL_CACHE_SIZE].
*/
fun setMaxSqlCacheSize(cacheSize: Int)
/**
* 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 `#openOrCreateDatabase`
* or in the [SupportSQLiteOpenHelper.Callback.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 [isDatabaseIntegrityOk].
*
* This method must not be called while a transaction is in progress.
*
* See also [SQLite Foreign Key Constraints](http://sqlite.org/foreignkeys.html)
* for more details about foreign key constraint support.
*
* @param enabled True to enable foreign key constraints, false to disable them.
* @throws IllegalStateException if the are transactions is in progress
* when this method is called.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
fun setForeignKeyConstraintsEnabled(enabled: Boolean)
/**
* 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 [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 [beginTransactionNonExclusive] or
* [beginTransactionWithListenerNonExclusive]
* 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, `enableWriteAheadLogging` returns false.
*
* The best way to enable write-ahead logging is to pass the
* [android.database.sqlite.SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING] flag to
* [android.database.sqlite.SQLiteDatabase.openDatabase]. This is more efficient than calling
*
* 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 `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](http://sqlite.org/wal.html) 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.
*/
fun enableWriteAheadLogging(): Boolean
/**
* This method disables the features enabled by [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.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
fun disableWriteAheadLogging()
/**
* Is true if write-ahead logging has been enabled for this database.
*/
@get:RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
val isWriteAheadLoggingEnabled: Boolean
/**
* The list of full path names of all attached databases including the main database
* by executing 'pragma database_list' on the database.
*/
@get:Suppress("NullableCollection")
val attachedDbs: List<Pair<String, String>>?
/**
* Is true if the given database (and all its attached databases) pass integrity_check,
* false otherwise.
*/
val isDatabaseIntegrityOk: Boolean
}

@ -0,0 +1,425 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db
import android.content.Context
import android.database.sqlite.SQLiteException
import android.os.Build
import android.util.Log
import android.util.Pair
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SupportSQLiteCompat.Api16Impl.deleteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback
import androidx.sqlite.db.SupportSQLiteOpenHelper.Factory
import java.io.Closeable
import java.io.File
import java.io.IOException
/**
* An interface to map the behavior of [android.database.sqlite.SQLiteOpenHelper].
* Note that since that class requires overriding certain methods, support implementation
* uses [Factory.create] to create this and [Callback] to implement
* the methods that should be overridden.
*/
interface SupportSQLiteOpenHelper : Closeable {
/**
* Return the name of the SQLite database being opened, as given to
* the constructor. `null` indicates an in-memory database.
*/
val databaseName: String?
/**
* Enables or disables the use of write-ahead logging for the database.
*
* See [SupportSQLiteDatabase.enableWriteAheadLogging] for details.
*
* 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.
*/
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
fun setWriteAheadLoggingEnabled(enabled: Boolean)
/**
* 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
* [Callback.onCreate], [Callback.onUpgrade] and/or [Callback.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 [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 [ContentProvider.onCreate()].
*
* @return a read/write database object valid until [close] is called
* @throws SQLiteException if the database cannot be opened for writing
*/
val writableDatabase: SupportSQLiteDatabase
/**
* Create and/or open a database. This will be the same object returned by
* [writableDatabase] 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 [writableDatabase] 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 [writableDatabase], this method may
* take a long time to return, so you should not call it from the
* application main thread, including from
* [ContentProvider.onCreate()].
*
* @return a database object valid until [writableDatabase]
* or [close] is called.
* @throws SQLiteException if the database cannot be opened
*/
val readableDatabase: SupportSQLiteDatabase
/**
* Close any open database object.
*/
override fun close()
/**
* Creates a new Callback to get database lifecycle events.
*
* Handles various lifecycle events for the SQLite connection, similar to
* [room-runtime.SQLiteOpenHelper].
*/
abstract class Callback(
/**
* Version number of the database (starting at 1); if the database is older,
* [Callback.onUpgrade] will be used to upgrade the database; if the database is newer,
* [Callback.onDowngrade] will be used to downgrade the database.
*/
@JvmField
val version: Int
) {
/**
* 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 [onCreate], [onUpgrade], [onDowngrade],
* or [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 [SupportSQLiteDatabase.enableWriteAheadLogging]
* [SupportSQLiteDatabase.setForeignKeyConstraintsEnabled],
* [SupportSQLiteDatabase.setLocale],
* [SupportSQLiteDatabase.setMaximumSize], or executing PRAGMA statements.
*
* @param db The database.
*/
open fun onConfigure(db: SupportSQLiteDatabase) {}
/**
* 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.
*/
abstract fun onCreate(db: SupportSQLiteDatabase)
/**
* 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](http://sqlite.org/lang_altertable.html). 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.
*/
abstract fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
)
/**
* Called when the database needs to be downgraded. This is strictly similar to
* [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.
*/
open fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
throw SQLiteException(
"Can't downgrade database from version $oldVersion to $newVersion"
)
}
/**
* Called when the database has been opened. The implementation
* should check [SupportSQLiteDatabase.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 [onConfigure] instead.
*
* @param db The database.
*/
open fun onOpen(db: SupportSQLiteDatabase) {}
/**
* The method invoked when database corruption is detected. Default implementation will
* delete the database file.
*
* @param db the [SupportSQLiteDatabase] object representing the database on which
* corruption is detected.
*/
open fun onCorruption(db: SupportSQLiteDatabase) {
// the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
Log.e(TAG, "Corruption reported by sqlite on database: $db.path")
// is the corruption detected even before database could be 'opened'?
if (!db.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).
db.path?.let { deleteDatabaseFile(it) }
return
}
var attachedDbs: List<Pair<String, String>>? = null
try {
// Close the database, which will cause subsequent operations to fail.
// before that, get the attached database list first.
try {
attachedDbs = db.attachedDbs
} catch (e: SQLiteException) {
/* ignore */
}
try {
db.close()
} catch (e: IOException) {
/* ignore */
}
} finally {
// Delete all files of this corrupt database and/or attached databases
// attachedDbs = null is possible when the database is so corrupt that even
// "PRAGMA database_list;" also fails. delete the main database file
attachedDbs?.forEach { p ->
deleteDatabaseFile(p.second)
} ?: db.path?.let { deleteDatabaseFile(it) }
}
}
private fun deleteDatabaseFile(fileName: String) {
if (fileName.equals(":memory:", ignoreCase = true) ||
fileName.trim { it <= ' ' }.isEmpty()
) {
return
}
Log.w(TAG, "deleting the database file: $fileName")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
deleteDatabase(File(fileName))
} else {
try {
val deleted = File(fileName).delete()
if (!deleted) {
Log.e(TAG, "Could not delete the database file $fileName")
}
} catch (error: Exception) {
Log.e(TAG, "error while deleting corrupted database file", error)
}
}
} catch (e: Exception) {
/* print warning and ignore exception */
Log.w(TAG, "delete failed: ", e)
}
}
internal companion object {
private const val TAG = "SupportSQLite"
}
}
/**
* The configuration to create an SQLite open helper object using [Factory].
*/
class Configuration
@Suppress("ExecutorRegistration") // For backwards compatibility
constructor(
/**
* Context to use to open or create the database.
*/
@JvmField
val context: Context,
/**
* Name of the database file, or null for an in-memory database.
*/
@JvmField
val name: String?,
/**
* The callback class to handle creation, upgrade and downgrade.
*/
@JvmField
val callback: Callback,
/**
* If `true` the database will be stored in the no-backup directory.
*/
@JvmField
@Suppress("ListenerLast")
val useNoBackupDirectory: Boolean = false,
/**
* If `true` the database will be delete and its data loss in the case that it
* cannot be opened.
*/
@JvmField
@Suppress("ListenerLast")
val allowDataLossOnRecovery: Boolean = false
) {
/**
* Builder class for [Configuration].
*/
open class Builder internal constructor(context: Context) {
private val context: Context
private var name: String? = null
private var callback: Callback? = null
private var useNoBackupDirectory = false
private var allowDataLossOnRecovery = false
/**
* Throws an [IllegalArgumentException] if the [Callback] is `null`.
*
* Throws an [IllegalArgumentException] if the [Context] is `null`.
*
* Throws an [IllegalArgumentException] if the [String] database
* name is `null`. [Context.getNoBackupFilesDir]
*
* @return The [Configuration] instance
*/
open fun build(): Configuration {
val callback = callback
requireNotNull(callback) {
"Must set a callback to create the configuration."
}
require(!useNoBackupDirectory || !name.isNullOrEmpty()) {
"Must set a non-null database name to a configuration that uses the " +
"no backup directory."
}
return Configuration(
context,
name,
callback,
useNoBackupDirectory,
allowDataLossOnRecovery
)
}
init {
this.context = context
}
/**
* @param name Name of the database file, or null for an in-memory database.
* @return This builder instance.
*/
open fun name(name: String?): Builder = apply {
this.name = name
}
/**
* @param callback The callback class to handle creation, upgrade and downgrade.
* @return This builder instance.
*/
open fun callback(callback: Callback): Builder = apply {
this.callback = callback
}
/**
* Sets whether to use a no backup directory or not.
*
* @param useNoBackupDirectory If `true` the database file will be stored in the
* no-backup directory.
* @return This builder instance.
*/
open fun noBackupDirectory(useNoBackupDirectory: Boolean): Builder = apply {
this.useNoBackupDirectory = useNoBackupDirectory
}
/**
* Sets whether to delete and recreate the database file in situations when the
* database file cannot be opened, thus allowing for its data to be lost.
*
* @param allowDataLossOnRecovery If `true` the database file might be recreated
* in the case that it cannot be opened.
* @return this
*/
open fun allowDataLossOnRecovery(allowDataLossOnRecovery: Boolean): Builder = apply {
this.allowDataLossOnRecovery = allowDataLossOnRecovery
}
}
companion object {
/**
* Creates a new Configuration.Builder to create an instance of Configuration.
*
* @param context to use to open or create the database.
*/
@JvmStatic
fun builder(context: Context): Builder {
return Builder(context)
}
}
}
/**
* Factory class to create instances of [SupportSQLiteOpenHelper] using
* [Configuration].
*/
fun interface Factory {
/**
* Creates an instance of [SupportSQLiteOpenHelper] using the given configuration.
*
* @param configuration The configuration to use while creating the open helper.
*
* @return A SupportSQLiteOpenHelper which can be used to open a database.
*/
fun create(configuration: Configuration): SupportSQLiteOpenHelper
}
}

@ -0,0 +1,72 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db
import java.io.Closeable
/**
* An interface to map the behavior of [android.database.sqlite.SQLiteProgram].
*/
interface SupportSQLiteProgram : Closeable {
/**
* Bind a NULL value to this statement. The value remains bound until
* [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind null to
*/
fun bindNull(index: Int)
/**
* Bind a long value to this statement. The value remains bound until
* [clearBindings] is called.
* addToBindArgs
* @param index The 1-based index to the parameter to bind
* @param value The value to bind
*/
fun bindLong(index: Int, value: Long)
/**
* Bind a double value to this statement. The value remains bound until
* [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind
*/
fun bindDouble(index: Int, value: Double)
/**
* Bind a String value to this statement. The value remains bound until
* [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind, must not be null
*/
fun bindString(index: Int, value: String)
/**
* Bind a byte array value to this statement. The value remains bound until
* [.clearBindings] is called.
*
* @param index The 1-based index to the parameter to bind
* @param value The value to bind, must not be null
*/
fun bindBlob(index: Int, value: ByteArray)
/**
* Clears all existing bindings. Unset bindings are treated as NULL.
*/
fun clearBindings()
}

@ -0,0 +1,41 @@
/*
* Copyright (C) 2017 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 androidx.sqlite.db
/**
* A query with typed bindings. It is better to use this API instead of
* [android.database.sqlite.SQLiteDatabase.rawQuery] because it allows
* binding type safe parameters.
*/
interface SupportSQLiteQuery {
/**
* The SQL query. This query can have placeholders(?) for bind arguments.
*/
val sql: String
/**
* Callback to bind the query parameters to the compiled statement.
*
* @param statement The compiled statement
*/
fun bindTo(statement: SupportSQLiteProgram)
/**
* Is the number of arguments in this query. This is equal to the number of placeholders
* in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
*/
val argCount: Int
}

@ -0,0 +1,186 @@
/*
* Copyright (C) 2017 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 androidx.sqlite.db
import java.util.regex.Pattern
/**
* A simple query builder to create SQL SELECT queries.
*/
class SupportSQLiteQueryBuilder private constructor(private val table: String) {
private var distinct = false
private var columns: Array<out String>? = null
private var selection: String? = null
private var bindArgs: Array<out Any?>? = null
private var groupBy: String? = null
private var having: String? = null
private var orderBy: String? = null
private var limit: String? = null
/**
* Adds DISTINCT keyword to the query.
*
* @return this
*/
fun distinct(): SupportSQLiteQueryBuilder = apply {
this.distinct = true
}
/**
* Sets the given list of columns as the columns that will be returned.
*
* @param columns The list of column names that should be returned.
*
* @return this
*/
fun columns(columns: Array<out String>?): SupportSQLiteQueryBuilder = apply {
this.columns = columns
}
/**
* Sets the arguments for the WHERE clause.
*
* @param selection The list of selection columns
* @param bindArgs The list of bind arguments to match against these columns
*
* @return this
*/
fun selection(
selection: String?,
bindArgs: Array<out Any?>?
): SupportSQLiteQueryBuilder = apply {
this.selection = selection
this.bindArgs = bindArgs
}
/**
* Adds a GROUP BY statement.
*
* @param groupBy The value of the GROUP BY statement.
*
* @return this
*/
fun groupBy(groupBy: String?): SupportSQLiteQueryBuilder = apply {
this.groupBy = groupBy
}
/**
* Adds a HAVING statement. You must also provide [groupBy] for this to work.
*
* @param having The having clause.
*
* @return this
*/
fun having(having: String?): SupportSQLiteQueryBuilder = apply {
this.having = having
}
/**
* Adds an ORDER BY statement.
*
* @param orderBy The order clause.
*
* @return this
*/
fun orderBy(orderBy: String?): SupportSQLiteQueryBuilder = apply {
this.orderBy = orderBy
}
/**
* Adds a LIMIT statement.
*
* @param limit The limit value.
*
* @return this
*/
fun limit(limit: String): SupportSQLiteQueryBuilder = apply {
val patternMatches = limitPattern.matcher(
limit
).matches()
require(limit.isEmpty() || patternMatches) { "invalid LIMIT clauses:$limit" }
this.limit = limit
}
/**
* Creates the [SupportSQLiteQuery] that can be passed into
* [SupportSQLiteDatabase.query].
*
* @return a new query
*/
fun create(): SupportSQLiteQuery {
require(!groupBy.isNullOrEmpty() || having.isNullOrEmpty()) {
"HAVING clauses are only permitted when using a groupBy clause"
}
val query = buildString(120) {
append("SELECT ")
if (distinct) {
append("DISTINCT ")
}
if (!columns.isNullOrEmpty()) {
appendColumns(columns!!)
} else {
append("* ")
}
append("FROM ")
append(table)
appendClause(" WHERE ", selection)
appendClause(" GROUP BY ", groupBy)
appendClause(" HAVING ", having)
appendClause(" ORDER BY ", orderBy)
appendClause(" LIMIT ", limit)
}
return SimpleSQLiteQuery(query, bindArgs)
}
private fun StringBuilder.appendClause(name: String, clause: String?) {
if (!clause.isNullOrEmpty()) {
append(name)
append(clause)
}
}
/**
* Add the names that are non-null in columns to string, separating
* them with commas.
*/
private fun StringBuilder.appendColumns(columns: Array<out String>) {
val n = columns.size
for (i in 0 until n) {
val column = columns[i]
if (i > 0) {
append(", ")
}
append(column)
}
append(' ')
}
companion object {
private val limitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?")
/**
* Creates a query for the given table name.
*
* @param tableName The table name(s) to query.
*
* @return A builder to create a query.
*/
@JvmStatic
fun builder(tableName: String): SupportSQLiteQueryBuilder {
return SupportSQLiteQueryBuilder(tableName)
}
}
}

@ -0,0 +1,71 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db
/**
* An interface to map the behavior of [android.database.sqlite.SQLiteStatement].
*/
interface SupportSQLiteStatement : SupportSQLiteProgram {
/**
* Execute this SQL statement, if it is not a SELECT / INSERT / DELETE / UPDATE, for example
* CREATE / DROP table, view, trigger, index etc.
*
* @throws [android.database.SQLException] If the SQL string is invalid for
* some reason
*/
fun execute()
/**
* Execute this SQL statement, if the the number of rows affected by execution of this SQL
* statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements.
*
* @return the number of rows affected by this SQL statement execution.
* @throws [android.database.SQLException] If the SQL string is invalid for
* some reason
*/
fun executeUpdateDelete(): Int
/**
* Execute this SQL statement and return the ID of the row inserted due to this call.
* The SQL statement should be an INSERT for this to be a useful call.
*
* @return the row ID of the last row inserted, if this insert is successful. -1 otherwise.
*
* @throws [android.database.SQLException] If the SQL string is invalid for
* some reason
*/
fun executeInsert(): Long
/**
* Execute a statement that returns a 1 by 1 table with a numeric value.
* For example, SELECT COUNT(*) FROM table;
*
* @return The result of the query.
*
* @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
*/
fun simpleQueryForLong(): Long
/**
* Execute a statement that returns a 1 by 1 table with a text value.
* For example, SELECT COUNT(*) FROM table;
*
* @return The result of the query.
*
* @throws [android.database.sqlite.SQLiteDoneException] if the query returns zero rows
*/
fun simpleQueryForString(): String?
}

@ -0,0 +1,335 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db.framework
import android.content.ContentValues
import android.database.Cursor
import android.database.SQLException
import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteCursorDriver
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQuery
import android.database.sqlite.SQLiteTransactionListener
import android.os.Build
import android.os.CancellationSignal
import android.text.TextUtils
import android.util.Pair
import androidx.annotation.DoNotInline
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteCompat
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteStatement
import java.io.IOException
import java.util.Locale
/**
* Delegates all calls to an implementation of [SQLiteDatabase].
*
* @constructor Creates a wrapper around [SQLiteDatabase].
*
* @param delegate The delegate to receive all calls.
*/
internal class FrameworkSQLiteDatabase(
private val delegate: SQLiteDatabase
) : SupportSQLiteDatabase {
override fun compileStatement(sql: String): SupportSQLiteStatement {
return FrameworkSQLiteStatement(delegate.compileStatement(sql))
}
override fun beginTransaction() {
delegate.beginTransaction()
}
override fun beginTransactionNonExclusive() {
delegate.beginTransactionNonExclusive()
}
override fun beginTransactionWithListener(
transactionListener: SQLiteTransactionListener
) {
delegate.beginTransactionWithListener(transactionListener)
}
override fun beginTransactionWithListenerNonExclusive(
transactionListener: SQLiteTransactionListener
) {
delegate.beginTransactionWithListenerNonExclusive(transactionListener)
}
override fun endTransaction() {
delegate.endTransaction()
}
override fun setTransactionSuccessful() {
delegate.setTransactionSuccessful()
}
override fun inTransaction(): Boolean {
return delegate.inTransaction()
}
override val isDbLockedByCurrentThread: Boolean
get() = delegate.isDbLockedByCurrentThread
override fun yieldIfContendedSafely(): Boolean {
return delegate.yieldIfContendedSafely()
}
override fun yieldIfContendedSafely(sleepAfterYieldDelayMillis: Long): Boolean {
return delegate.yieldIfContendedSafely(sleepAfterYieldDelayMillis)
}
override var version: Int
get() = delegate.version
set(value) {
delegate.version = value
}
override var maximumSize: Long
get() = delegate.maximumSize
set(numBytes) {
delegate.maximumSize = numBytes
}
override fun setMaximumSize(numBytes: Long): Long {
delegate.maximumSize = numBytes
return delegate.maximumSize
}
override val isExecPerConnectionSQLSupported: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
override fun execPerConnectionSQL(sql: String, bindArgs: Array<out Any?>?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Api30Impl.execPerConnectionSQL(delegate, sql, bindArgs)
} else {
throw UnsupportedOperationException(
"execPerConnectionSQL is not supported on a " +
"SDK version lower than 30, current version is: " + Build.VERSION.SDK_INT
)
}
}
override var pageSize: Long
get() = delegate.pageSize
set(numBytes) {
delegate.pageSize = numBytes
}
override fun query(query: String): Cursor {
return query(SimpleSQLiteQuery(query))
}
override fun query(query: String, bindArgs: Array<out Any?>): Cursor {
return query(SimpleSQLiteQuery(query, bindArgs))
}
override fun query(query: SupportSQLiteQuery): Cursor {
val cursorFactory = {
_: SQLiteDatabase?,
masterQuery: SQLiteCursorDriver?,
editTable: String?,
sqLiteQuery: SQLiteQuery? ->
query.bindTo(
FrameworkSQLiteProgram(
sqLiteQuery!!
)
)
SQLiteCursor(masterQuery, editTable, sqLiteQuery)
}
return delegate.rawQueryWithFactory(
cursorFactory, query.sql, EMPTY_STRING_ARRAY, null)
}
@RequiresApi(16)
override fun query(
query: SupportSQLiteQuery,
cancellationSignal: CancellationSignal?
): Cursor {
return SupportSQLiteCompat.Api16Impl.rawQueryWithFactory(delegate, query.sql,
EMPTY_STRING_ARRAY, null, cancellationSignal!!
) { _: SQLiteDatabase?,
masterQuery: SQLiteCursorDriver?,
editTable: String?,
sqLiteQuery: SQLiteQuery? ->
query.bindTo(
FrameworkSQLiteProgram(
sqLiteQuery!!
)
)
SQLiteCursor(masterQuery, editTable, sqLiteQuery)
}
}
@Throws(SQLException::class)
override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long {
return delegate.insertWithOnConflict(table, null, values, conflictAlgorithm)
}
override fun delete(table: String, whereClause: String?, whereArgs: Array<out Any?>?): Int {
val query = buildString {
append("DELETE FROM ")
append(table)
if (!whereClause.isNullOrEmpty()) {
append(" WHERE ")
append(whereClause)
}
}
val statement = compileStatement(query)
SimpleSQLiteQuery.bind(statement, whereArgs)
return statement.executeUpdateDelete()
}
override fun update(
table: String,
conflictAlgorithm: Int,
values: ContentValues,
whereClause: String?,
whereArgs: Array<out Any?>?
): Int {
// taken from SQLiteDatabase class.
require(values.size() != 0) { "Empty values" }
// move all bind args to one array
val setValuesSize = values.size()
val bindArgsSize =
if (whereArgs == null) setValuesSize else setValuesSize + whereArgs.size
val bindArgs = arrayOfNulls<Any>(bindArgsSize)
val sql = buildString {
append("UPDATE ")
append(CONFLICT_VALUES[conflictAlgorithm])
append(table)
append(" SET ")
var i = 0
for (colName in values.keySet()) {
append(if (i > 0) "," else "")
append(colName)
bindArgs[i++] = values[colName]
append("=?")
}
if (whereArgs != null) {
i = setValuesSize
while (i < bindArgsSize) {
bindArgs[i] = whereArgs[i - setValuesSize]
i++
}
}
if (!TextUtils.isEmpty(whereClause)) {
append(" WHERE ")
append(whereClause)
}
}
val stmt = compileStatement(sql)
SimpleSQLiteQuery.bind(stmt, bindArgs)
return stmt.executeUpdateDelete()
}
@Throws(SQLException::class)
override fun execSQL(sql: String) {
delegate.execSQL(sql)
}
@Throws(SQLException::class)
override fun execSQL(sql: String, bindArgs: Array<out Any?>) {
delegate.execSQL(sql, bindArgs)
}
override val isReadOnly: Boolean
get() = delegate.isReadOnly
override val isOpen: Boolean
get() = delegate.isOpen
override fun needUpgrade(newVersion: Int): Boolean {
return delegate.needUpgrade(newVersion)
}
override val path: String?
get() = delegate.path
override fun setLocale(locale: Locale) {
delegate.setLocale(locale)
}
override fun setMaxSqlCacheSize(cacheSize: Int) {
delegate.setMaxSqlCacheSize(cacheSize)
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun setForeignKeyConstraintsEnabled(enabled: Boolean) {
SupportSQLiteCompat.Api16Impl.setForeignKeyConstraintsEnabled(delegate, enabled)
}
override fun enableWriteAheadLogging(): Boolean {
return delegate.enableWriteAheadLogging()
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun disableWriteAheadLogging() {
SupportSQLiteCompat.Api16Impl.disableWriteAheadLogging(delegate)
}
@get:RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override val isWriteAheadLoggingEnabled: Boolean
get() = SupportSQLiteCompat.Api16Impl.isWriteAheadLoggingEnabled(delegate)
override val attachedDbs: List<Pair<String, String>>?
get() = delegate.attachedDbs
override val isDatabaseIntegrityOk: Boolean
get() = delegate.isDatabaseIntegrityOk
@Throws(IOException::class)
override fun close() {
delegate.close()
}
/**
* Checks if this object delegates to the same given database reference.
*/
fun isDelegate(sqLiteDatabase: SQLiteDatabase): Boolean {
return delegate == sqLiteDatabase
}
@RequiresApi(30)
internal object Api30Impl {
@DoNotInline
fun execPerConnectionSQL(
sQLiteDatabase: SQLiteDatabase,
sql: String,
bindArgs: Array<out Any?>?
) {
sQLiteDatabase.execPerConnectionSQL(sql, bindArgs)
}
}
companion object {
private val CONFLICT_VALUES =
arrayOf(
"",
" OR ROLLBACK ",
" OR ABORT ",
" OR FAIL ",
" OR IGNORE ",
" OR REPLACE "
)
private val EMPTY_STRING_ARRAY = arrayOfNulls<String>(0)
}
}

@ -0,0 +1,341 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db.framework
import android.content.Context
import android.database.DatabaseErrorHandler
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SupportSQLiteCompat
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import androidx.sqlite.util.ProcessLock
import java.io.File
import java.util.UUID
internal class FrameworkSQLiteOpenHelper @JvmOverloads constructor(
private val context: Context,
private val name: String?,
private val callback: SupportSQLiteOpenHelper.Callback,
private val useNoBackupDirectory: Boolean = false,
private val allowDataLossOnRecovery: Boolean = false
) : SupportSQLiteOpenHelper {
// Delegate is created lazily
private val lazyDelegate = lazy {
// OpenHelper initialization code
val openHelper: OpenHelper
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
name != null &&
useNoBackupDirectory
) {
val file = File(
SupportSQLiteCompat.Api21Impl.getNoBackupFilesDir(context),
name
)
openHelper = OpenHelper(
context = context,
name = file.absolutePath,
dbRef = DBRefHolder(null),
callback = callback,
allowDataLossOnRecovery = allowDataLossOnRecovery
)
} else {
openHelper = OpenHelper(
context = context,
name = name,
dbRef = DBRefHolder(null),
callback = callback,
allowDataLossOnRecovery = allowDataLossOnRecovery
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(
openHelper,
writeAheadLoggingEnabled
)
}
return@lazy openHelper
}
private var writeAheadLoggingEnabled = false
// getDelegate() is lazy because we don't want to File I/O until the call to
// getReadableDatabase() or getWritableDatabase(). This is better because the call to
// a getReadableDatabase() or a getWritableDatabase() happens on a background thread unless
// queries are allowed on the main thread.
// We defer computing the path the database from the constructor to getDelegate()
// because context.getNoBackupFilesDir() does File I/O :(
private val delegate: OpenHelper by lazyDelegate
override val databaseName: String?
get() = name
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
if (lazyDelegate.isInitialized()) {
// Use 'delegate', it is already initialized
SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(delegate, enabled)
}
writeAheadLoggingEnabled = enabled
}
override val writableDatabase: SupportSQLiteDatabase
get() = delegate.getSupportDatabase(true)
override val readableDatabase: SupportSQLiteDatabase
get() = delegate.getSupportDatabase(false)
override fun close() {
if (lazyDelegate.isInitialized()) {
delegate.close()
}
}
private class OpenHelper(
val context: Context,
name: String?,
/**
* This is used as an Object reference so that we can access the wrapped database inside
* the constructor. SQLiteOpenHelper requires the error handler to be passed in the
* constructor.
*/
val dbRef: DBRefHolder,
val callback: SupportSQLiteOpenHelper.Callback,
val allowDataLossOnRecovery: Boolean
) : SQLiteOpenHelper(
context, name, null, callback.version,
DatabaseErrorHandler { dbObj ->
callback.onCorruption(
getWrappedDb(
dbRef,
dbObj
)
)
}) {
// see b/78359448
private var migrated = false
// see b/193182592
private val lock: ProcessLock = ProcessLock(
name = name ?: UUID.randomUUID().toString(),
lockDir = context.cacheDir,
processLock = false
)
private var opened = false
fun getSupportDatabase(writable: Boolean): SupportSQLiteDatabase {
return try {
lock.lock(!opened && databaseName != null)
migrated = false
val db = innerGetDatabase(writable)
if (migrated) {
// there might be a connection w/ stale structure, we should re-open.
close()
return getSupportDatabase(writable)
}
getWrappedDb(db)
} finally {
lock.unlock()
}
}
private fun innerGetDatabase(writable: Boolean): SQLiteDatabase {
val name = databaseName
val isOpen = opened
if (name != null && !isOpen) {
val databaseFile = context.getDatabasePath(name)
val parentFile = databaseFile.parentFile
if (parentFile != null) {
parentFile.mkdirs()
if (!parentFile.isDirectory) {
Log.w(TAG, "Invalid database parent file, not a directory: $parentFile")
}
}
}
try {
return getWritableOrReadableDatabase(writable)
} catch (t: Throwable) {
// No good, just try again...
super.close()
}
try {
// Wait before trying to open the DB, ideally enough to account for some slow I/O.
// Similar to android_database_SQLiteConnection's BUSY_TIMEOUT_MS but not as much.
Thread.sleep(500)
} catch (e: InterruptedException) {
// Ignore, and continue
}
val openRetryError: Throwable = try {
return getWritableOrReadableDatabase(writable)
} catch (t: Throwable) {
super.close()
t
}
if (openRetryError is CallbackException) {
// Callback error (onCreate, onUpgrade, onOpen, etc), possibly user error.
val cause = openRetryError.cause
when (openRetryError.callbackName) {
CallbackName.ON_CONFIGURE,
CallbackName.ON_CREATE,
CallbackName.ON_UPGRADE,
CallbackName.ON_DOWNGRADE -> throw cause
CallbackName.ON_OPEN -> {}
}
// If callback exception is not an SQLiteException, then more certainly it is not
// recoverable.
if (cause !is SQLiteException) {
throw cause
}
} else if (openRetryError is SQLiteException) {
// Ideally we are looking for SQLiteCantOpenDatabaseException and similar, but
// corruption can manifest in others forms.
if (name == null || !allowDataLossOnRecovery) {
throw openRetryError
}
} else {
throw openRetryError
}
// Delete the database and try one last time. (mAllowDataLossOnRecovery == true)
context.deleteDatabase(name)
try {
return getWritableOrReadableDatabase(writable)
} catch (ex: CallbackException) {
// Unwrap our exception to avoid disruption with other try-catch in the call stack.
throw ex.cause
}
}
private fun getWritableOrReadableDatabase(writable: Boolean): SQLiteDatabase {
return if (writable) {
super.getWritableDatabase()
} else {
super.getReadableDatabase()
}
}
fun getWrappedDb(sqLiteDatabase: SQLiteDatabase): FrameworkSQLiteDatabase {
return getWrappedDb(dbRef, sqLiteDatabase)
}
override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
try {
callback.onCreate(getWrappedDb(sqLiteDatabase))
} catch (t: Throwable) {
throw CallbackException(CallbackName.ON_CREATE, t)
}
}
override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
migrated = true
try {
callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion)
} catch (t: Throwable) {
throw CallbackException(CallbackName.ON_UPGRADE, t)
}
}
override fun onConfigure(db: SQLiteDatabase) {
if (!migrated && callback.version != db.version) {
// Reduce the prepared statement cache to the minimum allowed (1) to avoid
// issues with queries executed during migrations. Note that when a migration is
// done the connection is closed and re-opened to avoid stale connections, which
// in turns resets the cache max size. See b/271083856
db.setMaxSqlCacheSize(1)
}
try {
callback.onConfigure(getWrappedDb(db))
} catch (t: Throwable) {
throw CallbackException(CallbackName.ON_CONFIGURE, t)
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
migrated = true
try {
callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion)
} catch (t: Throwable) {
throw CallbackException(CallbackName.ON_DOWNGRADE, t)
}
}
override fun onOpen(db: SQLiteDatabase) {
if (!migrated) {
// if we've migrated, we'll re-open the db so we should not call the callback.
try {
callback.onOpen(getWrappedDb(db))
} catch (t: Throwable) {
throw CallbackException(CallbackName.ON_OPEN, t)
}
}
opened = true
}
// No need sync due to locks.
override fun close() {
try {
lock.lock()
super.close()
dbRef.db = null
opened = false
} finally {
lock.unlock()
}
}
private class CallbackException(
val callbackName: CallbackName,
override val cause: Throwable
) : RuntimeException(cause)
internal enum class CallbackName {
ON_CONFIGURE, ON_CREATE, ON_UPGRADE, ON_DOWNGRADE, ON_OPEN
}
companion object {
fun getWrappedDb(
refHolder: DBRefHolder,
sqLiteDatabase: SQLiteDatabase
): FrameworkSQLiteDatabase {
val dbRef = refHolder.db
return if (dbRef == null || !dbRef.isDelegate(sqLiteDatabase)) {
FrameworkSQLiteDatabase(sqLiteDatabase).also { refHolder.db = it }
} else {
dbRef
}
}
}
}
companion object {
private const val TAG = "SupportSQLite"
}
/**
* This is used as an Object reference so that we can access the wrapped database inside
* the constructor. SQLiteOpenHelper requires the error handler to be passed in the
* constructor.
*/
private class DBRefHolder(var db: FrameworkSQLiteDatabase?)
}

@ -0,0 +1,36 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db.framework
import androidx.sqlite.db.SupportSQLiteOpenHelper
/**
* Implements [SupportSQLiteOpenHelper.Factory] using the SQLite implementation in the
* framework.
*/
class FrameworkSQLiteOpenHelperFactory : SupportSQLiteOpenHelper.Factory {
override fun create(
configuration: SupportSQLiteOpenHelper.Configuration
): SupportSQLiteOpenHelper {
return FrameworkSQLiteOpenHelper(
configuration.context,
configuration.name,
configuration.callback,
configuration.useNoBackupDirectory,
configuration.allowDataLossOnRecovery
)
}
}

@ -0,0 +1,54 @@
/*
* Copyright (C) 2017 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 androidx.sqlite.db.framework
import android.database.sqlite.SQLiteProgram
import androidx.sqlite.db.SupportSQLiteProgram
/**
* An wrapper around [SQLiteProgram] to implement [SupportSQLiteProgram] API.
*/
internal open class FrameworkSQLiteProgram(
private val delegate: SQLiteProgram
) : SupportSQLiteProgram {
override fun bindNull(index: Int) {
delegate.bindNull(index)
}
override fun bindLong(index: Int, value: Long) {
delegate.bindLong(index, value)
}
override fun bindDouble(index: Int, value: Double) {
delegate.bindDouble(index, value)
}
override fun bindString(index: Int, value: String) {
delegate.bindString(index, value)
}
override fun bindBlob(index: Int, value: ByteArray) {
delegate.bindBlob(index, value)
}
override fun clearBindings() {
delegate.clearBindings()
}
override fun close() {
delegate.close()
}
}

@ -0,0 +1,52 @@
/*
* Copyright (C) 2016 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 androidx.sqlite.db.framework
import android.database.sqlite.SQLiteStatement
import androidx.sqlite.db.SupportSQLiteStatement
/**
* Delegates all calls to a [SQLiteStatement].
*
* @constructor Creates a wrapper around a framework [SQLiteStatement].
*
* @param delegate The SQLiteStatement to delegate calls to.
*/
internal class FrameworkSQLiteStatement(
private val delegate: SQLiteStatement
) : FrameworkSQLiteProgram(
delegate
), SupportSQLiteStatement {
override fun execute() {
delegate.execute()
}
override fun executeUpdateDelete(): Int {
return delegate.executeUpdateDelete()
}
override fun executeInsert(): Long {
return delegate.executeInsert()
}
override fun simpleQueryForLong(): Long {
return delegate.simpleQueryForLong()
}
override fun simpleQueryForString(): String? {
return delegate.simpleQueryForString()
}
}

@ -0,0 +1,102 @@
/*
* Copyright 2019 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 androidx.sqlite.util
import android.util.Log
import androidx.annotation.RestrictTo
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.nio.channels.FileChannel
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
/**
* Utility class for in-process and multi-process key-based lock mechanism for safely doing
* synchronized operations.
*
* Acquiring the lock will be quick if no other thread or process has a lock with the same key.
* But if the lock is already held then acquiring it will block, until the other thread or process
* releases the lock. Note that the key and lock directory must be the same to achieve
* synchronization.
*
* Locking is done via two levels:
*
* 1. Thread locking within the same JVM process is done via a map of String key to ReentrantLock
* objects.
*
* 2. Multi-process locking is done via a lock file whose name contains the key and FileLock
* objects.
*
* Creates a lock with `name` and using `lockDir` as the directory for the
* lock files.
*
* @param name the name of this lock.
* @param lockDir the directory where the lock files will be located.
* @param processLock whether to use file for process level locking or not by default. The
* behaviour can be overridden via the [lock] method.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ProcessLock(
name: String,
lockDir: File?,
private val processLock: Boolean
) {
private val lockFile: File? = lockDir?.let { File(it, "$name.lck") }
private val threadLock: Lock = getThreadLock(name)
private var lockChannel: FileChannel? = null
/**
* Attempts to grab the lock, blocking if already held by another thread or process.
*
* @param [processLock] whether to use file for process level locking or not.
*/
fun lock(processLock: Boolean = this.processLock) {
threadLock.lock()
if (processLock) {
try {
if (lockFile == null) {
throw IOException("No lock directory was provided.")
}
// Verify parent dir
val parentDir = lockFile.parentFile
parentDir?.mkdirs()
lockChannel = FileOutputStream(lockFile).channel.apply { lock() }
} catch (e: IOException) {
lockChannel = null
Log.w(TAG, "Unable to grab file lock.", e)
}
}
}
/**
* Releases the lock.
*/
fun unlock() {
try {
lockChannel?.close()
} catch (ignored: IOException) { }
threadLock.unlock()
}
companion object {
private const val TAG = "SupportSQLiteLock"
// in-process lock map
private val threadLocksMap: MutableMap<String, Lock> = HashMap()
private fun getThreadLock(key: String): Lock = synchronized(threadLocksMap) {
return threadLocksMap.getOrPut(key) { ReentrantLock() }
}
}
}
Loading…
Cancel
Save