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