| /* |
| * 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.framework |
| |
| import android.content.Context |
| import android.database.DatabaseErrorHandler |
| import android.database.sqlite.SQLiteDatabase |
| import android.database.sqlite.SQLiteException |
| import android.database.sqlite.SQLiteOpenHelper |
| import android.os.Build |
| import android.util.Log |
| import androidx.annotation.RequiresApi |
| import androidx.sqlite.db.SupportSQLiteCompat |
| import androidx.sqlite.db.SupportSQLiteDatabase |
| import androidx.sqlite.db.SupportSQLiteOpenHelper |
| import androidx.sqlite.util.ProcessLock |
| import java.io.File |
| import java.util.UUID |
| |
| internal class FrameworkSQLiteOpenHelper @JvmOverloads constructor( |
| private val context: Context, |
| private val name: String?, |
| private val callback: SupportSQLiteOpenHelper.Callback, |
| private val useNoBackupDirectory: Boolean = false, |
| private val allowDataLossOnRecovery: Boolean = false |
| ) : SupportSQLiteOpenHelper { |
| |
| // Delegate is created lazily |
| private val lazyDelegate = lazy { |
| // OpenHelper initialization code |
| val openHelper: OpenHelper |
| |
| if ( |
| Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && |
| name != null && |
| useNoBackupDirectory |
| ) { |
| val file = File( |
| SupportSQLiteCompat.Api21Impl.getNoBackupFilesDir(context), |
| name |
| ) |
| openHelper = OpenHelper( |
| context = context, |
| name = file.absolutePath, |
| dbRef = DBRefHolder(null), |
| callback = callback, |
| allowDataLossOnRecovery = allowDataLossOnRecovery |
| ) |
| } else { |
| openHelper = OpenHelper( |
| context = context, |
| name = name, |
| dbRef = DBRefHolder(null), |
| callback = callback, |
| allowDataLossOnRecovery = allowDataLossOnRecovery |
| ) |
| } |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { |
| SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled( |
| openHelper, |
| writeAheadLoggingEnabled |
| ) |
| } |
| return@lazy openHelper |
| } |
| |
| private var writeAheadLoggingEnabled = false |
| |
| // getDelegate() is lazy because we don't want to File I/O until the call to |
| // getReadableDatabase() or getWritableDatabase(). This is better because the call to |
| // a getReadableDatabase() or a getWritableDatabase() happens on a background thread unless |
| // queries are allowed on the main thread. |
| |
| // We defer computing the path the database from the constructor to getDelegate() |
| // because context.getNoBackupFilesDir() does File I/O :( |
| private val delegate: OpenHelper by lazyDelegate |
| |
| override val databaseName: String? |
| get() = name |
| |
| @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN) |
| override fun setWriteAheadLoggingEnabled(enabled: Boolean) { |
| if (lazyDelegate.isInitialized()) { |
| // Use 'delegate', it is already initialized |
| SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(delegate, enabled) |
| } |
| writeAheadLoggingEnabled = enabled |
| } |
| |
| override val writableDatabase: SupportSQLiteDatabase |
| get() = delegate.getSupportDatabase(true) |
| |
| override val readableDatabase: SupportSQLiteDatabase |
| get() = delegate.getSupportDatabase(false) |
| |
| override fun close() { |
| if (lazyDelegate.isInitialized()) { |
| delegate.close() |
| } |
| } |
| |
| private class OpenHelper( |
| val context: Context, |
| name: String?, |
| /** |
| * This is used as an Object reference so that we can access the wrapped database inside |
| * the constructor. SQLiteOpenHelper requires the error handler to be passed in the |
| * constructor. |
| */ |
| val dbRef: DBRefHolder, |
| val callback: SupportSQLiteOpenHelper.Callback, |
| val allowDataLossOnRecovery: Boolean |
| ) : SQLiteOpenHelper( |
| context, name, null, callback.version, |
| DatabaseErrorHandler { dbObj -> |
| callback.onCorruption( |
| getWrappedDb( |
| dbRef, |
| dbObj |
| ) |
| ) |
| }) { |
| // see b/78359448 |
| private var migrated = false |
| |
| // see b/193182592 |
| private val lock: ProcessLock = ProcessLock( |
| name = name ?: UUID.randomUUID().toString(), |
| lockDir = context.cacheDir, |
| processLock = false |
| ) |
| private var opened = false |
| |
| fun getSupportDatabase(writable: Boolean): SupportSQLiteDatabase { |
| return try { |
| lock.lock(!opened && databaseName != null) |
| migrated = false |
| val db = innerGetDatabase(writable) |
| if (migrated) { |
| // there might be a connection w/ stale structure, we should re-open. |
| close() |
| return getSupportDatabase(writable) |
| } |
| getWrappedDb(db) |
| } finally { |
| lock.unlock() |
| } |
| } |
| |
| private fun innerGetDatabase(writable: Boolean): SQLiteDatabase { |
| val name = databaseName |
| val isOpen = opened |
| if (name != null && !isOpen) { |
| val databaseFile = context.getDatabasePath(name) |
| val parentFile = databaseFile.parentFile |
| if (parentFile != null) { |
| parentFile.mkdirs() |
| if (!parentFile.isDirectory) { |
| Log.w(TAG, "Invalid database parent file, not a directory: $parentFile") |
| } |
| } |
| } |
| try { |
| return getWritableOrReadableDatabase(writable) |
| } catch (t: Throwable) { |
| // No good, just try again... |
| super.close() |
| } |
| try { |
| // Wait before trying to open the DB, ideally enough to account for some slow I/O. |
| // Similar to android_database_SQLiteConnection's BUSY_TIMEOUT_MS but not as much. |
| Thread.sleep(500) |
| } catch (e: InterruptedException) { |
| // Ignore, and continue |
| } |
| val openRetryError: Throwable = try { |
| return getWritableOrReadableDatabase(writable) |
| } catch (t: Throwable) { |
| super.close() |
| t |
| } |
| if (openRetryError is CallbackException) { |
| // Callback error (onCreate, onUpgrade, onOpen, etc), possibly user error. |
| val cause = openRetryError.cause |
| when (openRetryError.callbackName) { |
| CallbackName.ON_CONFIGURE, |
| CallbackName.ON_CREATE, |
| CallbackName.ON_UPGRADE, |
| CallbackName.ON_DOWNGRADE -> throw cause |
| CallbackName.ON_OPEN -> {} |
| } |
| // If callback exception is not an SQLiteException, then more certainly it is not |
| // recoverable. |
| if (cause !is SQLiteException) { |
| throw cause |
| } |
| } else if (openRetryError is SQLiteException) { |
| // Ideally we are looking for SQLiteCantOpenDatabaseException and similar, but |
| // corruption can manifest in others forms. |
| if (name == null || !allowDataLossOnRecovery) { |
| throw openRetryError |
| } |
| } else { |
| throw openRetryError |
| } |
| |
| // Delete the database and try one last time. (mAllowDataLossOnRecovery == true) |
| context.deleteDatabase(name) |
| try { |
| return getWritableOrReadableDatabase(writable) |
| } catch (ex: CallbackException) { |
| // Unwrap our exception to avoid disruption with other try-catch in the call stack. |
| throw ex.cause |
| } |
| } |
| |
| private fun getWritableOrReadableDatabase(writable: Boolean): SQLiteDatabase { |
| return if (writable) { |
| super.getWritableDatabase() |
| } else { |
| super.getReadableDatabase() |
| } |
| } |
| |
| fun getWrappedDb(sqLiteDatabase: SQLiteDatabase): FrameworkSQLiteDatabase { |
| return getWrappedDb(dbRef, sqLiteDatabase) |
| } |
| |
| override fun onCreate(sqLiteDatabase: SQLiteDatabase) { |
| try { |
| callback.onCreate(getWrappedDb(sqLiteDatabase)) |
| } catch (t: Throwable) { |
| throw CallbackException(CallbackName.ON_CREATE, t) |
| } |
| } |
| |
| override fun onUpgrade(sqLiteDatabase: SQLiteDatabase, oldVersion: Int, newVersion: Int) { |
| migrated = true |
| try { |
| callback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion) |
| } catch (t: Throwable) { |
| throw CallbackException(CallbackName.ON_UPGRADE, t) |
| } |
| } |
| |
| override fun onConfigure(db: SQLiteDatabase) { |
| try { |
| callback.onConfigure(getWrappedDb(db)) |
| } catch (t: Throwable) { |
| throw CallbackException(CallbackName.ON_CONFIGURE, t) |
| } |
| } |
| |
| override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { |
| migrated = true |
| try { |
| callback.onDowngrade(getWrappedDb(db), oldVersion, newVersion) |
| } catch (t: Throwable) { |
| throw CallbackException(CallbackName.ON_DOWNGRADE, t) |
| } |
| } |
| |
| override fun onOpen(db: SQLiteDatabase) { |
| if (!migrated) { |
| // if we've migrated, we'll re-open the db so we should not call the callback. |
| try { |
| callback.onOpen(getWrappedDb(db)) |
| } catch (t: Throwable) { |
| throw CallbackException(CallbackName.ON_OPEN, t) |
| } |
| } |
| opened = true |
| } |
| |
| // No need sync due to locks. |
| override fun close() { |
| try { |
| lock.lock() |
| super.close() |
| dbRef.db = null |
| opened = false |
| } finally { |
| lock.unlock() |
| } |
| } |
| |
| private class CallbackException( |
| val callbackName: CallbackName, |
| override val cause: Throwable |
| ) : RuntimeException(cause) |
| |
| internal enum class CallbackName { |
| ON_CONFIGURE, ON_CREATE, ON_UPGRADE, ON_DOWNGRADE, ON_OPEN |
| } |
| |
| companion object { |
| fun getWrappedDb( |
| refHolder: DBRefHolder, |
| sqLiteDatabase: SQLiteDatabase |
| ): FrameworkSQLiteDatabase { |
| val dbRef = refHolder.db |
| return if (dbRef == null || !dbRef.isDelegate(sqLiteDatabase)) { |
| FrameworkSQLiteDatabase(sqLiteDatabase).also { refHolder.db = it } |
| } else { |
| dbRef |
| } |
| } |
| } |
| } |
| |
| companion object { |
| private const val TAG = "SupportSQLite" |
| } |
| |
| /** |
| * This is used as an Object reference so that we can access the wrapped database inside |
| * the constructor. SQLiteOpenHelper requires the error handler to be passed in the |
| * constructor. |
| */ |
| private class DBRefHolder(var db: FrameworkSQLiteDatabase?) |
| } |