blob: d664342cf1c5e611d82a68fb0e3e81e6c6ec80f2 [file] [log] [blame]
/*
* 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.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.util.Log
import android.util.Pair
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.
*/
fun setWriteAheadLoggingEnabled(enabled: Boolean)
/**
* Create and/or open a database that will be used for reading and writing.
* The first time this is called, the database will be opened and
* [Callback.onCreate], [Callback.onUpgrade] and/or [Callback.onOpen] will be
* called.
*
* Once opened successfully, the database is cached, so you can
* call this method every time you need to write to the database.
* (Make sure to call [close] when you no longer need the database.)
* Errors such as bad permissions or a full disk may cause this method
* to fail, but future attempts may succeed if the problem is fixed.
*
* Database upgrade may take a long time, you
* should not call this method from the application main thread, including
* from [ContentProvider.onCreate()].
*
* @return a read/write database object valid until [close] is called
* @throws SQLiteException if the database cannot be opened for writing
*/
val writableDatabase: SupportSQLiteDatabase
/**
* Create and/or open a database. This will be the same object returned by
* [writableDatabase] unless some problem, such as a full disk,
* requires the database to be opened read-only. In that case, a read-only
* database object will be returned. If the problem is fixed, a future call
* to [writableDatabase] may succeed, in which case the read-only
* database object will be closed and the read/write object will be returned
* in the future.
*
* Like [writableDatabase], this method may
* take a long time to return, so you should not call it from the
* application main thread, including from
* [ContentProvider.onCreate()].
*
* @return a database object valid until [writableDatabase]
* or [close] is called.
* @throws SQLiteException if the database cannot be opened
*/
val readableDatabase: SupportSQLiteDatabase
/**
* Close any open database object.
*/
override fun close()
/**
* Creates a new Callback to get database lifecycle events.
*
* Handles various lifecycle events for the SQLite connection, similar to
* [room-runtime.SQLiteOpenHelper].
*/
abstract class Callback(
/**
* Version number of the database (starting at 1); if the database is older,
* [Callback.onUpgrade] will be used to upgrade the database; if the database is newer,
* [Callback.onDowngrade] will be used to downgrade the database.
*/
@JvmField
val version: Int
) {
/**
* Called when the database connection is being configured, to enable features such as
* write-ahead logging or foreign key support.
*
* This method is called before [onCreate], [onUpgrade], [onDowngrade],
* or [onOpen] are called. It should not modify the database except to configure the
* database connection as required.
*
* This method should only call methods that configure the parameters of the database
* connection, such as [SupportSQLiteDatabase.enableWriteAheadLogging]
* [SupportSQLiteDatabase.setForeignKeyConstraintsEnabled],
* [SupportSQLiteDatabase.setLocale],
* [SupportSQLiteDatabase.setMaximumSize], or executing PRAGMA statements.
*
* @param db The database.
*/
open fun onConfigure(db: SupportSQLiteDatabase) {}
/**
* Called when the database is created for the first time. This is where the
* creation of tables and the initial population of the tables should happen.
*
* @param db The database.
*/
abstract fun onCreate(db: SupportSQLiteDatabase)
/**
* Called when the database needs to be upgraded. The implementation
* should use this method to drop tables, add tables, or do anything else it
* needs to upgrade to the new schema version.
*
* The SQLite ALTER TABLE documentation can be found
* [here](http://sqlite.org/lang_altertable.html). If you add new columns
* you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
* you can use ALTER TABLE to rename the old table, then create the new table and then
* populate the new table with the contents of the old table.
*
* This method executes within a transaction. If an exception is thrown, all changes
* will automatically be rolled back.
*
* @param db The database.
* @param oldVersion The old database version.
* @param newVersion The new database version.
*/
abstract fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
)
/**
* Called when the database needs to be downgraded. This is strictly similar to
* [onUpgrade] method, but is called whenever current version is newer than requested
* one.
* However, this method is not abstract, so it is not mandatory for a customer to
* implement it. If not overridden, default implementation will reject downgrade and
* throws SQLiteException
*
* This method executes within a transaction. If an exception is thrown, all changes
* will automatically be rolled back.
*
* @param db The database.
* @param oldVersion The old database version.
* @param newVersion The new database version.
*/
open fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
throw SQLiteException(
"Can't downgrade database from version $oldVersion to $newVersion"
)
}
/**
* Called when the database has been opened. The implementation
* should check [SupportSQLiteDatabase.isReadOnly] before updating the
* database.
*
* This method is called after the database connection has been configured
* and after the database schema has been created, upgraded or downgraded as necessary.
* If the database connection must be configured in some way before the schema
* is created, upgraded, or downgraded, do it in [onConfigure] instead.
*
* @param db The database.
*/
open fun onOpen(db: SupportSQLiteDatabase) {}
/**
* The method invoked when database corruption is detected. Default implementation will
* delete the database file.
*
* @param db the [SupportSQLiteDatabase] object representing the database on which
* corruption is detected.
*/
open fun onCorruption(db: SupportSQLiteDatabase) {
// the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
Log.e(TAG, "Corruption reported by sqlite on database: $db.path")
// is the corruption detected even before database could be 'opened'?
if (!db.isOpen) {
// database files are not even openable. delete this database file.
// NOTE if the database has attached databases, then any of them could be corrupt.
// and not deleting all of them could cause corrupted database file to remain and
// make the application crash on database open operation. To avoid this problem,
// the application should provide its own {@link DatabaseErrorHandler} impl class
// to delete ALL files of the database (including the attached databases).
db.path?.let { deleteDatabaseFile(it) }
return
}
var attachedDbs: List<Pair<String, String>>? = null
try {
// Close the database, which will cause subsequent operations to fail.
// before that, get the attached database list first.
try {
attachedDbs = db.attachedDbs
} catch (e: SQLiteException) {
/* ignore */
}
try {
db.close()
} catch (e: IOException) {
/* ignore */
}
} finally {
// Delete all files of this corrupt database and/or attached databases
// attachedDbs = null is possible when the database is so corrupt that even
// "PRAGMA database_list;" also fails. delete the main database file
attachedDbs?.forEach { p ->
deleteDatabaseFile(p.second)
} ?: db.path?.let { deleteDatabaseFile(it) }
}
}
private fun deleteDatabaseFile(fileName: String) {
if (fileName.equals(":memory:", ignoreCase = true) ||
fileName.trim { it <= ' ' }.isEmpty()
) {
return
}
Log.w(TAG, "deleting the database file: $fileName")
try {
SQLiteDatabase.deleteDatabase(File(fileName))
} 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
}
}