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