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.framework
17 
18 import android.content.Context
19 import android.database.DatabaseErrorHandler
20 import android.database.sqlite.SQLiteDatabase
21 import android.database.sqlite.SQLiteException
22 import android.database.sqlite.SQLiteOpenHelper
23 import android.os.Build
24 import android.util.Log
25 import androidx.sqlite.db.SupportSQLiteCompat
26 import androidx.sqlite.db.SupportSQLiteDatabase
27 import androidx.sqlite.db.SupportSQLiteOpenHelper
28 import androidx.sqlite.util.ProcessLock
29 import java.io.File
30 import java.util.UUID
31 
32 internal class FrameworkSQLiteOpenHelper
33 @JvmOverloads
34 constructor(
35     private val context: Context,
36     private val name: String?,
37     private val callback: SupportSQLiteOpenHelper.Callback,
38     private val useNoBackupDirectory: Boolean = false,
39     private val allowDataLossOnRecovery: Boolean = false
40 ) : SupportSQLiteOpenHelper {
41 
42     // Delegate is created lazily
43     private val lazyDelegate = lazy {
44         // OpenHelper initialization code
45         val openHelper: OpenHelper
46 
47         if (
48             Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && name != null && useNoBackupDirectory
49         ) {
50             val file = File(SupportSQLiteCompat.Api21Impl.getNoBackupFilesDir(context), name)
51             openHelper =
52                 OpenHelper(
53                     context = context,
54                     name = file.absolutePath,
55                     dbRef = DBRefHolder(null),
56                     callback = callback,
57                     allowDataLossOnRecovery = allowDataLossOnRecovery
58                 )
59         } else {
60             openHelper =
61                 OpenHelper(
62                     context = context,
63                     name = name,
64                     dbRef = DBRefHolder(null),
65                     callback = callback,
66                     allowDataLossOnRecovery = allowDataLossOnRecovery
67                 )
68         }
69         openHelper.setWriteAheadLoggingEnabled(writeAheadLoggingEnabled)
70         return@lazy openHelper
71     }
72 
73     private var writeAheadLoggingEnabled = false
74 
75     // getDelegate() is lazy because we don't want to File I/O until the call to
76     // getReadableDatabase() or getWritableDatabase(). This is better because the call to
77     // a getReadableDatabase() or a getWritableDatabase() happens on a background thread unless
78     // queries are allowed on the main thread.
79 
80     // We defer computing the path the database from the constructor to getDelegate()
81     // because context.getNoBackupFilesDir() does File I/O :(
82     private val delegate: OpenHelper by lazyDelegate
83 
84     override val databaseName: String?
85         get() = name
86 
87     override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
88         if (lazyDelegate.isInitialized()) {
89             // Use 'delegate', it is already initialized
90             delegate.setWriteAheadLoggingEnabled(enabled)
91         }
92         writeAheadLoggingEnabled = enabled
93     }
94 
95     override val writableDatabase: SupportSQLiteDatabase
96         get() = delegate.getSupportDatabase(true)
97 
98     override val readableDatabase: SupportSQLiteDatabase
99         get() = delegate.getSupportDatabase(false)
100 
101     override fun close() {
102         if (lazyDelegate.isInitialized()) {
103             delegate.close()
104         }
105     }
106 
107     private class OpenHelper(
108         val context: Context,
109         name: String?,
110         /**
111          * This is used as an Object reference so that we can access the wrapped database inside the
112          * constructor. SQLiteOpenHelper requires the error handler to be passed in the constructor.
113          */
114         val dbRef: DBRefHolder,
115         val callback: SupportSQLiteOpenHelper.Callback,
116         val allowDataLossOnRecovery: Boolean
117     ) :
118         SQLiteOpenHelper(
119             context,
120             name,
121             null,
122             callback.version,
123             DatabaseErrorHandler { dbObj -> callback.onCorruption(getWrappedDb(dbRef, dbObj)) }
124         ) {
125         // see b/78359448
126         private var migrated = false
127 
128         // see b/193182592
129         private val lock: ProcessLock =
130             ProcessLock(
131                 name = name ?: UUID.randomUUID().toString(),
132                 lockDir = context.cacheDir,
133                 processLock = false
134             )
135         private var opened = false
136 
137         fun getSupportDatabase(writable: Boolean): SupportSQLiteDatabase {
138             return try {
139                 lock.lock(!opened && databaseName != null)
140                 migrated = false
141                 val db = innerGetDatabase(writable)
142                 if (migrated) {
143                     // there might be a connection w/ stale structure, we should re-open.
144                     close()
145                     return getSupportDatabase(writable)
146                 }
147                 getWrappedDb(db)
148             } finally {
149                 lock.unlock()
150             }
151         }
152 
153         private fun innerGetDatabase(writable: Boolean): SQLiteDatabase {
154             val name = databaseName
155             val isOpen = opened
156             if (name != null && !isOpen) {
157                 val databaseFile = context.getDatabasePath(name)
158                 val parentFile = databaseFile.parentFile
159                 if (parentFile != null) {
160                     parentFile.mkdirs()
161                     if (!parentFile.isDirectory) {
162                         Log.w(TAG, "Invalid database parent file, not a directory: $parentFile")
163                     }
164                 }
165             }
166             try {
167                 return getWritableOrReadableDatabase(writable)
168             } catch (t: Throwable) {
169                 // No good, just try again...
170             }
171             try {
172                 // Wait before trying to open the DB, ideally enough to account for some slow I/O.
173                 // Similar to android_database_SQLiteConnection's BUSY_TIMEOUT_MS but not as much.
174                 Thread.sleep(500)
175             } catch (e: InterruptedException) {
176                 // Ignore, and continue
177             }
178             var openRetryError: Throwable =
179                 try {
180                     return getWritableOrReadableDatabase(writable)
181                 } catch (t: Throwable) {
182                     t
183                 }
184 
185             // Callback error (onCreate, onUpgrade, onOpen, etc), possibly user error.
186             if (openRetryError is CallbackException) {
187                 val cause = openRetryError.cause
188                 when (openRetryError.callbackName) {
189                     CallbackName.ON_CONFIGURE,
190                     CallbackName.ON_CREATE,
191                     CallbackName.ON_UPGRADE,
192                     CallbackName.ON_DOWNGRADE -> throw cause
193                     CallbackName.ON_OPEN -> {}
194                 }
195                 // If callback exception is not an SQLiteException, then more certainly it is not
196                 // recoverable, rethrow.
197                 if (cause !is SQLiteException) {
198                     throw cause
199                 }
200                 // Exception in callback is a SQLiteException, might be recoverable.
201                 openRetryError = cause
202             }
203 
204             // Ideally we are looking for SQLiteCantOpenDatabaseException and similar, but
205             // corruption can manifest in others forms so check if error is SQLiteException as
206             // that might be recoverable, unless it's an in-memory database or data loss is not
207             // allowed.
208             if (openRetryError !is SQLiteException || name == null || !allowDataLossOnRecovery) {
209                 throw openRetryError
210             }
211 
212             // Delete the database and try one last time. (mAllowDataLossOnRecovery == true)
213             context.deleteDatabase(name)
214             try {
215                 return getWritableOrReadableDatabase(writable)
216             } catch (ex: CallbackException) {
217                 // Unwrap our exception to avoid disruption with other try-catch in the call stack.
218                 throw ex.cause
219             }
220         }
221 
222         private fun getWritableOrReadableDatabase(writable: Boolean): SQLiteDatabase {
223             return if (writable) {
224                 super.getWritableDatabase()
225             } else {
226                 super.getReadableDatabase()
227             }
228         }
229 
230         fun getWrappedDb(sqLiteDatabase: SQLiteDatabase): FrameworkSQLiteDatabase {
231             return getWrappedDb(dbRef, sqLiteDatabase)
232         }
233 
234         override fun onCreate(sqLiteDatabase: SQLiteDatabase) {
235             try {
236                 callback.onCreate(getWrappedDb(sqLiteDatabase))
237             } catch (t: Throwable) {
238                 throw CallbackException(CallbackName.ON_CREATE, t)
239             }
240         }
241 
242         override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
243             migrated = true
244             try {
245                 callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion)
246             } catch (t: Throwable) {
247                 throw CallbackException(CallbackName.ON_UPGRADE, t)
248             }
249         }
250 
251         override fun onConfigure(db: SQLiteDatabase) {
252             if (!migrated && callback.version != db.version) {
253                 // Reduce the prepared statement cache to the minimum allowed (1) to avoid
254                 // issues with queries executed during migrations. Note that when a migration is
255                 // done the connection is closed and re-opened to avoid stale connections, which
256                 // in turns resets the cache max size. See b/271083856
257                 db.setMaxSqlCacheSize(1)
258             }
259             try {
260                 callback.onConfigure(getWrappedDb(db))
261             } catch (t: Throwable) {
262                 throw CallbackException(CallbackName.ON_CONFIGURE, t)
263             }
264         }
265 
266         override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
267             migrated = true
268             try {
269                 callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion)
270             } catch (t: Throwable) {
271                 throw CallbackException(CallbackName.ON_DOWNGRADE, t)
272             }
273         }
274 
275         override fun onOpen(db: SQLiteDatabase) {
276             if (!migrated) {
277                 // if we've migrated, we'll re-open the db so we should not call the callback.
278                 try {
279                     callback.onOpen(getWrappedDb(db))
280                 } catch (t: Throwable) {
281                     throw CallbackException(CallbackName.ON_OPEN, t)
282                 }
283             }
284             opened = true
285         }
286 
287         // No need sync due to locks.
288         override fun close() {
289             try {
290                 lock.lock()
291                 super.close()
292                 dbRef.db = null
293                 opened = false
294             } finally {
295                 lock.unlock()
296             }
297         }
298 
299         private class CallbackException(
300             val callbackName: CallbackName,
301             override val cause: Throwable
302         ) : RuntimeException(cause)
303 
304         internal enum class CallbackName {
305             ON_CONFIGURE,
306             ON_CREATE,
307             ON_UPGRADE,
308             ON_DOWNGRADE,
309             ON_OPEN
310         }
311 
312         companion object {
313             fun getWrappedDb(
314                 refHolder: DBRefHolder,
315                 sqLiteDatabase: SQLiteDatabase
316             ): FrameworkSQLiteDatabase {
317                 val dbRef = refHolder.db
318                 return if (dbRef == null || !dbRef.isDelegate(sqLiteDatabase)) {
319                     FrameworkSQLiteDatabase(sqLiteDatabase).also { refHolder.db = it }
320                 } else {
321                     dbRef
322                 }
323             }
324         }
325     }
326 
327     companion object {
328         private const val TAG = "SupportSQLite"
329     }
330 
331     /**
332      * This is used as an Object reference so that we can access the wrapped database inside the
333      * constructor. SQLiteOpenHelper requires the error handler to be passed in the constructor.
334      */
335     private class DBRefHolder(var db: FrameworkSQLiteDatabase?)
336 }
337