[GH] Support opening gzipped databases.
This patch adds a method for creating a database from a gzipped file.
This is only implemented for external files, since assets are
transparently gzipped and extracted by the build tools.
Test: Added a unit test to SQLiteCopyOpenHelperTest.
Fixes: b/146911060
This is an imported pull request from https://github.com/androidx/androidx/pull/49.
Resolves #49
Github-Pr-Head-Sha: 568196d7b1800e9055de0dae7f509008cbc64c71
GitOrigin-RevId: 5de26bc6b6107d6aa2079f53d303e1216879ffa9
Change-Id: Ibd6b85f03748062a1ced22daf250944d5f07ef5b
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrepackageTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrepackageTest.java
index 03bce56..5f4e930 100644
--- a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrepackageTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/PrepackageTest.java
@@ -42,11 +42,17 @@
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.concurrent.Callable;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
@MediumTest
@RunWith(AndroidJUnit4.class)
@@ -68,6 +74,30 @@
}
@Test
+ public void createFromZippedAsset() {
+ Context context = ApplicationProvider.getApplicationContext();
+ context.deleteDatabase("products.db");
+
+ final Callable inputStreamCallable = () -> {
+ final ZipInputStream zipInputStream =
+ new ZipInputStream(
+ context.getAssets().open("databases/products_v1.db.zip"));
+ zipInputStream.getNextEntry();
+ return zipInputStream;
+ };
+
+ ProductsDatabase database = Room.databaseBuilder(
+ context, ProductsDatabase.class, "products.db")
+ .createFromInputStream(inputStreamCallable)
+ .build();
+
+ ProductDao dao = database.getProductDao();
+ assertThat(dao.countProducts(), is(2));
+
+ database.close();
+ }
+
+ @Test
public void createFromAsset_badSchema() {
Context context = ApplicationProvider.getApplicationContext();
context.deleteDatabase("products_badSchema.db");
@@ -418,6 +448,31 @@
}
@Test
+ public void createFromInputStream() throws IOException {
+ Context context = ApplicationProvider.getApplicationContext();
+ context.deleteDatabase("products_external.db");
+ File dataDbFile = new File(ContextCompat.getDataDir(context), "products_external.db.gz");
+ context.deleteDatabase(dataDbFile.getAbsolutePath());
+
+ InputStream toCopyInput = context.getAssets().open("databases/products_v1.db");
+
+ // gzip the file while copying it - note that gzipping files in assets doesn't work because
+ // aapt drops the gz extension and makes them available without requiring a GZip stream.
+ final OutputStream output = new GZIPOutputStream(new FileOutputStream(dataDbFile));
+ copyStream(toCopyInput, output);
+
+ ProductsDatabase database = Room.databaseBuilder(
+ context, ProductsDatabase.class, "products_external.db")
+ .createFromInputStream(() -> new GZIPInputStream(new FileInputStream(dataDbFile)))
+ .build();
+
+ ProductDao dao = database.getProductDao();
+ assertThat(dao.countProducts(), is(2));
+
+ database.close();
+ }
+
+ @Test
public void openDataDirDatabase() throws IOException {
Context context = ApplicationProvider.getApplicationContext();
@@ -525,6 +580,10 @@
private static void copyAsset(InputStream input, File outputFile) throws IOException {
OutputStream output = new FileOutputStream(outputFile);
+ copyStream(input, output);
+ }
+
+ private static void copyStream(InputStream input, OutputStream output) throws IOException {
try {
int length;
byte[] buffer = new byte[1024 * 4];
diff --git a/room/integration-tests/testapp/src/main/assets/databases/products_v1.db.zip b/room/integration-tests/testapp/src/main/assets/databases/products_v1.db.zip
new file mode 100644
index 0000000..761460c
--- /dev/null
+++ b/room/integration-tests/testapp/src/main/assets/databases/products_v1.db.zip
Binary files differ
diff --git a/room/runtime/api/current.txt b/room/runtime/api/current.txt
index 1d210e5..7322c59 100644
--- a/room/runtime/api/current.txt
+++ b/room/runtime/api/current.txt
@@ -73,6 +73,7 @@
method public T build();
method public androidx.room.RoomDatabase.Builder createFromAsset(String);
method public androidx.room.RoomDatabase.Builder createFromFile(java.io.File);
+ method public androidx.room.RoomDatabase.Builder createFromInputStream(java.util.concurrent.Callable);
method public androidx.room.RoomDatabase.Builder enableMultiInstanceInvalidation();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigration();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigrationFrom(int...);
diff --git a/room/runtime/api/public_plus_experimental_current.txt b/room/runtime/api/public_plus_experimental_current.txt
index 1d210e5..7322c59 100644
--- a/room/runtime/api/public_plus_experimental_current.txt
+++ b/room/runtime/api/public_plus_experimental_current.txt
@@ -73,6 +73,7 @@
method public T build();
method public androidx.room.RoomDatabase.Builder createFromAsset(String);
method public androidx.room.RoomDatabase.Builder createFromFile(java.io.File);
+ method public androidx.room.RoomDatabase.Builder createFromInputStream(java.util.concurrent.Callable);
method public androidx.room.RoomDatabase.Builder enableMultiInstanceInvalidation();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigration();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigrationFrom(int...);
diff --git a/room/runtime/api/restricted_current.txt b/room/runtime/api/restricted_current.txt
index c4bbcbe..5fef922 100644
--- a/room/runtime/api/restricted_current.txt
+++ b/room/runtime/api/restricted_current.txt
@@ -112,6 +112,7 @@
method public T build();
method public androidx.room.RoomDatabase.Builder createFromAsset(String);
method public androidx.room.RoomDatabase.Builder createFromFile(java.io.File);
+ method public androidx.room.RoomDatabase.Builder createFromInputStream(java.util.concurrent.Callable);
method public androidx.room.RoomDatabase.Builder enableMultiInstanceInvalidation();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigration();
method public androidx.room.RoomDatabase.Builder fallbackToDestructiveMigrationFrom(int...);
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index 8595be4..6dc4f75 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -42,6 +42,7 @@
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import java.io.File;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -56,6 +57,8 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
+import kotlin.Suppress;
+
/**
* Base class for all Room databases. All classes that are annotated with {@link Database} must
* extend this class.
@@ -565,6 +568,7 @@
private String mCopyFromAssetPath;
private File mCopyFromFile;
+ private Callable mInputStreamCallable;
Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) {
mContext = context;
@@ -625,6 +629,34 @@
}
/**
+ * Configures Room to create and open the database using an {@link InputStream}.
+ *
+ * Provide Room with a pre-packaged database via an {@link InputStream}. This is useful
+ * for processing compressed files. Room does not open the pre-packaged database, instead
+ * it copies it into the internal app database folder, and then opens it. The given file
+ * must be accessible and the right permissions must be granted for Room to copy the file.
+ *
+ * The pre-packaged database schema will be validated. It might be best to create your
+ * pre-packaged database schema utilizing the exported schema files generated when
+ * {@link Database#exportSchema()} is enabled.
+ *
+ * This method is not supported for an in memory database {@link Builder}. The underlying
+ * {@link InputStream} will be closed.
+ *
+ * @param inputStreamCallable A callable that returns an InputStream from which to copy
+ * the database.
+ *
+ * @return This {@link Builder} instance.
+ */
+ @SuppressLint("BuilderSetStyle")
+ @NonNull
+ public Builder createFromInputStream(
+ @NonNull Callable inputStreamCallable) {
+ mInputStreamCallable = inputStreamCallable;
+ return this;
+ }
+
+ /**
* Sets the database factory. If not set, it defaults to
* {@link FrameworkSQLiteOpenHelperFactory}.
*
@@ -919,18 +951,24 @@
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
- if (mCopyFromAssetPath != null || mCopyFromFile != null) {
+ if (mCopyFromAssetPath != null || mCopyFromFile != null
+ || mInputStreamCallable != null) {
if (mName == null) {
throw new IllegalArgumentException("Cannot create from asset or file for an "
+ "in-memory database.");
}
- if (mCopyFromAssetPath != null && mCopyFromFile != null) {
- throw new IllegalArgumentException("Both createFromAsset() and "
- + "createFromFile() was called on this Builder but the database can "
- + "only be created using one of the two configurations.");
+
+ final int copyConfigurations = (mCopyFromAssetPath == null ? 0 : 1) +
+ (mCopyFromFile == null ? 0 : 1) +
+ (mInputStreamCallable == null ? 0 : 1);
+ if (copyConfigurations != 1) {
+ throw new IllegalArgumentException("More than one of createFromAsset(), "
+ + "createFromInputStream(), and createFromFile() were called on this "
+ + "Builder, but the database can only be created using one of the "
+ + "three configurations.");
}
mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
- mFactory);
+ mInputStreamCallable, mFactory);
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(
diff --git a/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.java b/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.java
index f8240ce..f6c9b9c 100644
--- a/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.java
+++ b/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.java
@@ -33,9 +33,13 @@
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.InvalidObjectException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
+import java.util.concurrent.Callable;
+import java.util.zip.GZIPInputStream;
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
@@ -49,6 +53,8 @@
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
+ @Nullable
+ private final Callable mInputStreamCallable;
private final int mDatabaseVersion;
@NonNull
private final SupportSQLiteOpenHelper mDelegate;
@@ -61,11 +67,13 @@
@NonNull Context context,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
+ @Nullable Callable inputStreamCallable,
int databaseVersion,
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
mContext = context;
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
+ mInputStreamCallable = inputStreamCallable;
mDatabaseVersion = databaseVersion;
mDelegate = supportSQLiteOpenHelper;
}
@@ -178,6 +186,14 @@
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
} else if (mCopyFromFile != null) {
input = new FileInputStream(mCopyFromFile).getChannel();
+ } else if (mInputStreamCallable != null) {
+ final InputStream inputStream;
+ try {
+ inputStream = mInputStreamCallable.call();
+ } catch (Exception e) {
+ throw new IllegalStateException("inputStreamCallable exception on call", e);
+ }
+ input = Channels.newChannel(inputStream);
} else {
throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
}
diff --git a/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java b/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java
index 22178e8..df9bcb6 100644
--- a/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java
+++ b/room/runtime/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java
@@ -21,6 +21,8 @@
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.Callable;
/**
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
@@ -32,24 +34,29 @@
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
+ private final Callable mInputStreamCallable;
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
SQLiteCopyOpenHelperFactory(
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
+ @Nullable Callable inputStreamCallable,
@NonNull SupportSQLiteOpenHelper.Factory factory) {
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
+ mInputStreamCallable = inputStreamCallable;
mDelegate = factory;
}
+ @NonNull
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new SQLiteCopyOpenHelper(
configuration.context,
mCopyFromAssetPath,
mCopyFromFile,
+ mInputStreamCallable,
configuration.callback.version,
mDelegate.create(configuration));
}
diff --git a/room/runtime/src/test/java/androidx/room/BuilderTest.java b/room/runtime/src/test/java/androidx/room/BuilderTest.java
index 6252178..ae6d83b 100644
--- a/room/runtime/src/test/java/androidx/room/BuilderTest.java
+++ b/room/runtime/src/test/java/androidx/room/BuilderTest.java
@@ -442,8 +442,9 @@
}
assertThat(exception, instanceOf(IllegalArgumentException.class));
assertThat(exception.getMessage(),
- containsString("Both createFromAsset() and createFromFile() was called on "
- + "this Builder"));
+ containsString(
+ "More than one of createFromAsset(), createFromInputStream(), and "
+ + "createFromFile() were called on this Builder"));
}
@Test
diff --git a/room/runtime/src/test/java/androidx/room/SQLiteCopyOpenHelperTest.kt b/room/runtime/src/test/java/androidx/room/SQLiteCopyOpenHelperTest.kt
index 91d15b3..46ee269 100644
--- a/room/runtime/src/test/java/androidx/room/SQLiteCopyOpenHelperTest.kt
+++ b/room/runtime/src/test/java/androidx/room/SQLiteCopyOpenHelperTest.kt
@@ -207,6 +207,7 @@
context,
copyFromAssetFile.name,
null,
+ null,
DB_VERSION,
delegate
).apply { setDatabaseConfiguration(configuration) }