Add SQLite open retry and recovery mechanism

Bug: 215592732
Test: OpenHelperRecoveryTest
Relnote: Add an API in SupportSQLite's configuration to allow data loss during the recovery mechanism.
Change-Id: I1b83020d4b49cf05b3a64617dceb6c62d04ff663
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
index 60b5462..7927944 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/migration/MigrationTest.java
@@ -38,6 +38,7 @@
 import androidx.room.util.TableInfo;
 import androidx.room.util.ViewInfo;
 import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
 import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -562,6 +563,37 @@
         }
     }
 
+    // Verifies that even with allowDataLossOnRecovery, bad migrations are propagated and the DB
+    // is not silently deleted.
+    @Test
+    public void badMigration_allowDataLossOnRecovery() throws IOException {
+        // Create DB at version 1
+        helper.createDatabase(TEST_DB, 1).close();
+
+        // Create DB at latest version, but no migrations and with allowDataLossOnRecovery, it
+        // should fail to open.
+        Context targetContext = ApplicationProvider.getApplicationContext();
+        MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+                .openHelperFactory(configuration -> {
+                    SupportSQLiteOpenHelper.Configuration config =
+                            SupportSQLiteOpenHelper.Configuration.builder(targetContext)
+                                    .name(configuration.name)
+                                    .callback(configuration.callback)
+                                    .allowDataLossOnRecovery(true)
+                                    .build();
+                    return new FrameworkSQLiteOpenHelperFactory().create(config);
+                })
+                .build();
+        try {
+            db.getOpenHelper().getWritableDatabase();
+            Assert.fail("Expected a missing migration exception");
+        } catch (IllegalStateException ex) {
+            // Verifies exception is not wrapped
+            Truth.assertThat(ex).hasMessageThat()
+                    .containsMatch("A migration from \\d+ to \\d+ was required but not found.");
+        }
+    }
+
     private void testFailure(int startVersion, int endVersion) throws IOException {
         final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
         db.close();
diff --git a/sqlite/sqlite-framework/build.gradle b/sqlite/sqlite-framework/build.gradle
index 8e92837..9dfb2cf 100644
--- a/sqlite/sqlite-framework/build.gradle
+++ b/sqlite/sqlite-framework/build.gradle
@@ -19,11 +19,20 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
+    id("org.jetbrains.kotlin.android")
 }
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
     api(project(":sqlite:sqlite"))
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner) {
+        exclude module: "support-annotations"
+        exclude module: "hamcrest-core"
+    }
+    androidTestImplementation(libs.truth)
 }
 
 androidx {
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt
new file mode 100644
index 0000000..2ff6161
--- /dev/null
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/OpenHelperRecoveryTest.kt
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2022 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.sqlite.SQLiteException
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+
+@LargeTest
+class OpenHelperRecoveryTest {
+
+    private val dbName = "test.db"
+    private val context: Context = ApplicationProvider.getApplicationContext()
+
+    @Before
+    fun setup() {
+        context.deleteDatabase(dbName)
+    }
+
+    @Test
+    fun writeOver() {
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, EmptyCallback(), false, false)
+        openHelper.writableDatabase.use { db ->
+            db.execSQL("CREATE TABLE Foo (id INTEGER PRIMARY KEY)")
+            db.query("SELECT * FROM sqlite_master WHERE name = 'Foo'").use {
+                assertThat(it.count).isEqualTo(1)
+            }
+        }
+
+        val dbFile = context.getDatabasePath(dbName)
+        assertThat(dbFile.exists()).isTrue()
+        assertThat(dbFile.length()).isGreaterThan(0)
+        dbFile.writeText("malas vibra")
+
+        try {
+            openHelper.writableDatabase
+            fail("Database should have failed to open.")
+        } catch (ex: SQLiteException) {
+            // Expected
+        }
+    }
+
+    @Test
+    fun writeOver_allowDataLossOnRecovery() {
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, EmptyCallback(), false, true)
+        openHelper.writableDatabase.use { db ->
+            db.execSQL("CREATE TABLE Foo (id INTEGER PRIMARY KEY)")
+            db.query("SELECT * FROM sqlite_master WHERE name = 'Foo'").use {
+                assertThat(it.count).isEqualTo(1)
+            }
+        }
+
+        val dbFile = context.getDatabasePath(dbName)
+        assertThat(dbFile.exists()).isTrue()
+        assertThat(dbFile.length()).isGreaterThan(0)
+        dbFile.writeText("malas vibra")
+
+        openHelper.writableDatabase.use { db ->
+            db.query("SELECT * FROM sqlite_master WHERE name = 'Foo'").use {
+                assertThat(it.count).isEqualTo(0)
+            }
+        }
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onCreateError() {
+        var createAttempts = 0
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+            override fun onCreate(db: SupportSQLiteDatabase) {
+                if (createAttempts++ < 2) {
+                    throw RuntimeException("Not an SQLiteException")
+                }
+            }
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+        }
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        try {
+            openHelper.writableDatabase
+            fail("Database should have failed to open.")
+        } catch (ex: RuntimeException) {
+            // Expected
+            assertThat(ex.message).contains("Not an SQLiteException")
+        }
+        assertThat(createAttempts).isEqualTo(2)
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onUpgradeError() {
+        // Create DB at version 1, open and close it
+        FrameworkSQLiteOpenHelper(context, dbName, EmptyCallback(1), false, true).let {
+            it.writableDatabase.close()
+        }
+
+        // A callback to open DB at version 2, it has a bad migration.
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(2) {
+            override fun onCreate(db: SupportSQLiteDatabase) {}
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+                db.execSQL("SELECT * FROM bad_table")
+            }
+        }
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        try {
+            openHelper.writableDatabase
+            fail("Database should have failed to open.")
+        } catch (ex: SQLiteException) {
+            // Expected
+            assertThat(ex.message).contains("no such table: bad_table")
+        }
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onOpenNonSQLiteError() {
+        var openAttempts = 0
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+            override fun onCreate(db: SupportSQLiteDatabase) {}
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+            override fun onOpen(db: SupportSQLiteDatabase) {
+                if (openAttempts++ < 2) {
+                    throw RuntimeException("Not an SQLiteException")
+                }
+            }
+        }
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        try {
+            openHelper.writableDatabase
+            fail("Database should have failed to open.")
+        } catch (ex: RuntimeException) {
+            // Expected
+            assertThat(ex.message).contains("Not an SQLiteException")
+        }
+        assertThat(openAttempts).isEqualTo(2)
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onOpenSQLiteError_intermediate() {
+        FrameworkSQLiteOpenHelper(context, dbName, EmptyCallback(), false, false)
+            .writableDatabase.use { db ->
+                db.execSQL("CREATE TABLE Foo (id INTEGER PRIMARY KEY)")
+            }
+
+        var openAttempts = 0
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+            override fun onCreate(db: SupportSQLiteDatabase) {}
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+            override fun onOpen(db: SupportSQLiteDatabase) {
+                if (openAttempts++ < 1) {
+                    db.execSQL("SELECT * FROM bad_table")
+                }
+            }
+        }
+        // With only 1 onOpen error, the database is opened without being deleted, simulates an
+        // intermediate error.
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        openHelper.writableDatabase.use { db ->
+            db.query("SELECT * FROM sqlite_master WHERE name = 'Foo'").use {
+                assertThat(it.count).isEqualTo(1)
+            }
+        }
+        assertThat(openAttempts).isEqualTo(2)
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onOpenSQLiteError_recoverable() {
+        FrameworkSQLiteOpenHelper(context, dbName, EmptyCallback(), false, false)
+            .writableDatabase.use { db ->
+                db.execSQL("CREATE TABLE Foo (id INTEGER PRIMARY KEY)")
+            }
+
+        var openAttempts = 0
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+            override fun onCreate(db: SupportSQLiteDatabase) {}
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+            override fun onOpen(db: SupportSQLiteDatabase) {
+                if (openAttempts++ < 2) {
+                    db.execSQL("SELECT * FROM bad_table")
+                }
+            }
+        }
+        // With 2 onOpen error, the database is opened by deleting it, simulating a recoverable
+        // error.
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        openHelper.writableDatabase.use { db ->
+            db.query("SELECT * FROM sqlite_master WHERE name = 'Foo'").use {
+                assertThat(it.count).isEqualTo(0)
+            }
+        }
+        assertThat(openAttempts).isEqualTo(3)
+    }
+
+    @Test
+    fun allowDataLossOnRecovery_onOpenSQLiteError_permanent() {
+        var openAttempts = 0
+        val badCallback = object : SupportSQLiteOpenHelper.Callback(1) {
+            override fun onCreate(db: SupportSQLiteDatabase) {}
+            override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {}
+            override fun onOpen(db: SupportSQLiteDatabase) {
+                openAttempts++
+                db.execSQL("SELECT * FROM bad_table")
+            }
+        }
+        // Consistent onOpen error, might be a user bug or an actual SQLite permanent error,
+        // nothing we can do here, expect failure
+        val openHelper = FrameworkSQLiteOpenHelper(context, dbName, badCallback, false, true)
+        try {
+            openHelper.writableDatabase
+            fail("Database should have failed to open.")
+        } catch (ex: SQLiteException) {
+            // Expected
+            assertThat(ex.message).contains("no such table: bad_table")
+        }
+        assertThat(openAttempts).isEqualTo(3)
+    }
+
+    class EmptyCallback(version: Int = 1) : SupportSQLiteOpenHelper.Callback(version) {
+        override fun onCreate(db: SupportSQLiteDatabase) {
+        }
+
+        override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+        }
+
+        override fun onOpen(db: SupportSQLiteDatabase) {
+        }
+
+        override fun onCorruption(db: SupportSQLiteDatabase) {
+        }
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java
index b3f6a12..7a76b70 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.java
@@ -19,24 +19,31 @@
 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.NonNull;
 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 androidx.sqlite.util.SneakyThrow;
 
 import java.io.File;
 import java.util.UUID;
 
 class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
 
+    private static final String TAG = "SupportSQLite";
+
     private final Context mContext;
     private final String mName;
     private final Callback mCallback;
     private final boolean mUseNoBackupDirectory;
+    private final boolean mAllowDataLossOnRecovery;
     private final Object mLock;
 
     // Delegate is created lazily
@@ -55,10 +62,20 @@
             String name,
             Callback callback,
             boolean useNoBackupDirectory) {
+        this(context, name, callback, useNoBackupDirectory, false);
+    }
+
+    FrameworkSQLiteOpenHelper(
+            Context context,
+            String name,
+            Callback callback,
+            boolean useNoBackupDirectory,
+            boolean allowDataLossOnRecovery) {
         mContext = context;
         mName = name;
         mCallback = callback;
         mUseNoBackupDirectory = useNoBackupDirectory;
+        mAllowDataLossOnRecovery = allowDataLossOnRecovery;
         mLock = new Object();
     }
 
@@ -80,9 +97,11 @@
                             SupportSQLiteCompat.Api21Impl.getNoBackupFilesDir(mContext),
                             mName
                     );
-                    mDelegate = new OpenHelper(mContext, file.getAbsolutePath(), dbRef, mCallback);
+                    mDelegate = new OpenHelper(mContext, file.getAbsolutePath(), dbRef, mCallback,
+                            mAllowDataLossOnRecovery);
                 } else {
-                    mDelegate = new OpenHelper(mContext, mName, dbRef, mCallback);
+                    mDelegate = new OpenHelper(mContext, mName, dbRef, mCallback,
+                            mAllowDataLossOnRecovery);
                 }
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                     SupportSQLiteCompat.Api16Impl.setWriteAheadLoggingEnabled(mDelegate,
@@ -111,12 +130,12 @@
 
     @Override
     public SupportSQLiteDatabase getWritableDatabase() {
-        return getDelegate().getWritableSupportDatabase();
+        return getDelegate().getSupportDatabase(true);
     }
 
     @Override
     public SupportSQLiteDatabase getReadableDatabase() {
-        return getDelegate().getReadableSupportDatabase();
+        return getDelegate().getSupportDatabase(false);
     }
 
     @Override
@@ -131,7 +150,9 @@
          * constructor.
          */
         final FrameworkSQLiteDatabase[] mDbRef;
+        final Context mContext;
         final Callback mCallback;
+        final boolean mAllowDataLossOnRecovery;
         // see b/78359448
         private boolean mMigrated;
         // see b/193182592
@@ -139,7 +160,7 @@
         private boolean mOpened;
 
         OpenHelper(Context context, String name, final FrameworkSQLiteDatabase[] dbRef,
-                final Callback callback) {
+                final Callback callback, boolean allowDataLossOnRecovery) {
             super(context, name, null, callback.version,
                     new DatabaseErrorHandler() {
                         @Override
@@ -147,21 +168,23 @@
                             callback.onCorruption(getWrappedDb(dbRef, dbObj));
                         }
                     });
+            mContext = context;
             mCallback = callback;
             mDbRef = dbRef;
+            mAllowDataLossOnRecovery = allowDataLossOnRecovery;
             mLock = new ProcessLock(name == null ? UUID.randomUUID().toString() : name,
                     context.getCacheDir(), false);
         }
 
-        SupportSQLiteDatabase getWritableSupportDatabase() {
+        SupportSQLiteDatabase getSupportDatabase(boolean writable) {
             try {
                 mLock.lock(!mOpened && getDatabaseName() != null);
                 mMigrated = false;
-                SQLiteDatabase db = super.getWritableDatabase();
+                final SQLiteDatabase db = innerGetDatabase(writable);
                 if (mMigrated) {
                     // there might be a connection w/ stale structure, we should re-open.
                     close();
-                    return getWritableSupportDatabase();
+                    return getSupportDatabase(writable);
                 }
                 return getWrappedDb(db);
             } finally {
@@ -169,19 +192,87 @@
             }
         }
 
-        SupportSQLiteDatabase getReadableSupportDatabase() {
-            try {
-                mLock.lock(!mOpened && getDatabaseName() != null);
-                mMigrated = false;
-                SQLiteDatabase db = super.getReadableDatabase();
-                if (mMigrated) {
-                    // there might be a connection w/ stale structure, we should re-open.
-                    close();
-                    return getReadableSupportDatabase();
+        private SQLiteDatabase innerGetDatabase(boolean writable) {
+            String name = getDatabaseName();
+            if (name != null) {
+                File databaseFile = mContext.getDatabasePath(name);
+                File parentFile = databaseFile.getParentFile();
+                if (parentFile != null) {
+                    parentFile.mkdirs();
+                    if (!parentFile.isDirectory()) {
+                        Log.w(TAG, "Invalid database parent file, not a directory: " + parentFile);
+                    }
                 }
-                return getWrappedDb(db);
-            } finally {
-                mLock.unlock();
+            }
+
+            try {
+                return getWritableOrReadableDatabase(writable);
+            } catch (Throwable t) {
+                // 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 (InterruptedException e) {
+                // Ignore, and continue
+            }
+
+            final Throwable openRetryError;
+            try {
+                return getWritableOrReadableDatabase(writable);
+            } catch (Throwable t) {
+                super.close();
+                openRetryError = t;
+            }
+            if (openRetryError instanceof CallbackException) {
+                // Callback error (onCreate, onUpgrade, onOpen, etc), possibly user error.
+                final CallbackException callbackException = (CallbackException) openRetryError;
+                final Throwable cause = callbackException.getCause();
+                switch (callbackException.getCallbackName()) {
+                    case ON_CONFIGURE:
+                    case ON_CREATE:
+                    case ON_UPGRADE:
+                    case ON_DOWNGRADE:
+                        SneakyThrow.reThrow(cause);
+                        break;
+                    case ON_OPEN:
+                    default:
+                        break;
+                }
+                // If callback exception is not an SQLiteException, then more certainly it is not
+                // recoverable.
+                if (!(cause instanceof SQLiteException)) {
+                    SneakyThrow.reThrow(cause);
+                }
+            } else if (openRetryError instanceof SQLiteException) {
+                // Ideally we are looking for SQLiteCantOpenDatabaseException and similar, but
+                // corruption can manifest in others forms.
+                if (name == null || !mAllowDataLossOnRecovery) {
+                    SneakyThrow.reThrow(openRetryError);
+                }
+            } else {
+                SneakyThrow.reThrow(openRetryError);
+            }
+
+            // Delete the database and try one last time. (mAllowDataLossOnRecovery == true)
+            mContext.deleteDatabase(name);
+            try {
+                return getWritableOrReadableDatabase(writable);
+            } catch (CallbackException ex) {
+                // Unwrap our exception to avoid disruption with other try-catch in the call stack.
+                SneakyThrow.reThrow(ex.getCause());
+                return null; // Unreachable code, but compiler doesn't know it.
+            }
+        }
+
+        private SQLiteDatabase getWritableOrReadableDatabase(boolean writable) {
+            if (writable) {
+                return super.getWritableDatabase();
+            } else {
+                return super.getReadableDatabase();
             }
         }
 
@@ -191,31 +282,51 @@
 
         @Override
         public void onCreate(SQLiteDatabase sqLiteDatabase) {
-            mCallback.onCreate(getWrappedDb(sqLiteDatabase));
+            try {
+                mCallback.onCreate(getWrappedDb(sqLiteDatabase));
+            } catch (Throwable t) {
+                throw new CallbackException(CallbackName.ON_CREATE, t);
+            }
         }
 
         @Override
         public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
             mMigrated = true;
-            mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);
+            try {
+                mCallback.onUpgrade(getWrappedDb(sqLiteDatabase), oldVersion, newVersion);
+            } catch (Throwable t) {
+                throw new CallbackException(CallbackName.ON_UPGRADE, t);
+            }
         }
 
         @Override
         public void onConfigure(SQLiteDatabase db) {
-            mCallback.onConfigure(getWrappedDb(db));
+            try {
+                mCallback.onConfigure(getWrappedDb(db));
+            } catch (Throwable t) {
+                throw new CallbackException(CallbackName.ON_CONFIGURE, t);
+            }
         }
 
         @Override
         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             mMigrated = true;
-            mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);
+            try {
+                mCallback.onDowngrade(getWrappedDb(db), oldVersion, newVersion);
+            } catch (Throwable t) {
+                throw new CallbackException(CallbackName.ON_DOWNGRADE, t);
+            }
         }
 
         @Override
         public void onOpen(SQLiteDatabase db) {
             if (!mMigrated) {
                 // if we've migrated, we'll re-open the db so we should not call the callback.
-                mCallback.onOpen(getWrappedDb(db));
+                try {
+                    mCallback.onOpen(getWrappedDb(db));
+                } catch (Throwable t) {
+                    throw new CallbackException(CallbackName.ON_OPEN, t);
+                }
             }
             mOpened = true;
         }
@@ -241,5 +352,36 @@
             }
             return refHolder[0];
         }
+
+        private static final class CallbackException extends RuntimeException {
+
+            private final CallbackName mCallbackName;
+            private final Throwable mCause;
+
+            CallbackException(CallbackName callbackName, Throwable cause) {
+                super(cause);
+                mCallbackName = callbackName;
+                mCause = cause;
+            }
+
+            public CallbackName getCallbackName() {
+                return mCallbackName;
+            }
+
+            @NonNull
+            @Override
+            @SuppressWarnings("UnsynchronizedOverridesSynchronized") // Not needed, cause is final
+            public Throwable getCause() {
+                return mCause;
+            }
+        }
+
+        enum CallbackName {
+            ON_CONFIGURE,
+            ON_CREATE,
+            ON_UPGRADE,
+            ON_DOWNGRADE,
+            ON_OPEN
+        }
     }
 }
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java
index 2f55202..34a0ce0 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelperFactory.java
@@ -33,6 +33,7 @@
                 configuration.context,
                 configuration.name,
                 configuration.callback,
-                configuration.useNoBackupDirectory);
+                configuration.useNoBackupDirectory,
+                configuration.allowDataLossOnRecovery);
     }
 }
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/SneakyThrow.java b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/SneakyThrow.java
new file mode 100644
index 0000000..e1309b4
--- /dev/null
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/SneakyThrow.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019 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.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Java 8 Sneaky Throw technique.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SneakyThrow {
+
+    /**
+     * Re-throws a throwable as if it was a runtime exception without wrapping it.
+     *
+     * @param t the throwable to re-throw.
+     */
+    public static void reThrow(@NonNull Throwable t) {
+        sneakyThrow(t);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <E extends Throwable> void sneakyThrow(@NonNull Throwable t) throws E {
+        throw (E) t;
+    }
+
+    private SneakyThrow() {
+
+    }
+}
diff --git a/sqlite/sqlite/api/api_lint.ignore b/sqlite/sqlite/api/api_lint.ignore
index 4e9a7e3..3d31715 100644
--- a/sqlite/sqlite/api/api_lint.ignore
+++ b/sqlite/sqlite/api/api_lint.ignore
@@ -41,6 +41,8 @@
     Builder methods names should use setFoo() / addFoo() / clearFoo() style: method androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder.name(String)
 BuilderSetStyle: androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder#noBackupDirectory(boolean):
     Builder methods names should use setFoo() / addFoo() / clearFoo() style: method androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder.noBackupDirectory(boolean)
+BuilderSetStyle: androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder#allowDataLossOnRecovery(boolean):
+    Builder methods names should use setFoo() / addFoo() / clearFoo() style: method androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder.allowDataLossOnRecovery(boolean)
 BuilderSetStyle: androidx.sqlite.db.SupportSQLiteQueryBuilder#builder(String):
     Builder methods names should use setFoo() / addFoo() / clearFoo() style: method androidx.sqlite.db.SupportSQLiteQueryBuilder.builder(String)
 BuilderSetStyle: androidx.sqlite.db.SupportSQLiteQueryBuilder#columns(String[]):
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index dbdea61..927cb1b 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -74,6 +74,7 @@
 
   public static class SupportSQLiteOpenHelper.Configuration {
     method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+    field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
     field public final String? name;
@@ -81,6 +82,7 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
diff --git a/sqlite/sqlite/api/public_plus_experimental_current.txt b/sqlite/sqlite/api/public_plus_experimental_current.txt
index dbdea61..927cb1b 100644
--- a/sqlite/sqlite/api/public_plus_experimental_current.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_current.txt
@@ -74,6 +74,7 @@
 
   public static class SupportSQLiteOpenHelper.Configuration {
     method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+    field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
     field public final String? name;
@@ -81,6 +82,7 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index dbdea61..927cb1b 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -74,6 +74,7 @@
 
   public static class SupportSQLiteOpenHelper.Configuration {
     method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+    field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
     field public final String? name;
@@ -81,6 +82,7 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
index 0123606..55d37ea 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
@@ -322,6 +322,11 @@
          * If {@code true} the database will be stored in the no-backup directory.
          */
         public final boolean useNoBackupDirectory;
+        /**
+         * If {@code true} the database will be delete and its data loss in the case that it
+         * cannot be opened.
+         */
+        public final boolean allowDataLossOnRecovery;
 
         Configuration(
                 @NonNull Context context,
@@ -335,10 +340,20 @@
                 @Nullable String name,
                 @NonNull Callback callback,
                 boolean useNoBackupDirectory) {
+            this(context, name, callback, useNoBackupDirectory, false);
+        }
+
+        Configuration(
+                @NonNull Context context,
+                @Nullable String name,
+                @NonNull Callback callback,
+                boolean useNoBackupDirectory,
+                boolean allowDataLossOnRecovery) {
             this.context = context;
             this.name = name;
             this.callback = callback;
             this.useNoBackupDirectory = useNoBackupDirectory;
+            this.allowDataLossOnRecovery = allowDataLossOnRecovery;
         }
 
         /**
@@ -359,6 +374,7 @@
             String mName;
             SupportSQLiteOpenHelper.Callback mCallback;
             boolean mUseNoBackupDirectory;
+            boolean mAllowDataLossOnRecovery;
 
             /**
              * <p>
@@ -386,7 +402,8 @@
                             "Must set a non-null database name to a configuration that uses the "
                                     + "no backup directory.");
                 }
-                return new Configuration(mContext, mName, mCallback, mUseNoBackupDirectory);
+                return new Configuration(mContext, mName, mCallback, mUseNoBackupDirectory,
+                        mAllowDataLossOnRecovery);
             }
 
             Builder(@NonNull Context context) {
@@ -424,6 +441,19 @@
                 mUseNoBackupDirectory = useNoBackupDirectory;
                 return this;
             }
+
+            /**
+             * Sets whether to delete and recreate the database file in situations when the
+             * database file cannot be opened, thus allowing for its data to be lost.
+             * @param allowDataLossOnRecovery If {@code true} the database file might be recreated
+             *                                in the case that it cannot be opened.
+             * @return this
+             */
+            @NonNull
+            public Builder allowDataLossOnRecovery(boolean allowDataLossOnRecovery) {
+                mAllowDataLossOnRecovery = allowDataLossOnRecovery;
+                return this;
+            }
         }
     }