Add the ability to use the no backup directory for creating the database.
* Configuration.Builder exposes a new API `noBackupDirectory`.
Fixes: b/142681857
Test: Added unit tests. Integration tests pass. Ran ./gradlew bOS
Change-Id: I61d3d4a75c214665b5c39421411bd3f6f5f4562c
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/NoBackupDirectoryTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/NoBackupDirectoryTest.java
new file mode 100644
index 0000000..ace1db1
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/NoBackupDirectoryTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NoBackupDirectoryTest {
+
+ private static final String NAME = "database.db";
+
+ private Context mContext;
+ private SupportSQLiteOpenHelper.Factory mFactory;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ mFactory = new AlwaysNoBackupFactory();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 23)
+ public void testBuildDatabase_withCustomSupportSQLiteOpenHelper() {
+ // Setting the minSdkVersion to 23 even though Context.getNoBackupFilesDir() is supported
+ // on API 21+ because it was unused until API 23.
+ RoomDatabase database = Room.databaseBuilder(mContext, TestDatabase.class, NAME)
+ .openHelperFactory(mFactory)
+ .build();
+
+ SupportSQLiteOpenHelper helper = database.getOpenHelper();
+ assertThat(helper.getDatabaseName(), is(NAME));
+ SupportSQLiteDatabase sqLiteDatabase = helper.getWritableDatabase();
+ String expectedPath = new File(mContext.getNoBackupFilesDir(), NAME).getPath();
+ assertThat(sqLiteDatabase.getPath(), is(expectedPath));
+ database.close();
+ }
+
+ @Test
+ public void testBuildInMemoryDatabase_withAlwaysNoBackupFactory() {
+ Throwable exception = null;
+ try {
+ RoomDatabase database = Room.inMemoryDatabaseBuilder(mContext, TestDatabase.class)
+ .openHelperFactory(mFactory)
+ .build();
+ database.getOpenHelper().getWritableDatabase();
+ } catch (Throwable expected) {
+ exception = expected;
+ }
+
+ assertThat(exception, notNullValue());
+ assertThat(exception.getMessage(),
+ is("Must set a non-null database name to a configuration that uses the no backup "
+ + "directory."));
+ }
+
+ static class AlwaysNoBackupFactory implements SupportSQLiteOpenHelper.Factory {
+ private final SupportSQLiteOpenHelper.Factory mDelegate;
+
+ AlwaysNoBackupFactory() {
+ mDelegate = new FrameworkSQLiteOpenHelperFactory();
+ }
+
+ @NonNull
+ @Override
+ public SupportSQLiteOpenHelper create(
+ @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
+ SupportSQLiteOpenHelper.Configuration backupConfiguration =
+ SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
+ .callback(configuration.callback)
+ .name(configuration.name)
+ .noBackupDirectory(true)
+ .build();
+ return mDelegate.create(backupConfiguration);
+ }
+ }
+}
diff --git a/room/runtime/build.gradle b/room/runtime/build.gradle
index 113582b..0f271d7 100644
--- a/room/runtime/build.gradle
+++ b/room/runtime/build.gradle
@@ -34,8 +34,8 @@
dependencies {
api(project(":room:room-common"))
- api("androidx.sqlite:sqlite-framework:2.0.1")
- api("androidx.sqlite:sqlite:2.0.1")
+ api(project(":sqlite:sqlite-framework"))
+ api(project(":sqlite:sqlite"))
implementation("androidx.arch.core:core-runtime:2.0.1")
compileOnly("androidx.paging:paging-common:2.0.0")
compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
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 3275857..a457b14 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
@@ -25,42 +25,95 @@
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
-class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
- private final OpenHelper mDelegate;
+import java.io.File;
- FrameworkSQLiteOpenHelper(Context context, String name, Callback callback) {
- mDelegate = createDelegate(context, name, callback);
+class FrameworkSQLiteOpenHelper implements SupportSQLiteOpenHelper {
+
+ private final Context mContext;
+ private final String mName;
+ private final Callback mCallback;
+ private final boolean mUseNoBackupDirectory;
+ private final Object mLock;
+
+ // Delegate is created lazily
+ private OpenHelper mDelegate;
+ private boolean mWriteAheadLoggingEnabled;
+
+ FrameworkSQLiteOpenHelper(
+ Context context,
+ String name,
+ Callback callback) {
+ this(context, name, callback, false);
}
- private OpenHelper createDelegate(Context context, String name, Callback callback) {
- final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
- return new OpenHelper(context, name, dbRef, callback);
+ FrameworkSQLiteOpenHelper(
+ Context context,
+ String name,
+ Callback callback,
+ boolean useNoBackupDirectory) {
+ mContext = context;
+ mName = name;
+ mCallback = callback;
+ mUseNoBackupDirectory = useNoBackupDirectory;
+ mLock = new Object();
+ }
+
+ private OpenHelper getDelegate() {
+ // 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 :(
+ synchronized (mLock) {
+ if (mDelegate == null) {
+ final FrameworkSQLiteDatabase[] dbRef = new FrameworkSQLiteDatabase[1];
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && mName != null
+ && mUseNoBackupDirectory) {
+ File file = new File(mContext.getNoBackupFilesDir(), mName);
+ mDelegate = new OpenHelper(mContext, file.getAbsolutePath(), dbRef, mCallback);
+ } else {
+ mDelegate = new OpenHelper(mContext, mName, dbRef, mCallback);
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ mDelegate.setWriteAheadLoggingEnabled(mWriteAheadLoggingEnabled);
+ }
+ }
+ return mDelegate;
+ }
}
@Override
public String getDatabaseName() {
- return mDelegate.getDatabaseName();
+ return mName;
}
@Override
@androidx.annotation.RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
- mDelegate.setWriteAheadLoggingEnabled(enabled);
+ synchronized (mLock) {
+ if (mDelegate != null) {
+ mDelegate.setWriteAheadLoggingEnabled(enabled);
+ }
+ mWriteAheadLoggingEnabled = enabled;
+ }
}
@Override
public SupportSQLiteDatabase getWritableDatabase() {
- return mDelegate.getWritableSupportDatabase();
+ return getDelegate().getWritableSupportDatabase();
}
@Override
public SupportSQLiteDatabase getReadableDatabase() {
- return mDelegate.getReadableSupportDatabase();
+ return getDelegate().getReadableSupportDatabase();
}
@Override
public void close() {
- mDelegate.close();
+ getDelegate().close();
}
static class OpenHelper extends SQLiteOpenHelper {
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 d27abee..2f55202 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
@@ -30,6 +30,9 @@
public SupportSQLiteOpenHelper create(
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
return new FrameworkSQLiteOpenHelper(
- configuration.context, configuration.name, configuration.callback);
+ configuration.context,
+ configuration.name,
+ configuration.callback,
+ configuration.useNoBackupDirectory);
}
}
diff --git a/sqlite/sqlite/api/2.1.0-alpha01.txt b/sqlite/sqlite/api/2.1.0-alpha01.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/2.1.0-alpha01.txt
+++ b/sqlite/sqlite/api/2.1.0-alpha01.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
diff --git a/sqlite/sqlite/api/public_plus_experimental_2.1.0-alpha01.txt b/sqlite/sqlite/api/public_plus_experimental_2.1.0-alpha01.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/public_plus_experimental_2.1.0-alpha01.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_2.1.0-alpha01.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
diff --git a/sqlite/sqlite/api/public_plus_experimental_current.txt b/sqlite/sqlite/api/public_plus_experimental_current.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/public_plus_experimental_current.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_current.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
diff --git a/sqlite/sqlite/api/restricted_2.1.0-alpha01.txt b/sqlite/sqlite/api/restricted_2.1.0-alpha01.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/restricted_2.1.0-alpha01.txt
+++ b/sqlite/sqlite/api/restricted_2.1.0-alpha01.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index f4688a2f..8e1ecd7 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -75,12 +75,14 @@
field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
field public final android.content.Context context;
field public final String? name;
+ field public final boolean useNoBackupDirectory;
}
public static class SupportSQLiteOpenHelper.Configuration.Builder {
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?);
+ method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
}
public static interface SupportSQLiteOpenHelper.Factory {
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 8c0b7f6..afe705f 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
@@ -20,6 +20,7 @@
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
@@ -317,11 +318,27 @@
*/
@NonNull
public final SupportSQLiteOpenHelper.Callback callback;
+ /**
+ * If {@code true} the database will be stored in the no-backup directory.
+ */
+ public final boolean useNoBackupDirectory;
- Configuration(@NonNull Context context, @Nullable String name, @NonNull Callback callback) {
+ Configuration(
+ @NonNull Context context,
+ @Nullable String name,
+ @NonNull Callback callback) {
+ this(context, name, callback, false);
+ }
+
+ Configuration(
+ @NonNull Context context,
+ @Nullable String name,
+ @NonNull Callback callback,
+ boolean useNoBackupDirectory) {
this.context = context;
this.name = name;
this.callback = callback;
+ this.useNoBackupDirectory = useNoBackupDirectory;
}
/**
@@ -341,7 +358,19 @@
Context mContext;
String mName;
SupportSQLiteOpenHelper.Callback mCallback;
+ boolean mUseNoBackUpDirectory;
+ /**
+ * <p>
+ * Throws an {@link IllegalArgumentException} if the {@link Callback} is {@code null}.
+ * <p>
+ * Throws an {@link IllegalArgumentException} if the {@link Context} is {@code null}.
+ * <p>
+ * Throws an {@link IllegalArgumentException} if the {@link String} database
+ * name is {@code null}. {@see Context#getNoBackupFilesDir()}
+ *
+ * @return The {@link Configuration} instance
+ */
@NonNull
public Configuration build() {
if (mCallback == null) {
@@ -352,7 +381,12 @@
throw new IllegalArgumentException("Must set a non-null context to create"
+ " the configuration.");
}
- return new Configuration(mContext, mName, mCallback);
+ if (mUseNoBackUpDirectory && TextUtils.isEmpty(mName)) {
+ throw new IllegalArgumentException(
+ "Must set a non-null database name to a configuration that uses the "
+ + "no backup directory.");
+ }
+ return new Configuration(mContext, mName, mCallback, mUseNoBackUpDirectory);
}
Builder(@NonNull Context context) {
@@ -378,6 +412,18 @@
mCallback = callback;
return this;
}
+
+ /**
+ * Sets whether to use a no backup directory or not.
+ * @param useNoBackUpDirectory If {@code true} the database file will be stored in the
+ * no-backup directory.
+ * @return this
+ */
+ @NonNull
+ public Builder noBackupDirectory(boolean useNoBackUpDirectory) {
+ mUseNoBackUpDirectory = useNoBackUpDirectory;
+ return this;
+ }
}
}