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;
+ }
}
}