diff --git a/app/build.gradle b/app/build.gradle index c3ddf069da..04725d0605 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java index 03e68d4ec4..548720d140 100644 --- a/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java +++ b/app/src/main/java/androidx/room/AutoClosingRoomOpenHelper.java @@ -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) { + } } /** diff --git a/app/src/main/java/androidx/room/QueryInterceptorDatabase.java b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java index c5ef6bc4f9..386b583587 100644 --- a/app/src/main/java/androidx/room/QueryInterceptorDatabase.java +++ b/app/src/main/java/androidx/room/QueryInterceptorDatabase.java @@ -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) { + } } diff --git a/app/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt b/app/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt new file mode 100644 index 0000000000..eee04ba523 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SimpleSQLiteQuery.kt @@ -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? + ) : 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? + ) { + 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" + ) + } + } + } +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt new file mode 100644 index 0000000000..36c2f875fa --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteCompat.kt @@ -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, + 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 + ) { + 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 { + return cursor.notificationUris!! + } + } +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt new file mode 100644 index 0000000000..d2fa862125 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt @@ -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? + ) { + 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): 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?): 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? + ): 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) + + /** + * 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>? + + /** + * Is true if the given database (and all its attached databases) pass integrity_check, + * false otherwise. + */ + val isDatabaseIntegrityOk: Boolean +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt new file mode 100644 index 0000000000..070dd70b0c --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt @@ -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>? = 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 + } +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt new file mode 100644 index 0000000000..12a68e7f09 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteProgram.kt @@ -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() +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt new file mode 100644 index 0000000000..6e9a598834 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteQuery.kt @@ -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 +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt new file mode 100644 index 0000000000..3ed9087f44 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt @@ -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? = null + private var selection: String? = null + private var bindArgs: Array? = 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?): 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? + ): 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) { + 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) + } + } +} diff --git a/app/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt b/app/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt new file mode 100644 index 0000000000..32fc2feff9 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/SupportSQLiteStatement.kt @@ -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? +} diff --git a/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt new file mode 100644 index 0000000000..886f90b8df --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt @@ -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?) { + 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): 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?): 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? + ): 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(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) { + 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>? + 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? + ) { + 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(0) + } +} diff --git a/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt new file mode 100644 index 0000000000..21ea6a0b86 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt @@ -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?) +} diff --git a/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.kt b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.kt new file mode 100644 index 0000000000..19e1677c0e --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.kt b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.kt new file mode 100644 index 0000000000..e84d133c79 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteProgram.kt @@ -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() + } +} diff --git a/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.kt b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.kt new file mode 100644 index 0000000000..866bdaf683 --- /dev/null +++ b/app/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteStatement.kt @@ -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() + } +} diff --git a/app/src/main/java/androidx/sqlite/util/ProcessLock.kt b/app/src/main/java/androidx/sqlite/util/ProcessLock.kt new file mode 100644 index 0000000000..eb2b4e7dde --- /dev/null +++ b/app/src/main/java/androidx/sqlite/util/ProcessLock.kt @@ -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 = HashMap() + private fun getThreadLock(key: String): Lock = synchronized(threadLocksMap) { + return threadLocksMap.getOrPut(key) { ReentrantLock() } + } + } +}