1 /* <lambda>null2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package androidx.sqlite.db 17 18 import android.content.Context 19 import android.database.sqlite.SQLiteDatabase 20 import android.database.sqlite.SQLiteException 21 import android.util.Log 22 import android.util.Pair 23 import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback 24 import androidx.sqlite.db.SupportSQLiteOpenHelper.Factory 25 import java.io.Closeable 26 import java.io.File 27 import java.io.IOException 28 29 /** 30 * An interface to map the behavior of [android.database.sqlite.SQLiteOpenHelper]. Note that since 31 * that class requires overriding certain methods, support implementation uses [Factory.create] to 32 * create this and [Callback] to implement the methods that should be overridden. 33 */ 34 public interface SupportSQLiteOpenHelper : Closeable { 35 /** 36 * Return the name of the SQLite database being opened, as given to the constructor. `null` 37 * indicates an in-memory database. 38 */ 39 public val databaseName: String? 40 41 /** 42 * Enables or disables the use of write-ahead logging for the database. 43 * 44 * See [SupportSQLiteDatabase.enableWriteAheadLogging] for details. 45 * 46 * Write-ahead logging cannot be used with read-only databases so the value of this flag is 47 * ignored if the database is opened read-only. 48 * 49 * @param enabled True if write-ahead logging should be enabled, false if it should be disabled. 50 */ 51 public fun setWriteAheadLoggingEnabled(enabled: Boolean) 52 53 /** 54 * Create and/or open a database that will be used for reading and writing. The first time this 55 * is called, the database will be opened and [Callback.onCreate], [Callback.onUpgrade] and/or 56 * [Callback.onOpen] will be called. 57 * 58 * Once opened successfully, the database is cached, so you can call this method every time you 59 * need to write to the database. (Make sure to call [close] when you no longer need the 60 * database.) Errors such as bad permissions or a full disk may cause this method to fail, but 61 * future attempts may succeed if the problem is fixed. 62 * 63 * Database upgrade may take a long time, you should not call this method from the application 64 * main thread, including from [ContentProvider.onCreate()]. 65 * 66 * @return a read/write database object valid until [close] is called 67 * @throws SQLiteException if the database cannot be opened for writing 68 */ 69 public val writableDatabase: SupportSQLiteDatabase 70 71 /** 72 * Create and/or open a database. This will be the same object returned by [writableDatabase] 73 * unless some problem, such as a full disk, requires the database to be opened read-only. In 74 * that case, a read-only database object will be returned. If the problem is fixed, a future 75 * call to [writableDatabase] may succeed, in which case the read-only database object will be 76 * closed and the read/write object will be returned in the future. 77 * 78 * Like [writableDatabase], this method may take a long time to return, so you should not call 79 * it from the application main thread, including from [ContentProvider.onCreate()]. 80 * 81 * @return a database object valid until [writableDatabase] or [close] is called. 82 * @throws SQLiteException if the database cannot be opened 83 */ 84 public val readableDatabase: SupportSQLiteDatabase 85 86 /** Close any open database object. */ 87 override fun close() 88 89 /** 90 * Creates a new Callback to get database lifecycle events. 91 * 92 * Handles various lifecycle events for the SQLite connection, similar to 93 * [room-runtime.SQLiteOpenHelper]. 94 */ 95 public abstract class Callback( 96 /** 97 * Version number of the database (starting at 1); if the database is older, 98 * [Callback.onUpgrade] will be used to upgrade the database; if the database is newer, 99 * [Callback.onDowngrade] will be used to downgrade the database. 100 */ 101 @JvmField public val version: Int 102 ) { 103 /** 104 * Called when the database connection is being configured, to enable features such as 105 * write-ahead logging or foreign key support. 106 * 107 * This method is called before [onCreate], [onUpgrade], [onDowngrade], or [onOpen] are 108 * called. It should not modify the database except to configure the database connection as 109 * required. 110 * 111 * This method should only call methods that configure the parameters of the database 112 * connection, such as [SupportSQLiteDatabase.enableWriteAheadLogging] 113 * [SupportSQLiteDatabase.setForeignKeyConstraintsEnabled], 114 * [SupportSQLiteDatabase.setLocale], [SupportSQLiteDatabase.setMaximumSize], or executing 115 * PRAGMA statements. 116 * 117 * @param db The database. 118 */ 119 public open fun onConfigure(db: SupportSQLiteDatabase) {} 120 121 /** 122 * Called when the database is created for the first time. This is where the creation of 123 * tables and the initial population of the tables should happen. 124 * 125 * @param db The database. 126 */ 127 public abstract fun onCreate(db: SupportSQLiteDatabase) 128 129 /** 130 * Called when the database needs to be upgraded. The implementation should use this method 131 * to drop tables, add tables, or do anything else it needs to upgrade to the new schema 132 * version. 133 * 134 * The SQLite ALTER TABLE documentation can be found 135 * [here](http://sqlite.org/lang_altertable.html). If you add new columns you can use ALTER 136 * TABLE to insert them into a live table. If you rename or remove columns you can use ALTER 137 * TABLE to rename the old table, then create the new table and then populate the new table 138 * with the contents of the old table. 139 * 140 * This method executes within a transaction. If an exception is thrown, all changes will 141 * automatically be rolled back. 142 * 143 * @param db The database. 144 * @param oldVersion The old database version. 145 * @param newVersion The new database version. 146 */ 147 public abstract fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) 148 149 /** 150 * Called when the database needs to be downgraded. This is strictly similar to [onUpgrade] 151 * method, but is called whenever current version is newer than requested one. However, this 152 * method is not abstract, so it is not mandatory for a customer to implement it. If not 153 * overridden, default implementation will reject downgrade and throws SQLiteException 154 * 155 * This method executes within a transaction. If an exception is thrown, all changes will 156 * automatically be rolled back. 157 * 158 * @param db The database. 159 * @param oldVersion The old database version. 160 * @param newVersion The new database version. 161 */ 162 public open fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { 163 throw SQLiteException( 164 "Can't downgrade database from version $oldVersion to $newVersion" 165 ) 166 } 167 168 /** 169 * Called when the database has been opened. The implementation should check 170 * [SupportSQLiteDatabase.isReadOnly] before updating the database. 171 * 172 * This method is called after the database connection has been configured and after the 173 * database schema has been created, upgraded or downgraded as necessary. If the database 174 * connection must be configured in some way before the schema is created, upgraded, or 175 * downgraded, do it in [onConfigure] instead. 176 * 177 * @param db The database. 178 */ 179 public open fun onOpen(db: SupportSQLiteDatabase) {} 180 181 /** 182 * The method invoked when database corruption is detected. Default implementation will 183 * delete the database file. 184 * 185 * @param db the [SupportSQLiteDatabase] object representing the database on which 186 * corruption is detected. 187 */ 188 public open fun onCorruption(db: SupportSQLiteDatabase) { 189 // the following implementation is taken from {@link DefaultDatabaseErrorHandler}. 190 Log.e(TAG, "Corruption reported by sqlite on database: $db.path") 191 // is the corruption detected even before database could be 'opened'? 192 if (!db.isOpen) { 193 // database files are not even openable. delete this database file. 194 // NOTE if the database has attached databases, then any of them could be corrupt. 195 // and not deleting all of them could cause corrupted database file to remain and 196 // make the application crash on database open operation. To avoid this problem, 197 // the application should provide its own {@link DatabaseErrorHandler} impl class 198 // to delete ALL files of the database (including the attached databases). 199 db.path?.let { deleteDatabaseFile(it) } 200 return 201 } 202 var attachedDbs: List<Pair<String, String>>? = null 203 try { 204 // Close the database, which will cause subsequent operations to fail. 205 // before that, get the attached database list first. 206 try { 207 attachedDbs = db.attachedDbs 208 } catch (e: SQLiteException) { 209 /* ignore */ 210 } 211 try { 212 db.close() 213 } catch (e: IOException) { 214 /* ignore */ 215 } 216 } finally { 217 // Delete all files of this corrupt database and/or attached databases 218 // attachedDbs = null is possible when the database is so corrupt that even 219 // "PRAGMA database_list;" also fails. delete the main database file 220 attachedDbs?.forEach { p -> deleteDatabaseFile(p.second) } 221 ?: db.path?.let { deleteDatabaseFile(it) } 222 } 223 } 224 225 private fun deleteDatabaseFile(fileName: String) { 226 if ( 227 fileName.equals(":memory:", ignoreCase = true) || 228 fileName.trim { it <= ' ' }.isEmpty() 229 ) { 230 return 231 } 232 Log.w(TAG, "deleting the database file: $fileName") 233 try { 234 SQLiteDatabase.deleteDatabase(File(fileName)) 235 } catch (e: Exception) { 236 /* print warning and ignore exception */ 237 Log.w(TAG, "delete failed: ", e) 238 } 239 } 240 241 internal companion object { 242 private const val TAG = "SupportSQLite" 243 } 244 } 245 246 /** The configuration to create an SQLite open helper object using [Factory]. */ 247 public class Configuration 248 @Suppress("ExecutorRegistration") // For backwards compatibility 249 constructor( 250 /** Context to use to open or create the database. */ 251 @JvmField public val context: Context, 252 /** Name of the database file, or null for an in-memory database. */ 253 @JvmField public val name: String?, 254 /** The callback class to handle creation, upgrade and downgrade. */ 255 @JvmField public val callback: Callback, 256 /** If `true` the database will be stored in the no-backup directory. */ 257 @JvmField @Suppress("ListenerLast") public val useNoBackupDirectory: Boolean = false, 258 /** 259 * If `true` the database will be delete and its data loss in the case that it cannot be 260 * opened. 261 */ 262 @JvmField @Suppress("ListenerLast") public val allowDataLossOnRecovery: Boolean = false 263 ) { 264 265 /** Builder class for [Configuration]. */ 266 public open class Builder internal constructor(context: Context) { 267 private val context: Context 268 private var name: String? = null 269 private var callback: Callback? = null 270 private var useNoBackupDirectory = false 271 private var allowDataLossOnRecovery = false 272 273 /** 274 * Throws an [IllegalArgumentException] if the [Callback] is `null`. 275 * 276 * Throws an [IllegalArgumentException] if the [Context] is `null`. 277 * 278 * Throws an [IllegalArgumentException] if the [String] database name is `null`. 279 * [Context.getNoBackupFilesDir] 280 * 281 * @return The [Configuration] instance 282 */ 283 public open fun build(): Configuration { 284 val callback = callback 285 requireNotNull(callback) { "Must set a callback to create the configuration." } 286 require(!useNoBackupDirectory || !name.isNullOrEmpty()) { 287 "Must set a non-null database name to a configuration that uses the " + 288 "no backup directory." 289 } 290 return Configuration( 291 context, 292 name, 293 callback, 294 useNoBackupDirectory, 295 allowDataLossOnRecovery 296 ) 297 } 298 299 init { 300 this.context = context 301 } 302 303 /** 304 * @param name Name of the database file, or null for an in-memory database. 305 * @return This builder instance. 306 */ 307 public open fun name(name: String?): Builder = apply { this.name = name } 308 309 /** 310 * @param callback The callback class to handle creation, upgrade and downgrade. 311 * @return This builder instance. 312 */ 313 public open fun callback(callback: Callback): Builder = apply { 314 this.callback = callback 315 } 316 317 /** 318 * Sets whether to use a no backup directory or not. 319 * 320 * @param useNoBackupDirectory If `true` the database file will be stored in the 321 * no-backup directory. 322 * @return This builder instance. 323 */ 324 public open fun noBackupDirectory(useNoBackupDirectory: Boolean): Builder = apply { 325 this.useNoBackupDirectory = useNoBackupDirectory 326 } 327 328 /** 329 * Sets whether to delete and recreate the database file in situations when the database 330 * file cannot be opened, thus allowing for its data to be lost. 331 * 332 * @param allowDataLossOnRecovery If `true` the database file might be recreated in the 333 * case that it cannot be opened. 334 * @return this 335 */ 336 public open fun allowDataLossOnRecovery(allowDataLossOnRecovery: Boolean): Builder = 337 apply { 338 this.allowDataLossOnRecovery = allowDataLossOnRecovery 339 } 340 } 341 342 public companion object { 343 /** 344 * Creates a new Configuration.Builder to create an instance of Configuration. 345 * 346 * @param context to use to open or create the database. 347 */ 348 @JvmStatic 349 public fun builder(context: Context): Builder { 350 return Builder(context) 351 } 352 } 353 } 354 355 /** Factory class to create instances of [SupportSQLiteOpenHelper] using [Configuration]. */ 356 public fun interface Factory { 357 /** 358 * Creates an instance of [SupportSQLiteOpenHelper] using the given configuration. 359 * 360 * @param configuration The configuration to use while creating the open helper. 361 * @return A SupportSQLiteOpenHelper which can be used to open a database. 362 */ 363 public fun create(configuration: Configuration): SupportSQLiteOpenHelper 364 } 365 } 366