diff --git a/app/build.gradle b/app/build.gradle
index 2fc147223d..2ef95f5970 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -40,6 +40,11 @@ android {
abortOnError false
}
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
packagingOptions {
exclude 'META-INF/LICENSE.txt'
exclude 'META-INF/README.md'
@@ -132,6 +137,10 @@ repositories {
}
configurations.all {
+ // Workaround https://issuetracker.google.com/issues/138441698
+ // Support @69c481c39a17d4e1e44a4eb298bb81c48f226eef
+ exclude group: "androidx.room", module: "room-runtime"
+ // Workaround https://issuetracker.google.com/issues/134685570
exclude group: "androidx.lifecycle", module: "lifecycle-livedata"
}
@@ -195,6 +204,9 @@ dependencies {
// https://mvnrepository.com/artifact/androidx.room/room-runtime
implementation "androidx.room:room-runtime:$room_version"
+ implementation "androidx.room:room-common:$room_version" // because of exclude
+ // https://mvnrepository.com/artifact/androidx.sqlite/sqlite-framework
+ implementation "androidx.sqlite:sqlite-framework:2.0.1" // because of exclude
annotationProcessor "androidx.room:room-compiler:$room_version"
// https://mvnrepository.com/artifact/androidx.paging/paging-runtime
diff --git a/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationCallback.aidl b/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationCallback.aidl
new file mode 100644
index 0000000000..7c702fff1b
--- /dev/null
+++ b/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2018 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;
+
+/**
+ * RPC Callbacks for {@link IMultiInstanceInvalidationService}.
+ *
+ * @hide
+ */
+interface IMultiInstanceInvalidationCallback {
+
+ /**
+ * Called when invalidation is detected in another instance of the same database.
+ *
+ * @param tables List of invalidated table names
+ */
+ oneway void onInvalidation(in String[] tables);
+
+}
diff --git a/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationService.aidl b/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationService.aidl
new file mode 100644
index 0000000000..3b2c18c5ab
--- /dev/null
+++ b/app/src/main/aidl/androidx/room/IMultiInstanceInvalidationService.aidl
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2018 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;
+
+import androidx.room.IMultiInstanceInvalidationCallback;
+
+/**
+ * RPC Service that controls interaction about multi-instance invalidation.
+ *
+ * @hide
+ */
+interface IMultiInstanceInvalidationService {
+
+ /**
+ * Registers a new {@link IMultiInstanceInvalidationCallback} as a client of this service.
+ *
+ * @param callback The RPC callback.
+ * @param name The name of the database file as it is passed to {@link RoomDatabase.Builder}.
+ * @return A new client ID. The client needs to hold on to this ID and pass it to the service
+ * for subsequent calls.
+ */
+ int registerCallback(IMultiInstanceInvalidationCallback callback, String name);
+
+ /**
+ * Unregisters the specified {@link IMultiInstanceInvalidationCallback} from this service.
+ *
+ * Clients might die without explicitly calling this method. In that case, the service should
+ * handle the clean up.
+ *
+ * @param callback The RPC callback.
+ * @param clientId The client ID returned from {@link #registerCallback}.
+ */
+ void unregisterCallback(IMultiInstanceInvalidationCallback callback, int clientId);
+
+ /**
+ * Broadcasts invalidation of database tables to other clients registered to this service.
+ *
+ * The broadcast is delivered to {@link IMultiInstanceInvalidationCallback#onInvalidation} of
+ * the registered clients. The client calling this method will not receive its own broadcast.
+ * Clients that are associated with a different database file will not be notified.
+ *
+ * @param clientId The client ID returned from {@link #registerCallback}.
+ * @param tables The names of invalidated tables.
+ */
+ oneway void broadcastInvalidation(int clientId, in String[] tables);
+
+}
diff --git a/app/src/main/java/androidx/room/DatabaseConfiguration.java b/app/src/main/java/androidx/room/DatabaseConfiguration.java
new file mode 100644
index 0000000000..f994445016
--- /dev/null
+++ b/app/src/main/java/androidx/room/DatabaseConfiguration.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Configuration class for a {@link RoomDatabase}.
+ */
+@SuppressWarnings("WeakerAccess")
+public class DatabaseConfiguration {
+
+ /**
+ * The factory to use to access the database.
+ */
+ @NonNull
+ public final SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory;
+ /**
+ * The context to use while connecting to the database.
+ */
+ @NonNull
+ public final Context context;
+ /**
+ * The name of the database file or null if it is an in-memory database.
+ */
+ @Nullable
+ public final String name;
+
+ /**
+ * Collection of available migrations.
+ */
+ @NonNull
+ public final RoomDatabase.MigrationContainer migrationContainer;
+
+ @Nullable
+ public final List callbacks;
+
+ /**
+ * Whether Room should throw an exception for queries run on the main thread.
+ */
+ public final boolean allowMainThreadQueries;
+
+ /**
+ * The journal mode for this database.
+ */
+ public final RoomDatabase.JournalMode journalMode;
+
+ /**
+ * The Executor used to execute asynchronous queries.
+ */
+ @NonNull
+ public final Executor queryExecutor;
+
+ /**
+ * The Executor used to execute asynchronous transactions.
+ */
+ @NonNull
+ public final Executor transactionExecutor;
+
+ /**
+ * If true, table invalidation in an instance of {@link RoomDatabase} is broadcast and
+ * synchronized with other instances of the same {@link RoomDatabase} file, including those
+ * in a separate process.
+ */
+ public final boolean multiInstanceInvalidation;
+
+ /**
+ * If true, Room should crash if a migration is missing.
+ */
+ public final boolean requireMigration;
+
+ /**
+ * If true, Room should perform a destructive migration when downgrading without an available
+ * migration.
+ */
+ public final boolean allowDestructiveMigrationOnDowngrade;
+
+ /**
+ * The collection of schema versions from which migrations aren't required.
+ */
+ private final Set mMigrationNotRequiredFrom;
+
+ /**
+ * The assets path to a pre-packaged database to copy from.
+ */
+ @Nullable
+ public final String copyFromAssetPath;
+
+ /**
+ * The pre-packaged database file to copy from.
+ */
+ @Nullable
+ public final File copyFromFile;
+
+
+ /**
+ * Creates a database configuration with the given values.
+ *
+ * @deprecated Use {@link #DatabaseConfiguration(Context, String,
+ * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
+ * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
+ *
+ * @param context The application context.
+ * @param name Name of the database, can be null if it is in memory.
+ * @param sqliteOpenHelperFactory The open helper factory to use.
+ * @param migrationContainer The migration container for migrations.
+ * @param callbacks The list of callbacks for database events.
+ * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
+ * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
+ * @param queryExecutor The Executor used to execute asynchronous queries.
+ * @param requireMigration True if Room should require a valid migration if version changes,
+ * instead of recreating the tables.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+ @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
+ @NonNull RoomDatabase.MigrationContainer migrationContainer,
+ @Nullable List callbacks,
+ boolean allowMainThreadQueries,
+ RoomDatabase.JournalMode journalMode,
+ @NonNull Executor queryExecutor,
+ boolean requireMigration,
+ @Nullable Set migrationNotRequiredFrom) {
+ this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
+ allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
+ requireMigration, false, migrationNotRequiredFrom, null, null);
+ }
+
+ /**
+ * Creates a database configuration with the given values.
+ *
+ * @deprecated Use {@link #DatabaseConfiguration(Context, String,
+ * SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
+ * RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
+ *
+ * @param context The application context.
+ * @param name Name of the database, can be null if it is in memory.
+ * @param sqliteOpenHelperFactory The open helper factory to use.
+ * @param migrationContainer The migration container for migrations.
+ * @param callbacks The list of callbacks for database events.
+ * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
+ * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
+ * @param queryExecutor The Executor used to execute asynchronous queries.
+ * @param transactionExecutor The Executor used to execute asynchronous transactions.
+ * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
+ * @param requireMigration True if Room should require a valid migration if version changes,
+ * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
+ * migration is supplied during a downgrade.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
+ *
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+ @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
+ @NonNull RoomDatabase.MigrationContainer migrationContainer,
+ @Nullable List callbacks,
+ boolean allowMainThreadQueries,
+ RoomDatabase.JournalMode journalMode,
+ @NonNull Executor queryExecutor,
+ @NonNull Executor transactionExecutor,
+ boolean multiInstanceInvalidation,
+ boolean requireMigration,
+ boolean allowDestructiveMigrationOnDowngrade,
+ @Nullable Set migrationNotRequiredFrom) {
+ this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
+ allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
+ multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
+ migrationNotRequiredFrom, null, null);
+ }
+
+ /**
+ * Creates a database configuration with the given values.
+ *
+ * @param context The application context.
+ * @param name Name of the database, can be null if it is in memory.
+ * @param sqliteOpenHelperFactory The open helper factory to use.
+ * @param migrationContainer The migration container for migrations.
+ * @param callbacks The list of callbacks for database events.
+ * @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
+ * @param journalMode The journal mode. This has to be either TRUNCATE or WRITE_AHEAD_LOGGING.
+ * @param queryExecutor The Executor used to execute asynchronous queries.
+ * @param transactionExecutor The Executor used to execute asynchronous transactions.
+ * @param multiInstanceInvalidation True if Room should perform multi-instance invalidation.
+ * @param requireMigration True if Room should require a valid migration if version changes,
+ * @param allowDestructiveMigrationOnDowngrade True if Room should recreate tables if no
+ * migration is supplied during a downgrade.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
+ * @param copyFromAssetPath The assets path to the pre-packaged database.
+ * @param copyFromFile The pre-packaged database file.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
+ @NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
+ @NonNull RoomDatabase.MigrationContainer migrationContainer,
+ @Nullable List callbacks,
+ boolean allowMainThreadQueries,
+ RoomDatabase.JournalMode journalMode,
+ @NonNull Executor queryExecutor,
+ @NonNull Executor transactionExecutor,
+ boolean multiInstanceInvalidation,
+ boolean requireMigration,
+ boolean allowDestructiveMigrationOnDowngrade,
+ @Nullable Set migrationNotRequiredFrom,
+ @Nullable String copyFromAssetPath,
+ @Nullable File copyFromFile) {
+ this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
+ this.context = context;
+ this.name = name;
+ this.migrationContainer = migrationContainer;
+ this.callbacks = callbacks;
+ this.allowMainThreadQueries = allowMainThreadQueries;
+ this.journalMode = journalMode;
+ this.queryExecutor = queryExecutor;
+ this.transactionExecutor = transactionExecutor;
+ this.multiInstanceInvalidation = multiInstanceInvalidation;
+ this.requireMigration = requireMigration;
+ this.allowDestructiveMigrationOnDowngrade = allowDestructiveMigrationOnDowngrade;
+ this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
+ this.copyFromAssetPath = copyFromAssetPath;
+ this.copyFromFile = copyFromFile;
+ }
+
+ /**
+ * Returns whether a migration is required from the specified version.
+ *
+ * @param version The schema version.
+ * @return True if a valid migration is required, false otherwise.
+ *
+ * @deprecated Use {@link #isMigrationRequired(int, int)} which takes
+ * {@link #allowDestructiveMigrationOnDowngrade} into account.
+ */
+ @Deprecated
+ public boolean isMigrationRequiredFrom(int version) {
+ return isMigrationRequired(version, version + 1);
+ }
+
+ /**
+ * Returns whether a migration is required between two versions.
+ *
+ * @param fromVersion The old schema version.
+ * @param toVersion The new schema version.
+ * @return True if a valid migration is required, false otherwise.
+ */
+ public boolean isMigrationRequired(int fromVersion, int toVersion) {
+ // Migrations are not required if its a downgrade AND destructive migration during downgrade
+ // has been allowed.
+ final boolean isDowngrade = fromVersion > toVersion;
+ if (isDowngrade && allowDestructiveMigrationOnDowngrade) {
+ return false;
+ }
+
+ // Migrations are required between the two versions if we generally require migrations
+ // AND EITHER there are no exceptions OR the supplied fromVersion is not one of the
+ // exceptions.
+ return requireMigration
+ && (mMigrationNotRequiredFrom == null
+ || !mMigrationNotRequiredFrom.contains(fromVersion));
+ }
+}
diff --git a/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java
new file mode 100644
index 0000000000..154103cfcb
--- /dev/null
+++ b/app/src/main/java/androidx/room/EntityDeletionOrUpdateAdapter.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+/**
+ * Implementations of this class knows how to delete or update a particular entity.
+ *
+ * This is an internal library class and all of its implementations are auto-generated.
+ *
+ * @param The type parameter of the entity to be deleted
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@SuppressWarnings({"WeakerAccess", "unused"})
+public abstract class EntityDeletionOrUpdateAdapter extends SharedSQLiteStatement {
+ /**
+ * Creates a DeletionOrUpdateAdapter that can delete or update the entity type T on the given
+ * database.
+ *
+ * @param database The database to delete / update the item in.
+ */
+ public EntityDeletionOrUpdateAdapter(RoomDatabase database) {
+ super(database);
+ }
+
+ /**
+ * Create the deletion or update query
+ *
+ * @return An SQL query that can delete or update instances of T.
+ */
+ @Override
+ protected abstract String createQuery();
+
+ /**
+ * Binds the entity into the given statement.
+ *
+ * @param statement The SQLite statement that prepared for the query returned from
+ * createQuery.
+ * @param entity The entity of type T.
+ */
+ protected abstract void bind(SupportSQLiteStatement statement, T entity);
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entity The entity to delete or update
+ * @return The number of affected rows
+ */
+ public final int handle(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ return stmt.executeUpdateDelete();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entities Entities to delete or update
+ * @return The number of affected rows
+ */
+ public final int handleMultiple(Iterable extends T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ int total = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ total += stmt.executeUpdateDelete();
+ }
+ return total;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Deletes or updates the given entities in the database and returns the affected row count.
+ *
+ * @param entities Entities to delete or update
+ * @return The number of affected rows
+ */
+ public final int handleMultiple(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ int total = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ total += stmt.executeUpdateDelete();
+ }
+ return total;
+ } finally {
+ release(stmt);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/EntityInsertionAdapter.java b/app/src/main/java/androidx/room/EntityInsertionAdapter.java
new file mode 100644
index 0000000000..3046d6cd0b
--- /dev/null
+++ b/app/src/main/java/androidx/room/EntityInsertionAdapter.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Implementations of this class knows how to insert a particular entity.
+ *
+ * This is an internal library class and all of its implementations are auto-generated.
+ *
+ * @param The type parameter of the entity to be inserted
+ * @hide
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class EntityInsertionAdapter extends SharedSQLiteStatement {
+ /**
+ * Creates an InsertionAdapter that can insert the entity type T into the given database.
+ *
+ * @param database The database to insert into.
+ */
+ public EntityInsertionAdapter(RoomDatabase database) {
+ super(database);
+ }
+
+ /**
+ * Binds the entity into the given statement.
+ *
+ * @param statement The SQLite statement that prepared for the query returned from
+ * createInsertQuery.
+ * @param entity The entity of type T.
+ */
+ protected abstract void bind(SupportSQLiteStatement statement, T entity);
+
+ /**
+ * Inserts the entity into the database.
+ *
+ * @param entity The entity to insert
+ */
+ public final void insert(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database.
+ *
+ * @param entities Entities to insert
+ */
+ public final void insert(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ for (T entity : entities) {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ }
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database.
+ *
+ * @param entities Entities to insert
+ */
+ public final void insert(Iterable extends T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ for (T entity : entities) {
+ bind(stmt, entity);
+ stmt.executeInsert();
+ }
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entity into the database and returns the row id.
+ *
+ * @param entity The entity to insert
+ * @return The SQLite row id or -1 if no row is inserted
+ */
+ public final long insertAndReturnId(T entity) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ bind(stmt, entity);
+ return stmt.executeInsert();
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final long[] insertAndReturnIdsArray(Collection extends T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final long[] result = new long[entities.size()];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final long[] insertAndReturnIdsArray(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final long[] result = new long[entities.length];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final Long[] insertAndReturnIdsArrayBox(Collection extends T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final Long[] result = new Long[entities.size()];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final Long[] insertAndReturnIdsArrayBox(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final Long[] result = new Long[entities.length];
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result[index] = stmt.executeInsert();
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final List insertAndReturnIdsList(T[] entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final List result = new ArrayList<>(entities.length);
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result.add(index, stmt.executeInsert());
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+
+ /**
+ * Inserts the given entities into the database and returns the row ids.
+ *
+ * @param entities Entities to insert
+ * @return The SQLite row ids, for entities that are not inserted the row id returned will be -1
+ */
+ public final List insertAndReturnIdsList(Collection extends T> entities) {
+ final SupportSQLiteStatement stmt = acquire();
+ try {
+ final List result = new ArrayList<>(entities.size());
+ int index = 0;
+ for (T entity : entities) {
+ bind(stmt, entity);
+ result.add(index, stmt.executeInsert());
+ index++;
+ }
+ return result;
+ } finally {
+ release(stmt);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java
new file mode 100644
index 0000000000..e1e8155f8a
--- /dev/null
+++ b/app/src/main/java/androidx/room/InvalidationLiveDataContainer.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 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;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.LiveData;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+/**
+ * A helper class that maintains {@link RoomTrackingLiveData} instances for an
+ * {@link InvalidationTracker}.
+ *
+ * We keep a strong reference to active LiveData instances to avoid garbage collection in case
+ * developer does not hold onto the returned LiveData.
+ */
+class InvalidationLiveDataContainer {
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final Set mLiveDataSet = Collections.newSetFromMap(
+ new IdentityHashMap()
+ );
+ private final RoomDatabase mDatabase;
+
+ InvalidationLiveDataContainer(RoomDatabase database) {
+ mDatabase = database;
+ }
+
+ LiveData create(String[] tableNames, boolean inTransaction,
+ Callable computeFunction) {
+ return new RoomTrackingLiveData<>(mDatabase, this, inTransaction, computeFunction,
+ tableNames);
+ }
+
+ void onActive(LiveData liveData) {
+ mLiveDataSet.add(liveData);
+ }
+
+ void onInactive(LiveData liveData) {
+ mLiveDataSet.remove(liveData);
+ }
+}
diff --git a/app/src/main/java/androidx/room/InvalidationTracker.java b/app/src/main/java/androidx/room/InvalidationTracker.java
new file mode 100644
index 0000000000..f35b7f9550
--- /dev/null
+++ b/app/src/main/java/androidx/room/InvalidationTracker.java
@@ -0,0 +1,853 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.internal.SafeIterableMap;
+import androidx.lifecycle.LiveData;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+
+/**
+ * InvalidationTracker keeps a list of tables modified by queries and notifies its callbacks about
+ * these tables.
+ */
+// Some details on how the InvalidationTracker works:
+// * An in memory table is created with (table_id, invalidated) table_id is a hardcoded int from
+// initialization, while invalidated is a boolean bit to indicate if the table has been invalidated.
+// * ObservedTableTracker tracks list of tables we should be watching (e.g. adding triggers for).
+// * Before each beginTransaction, RoomDatabase invokes InvalidationTracker to sync trigger states.
+// * After each endTransaction, RoomDatabase invokes InvalidationTracker to refresh invalidated
+// tables.
+// * Each update (write operation) on one of the observed tables triggers an update into the
+// memory table table, flipping the invalidated flag ON.
+// * When multi-instance invalidation is turned on, MultiInstanceInvalidationClient will be created.
+// It works as an Observer, and notifies other instances of table invalidation.
+public class InvalidationTracker {
+
+ private static final String[] TRIGGERS = new String[]{"UPDATE", "DELETE", "INSERT"};
+
+ private static final String UPDATE_TABLE_NAME = "room_table_modification_log";
+
+ private static final String TABLE_ID_COLUMN_NAME = "table_id";
+
+ private static final String INVALIDATED_COLUMN_NAME = "invalidated";
+
+ private static final String CREATE_TRACKING_TABLE_SQL = "CREATE TEMP TABLE " + UPDATE_TABLE_NAME
+ + "(" + TABLE_ID_COLUMN_NAME + " INTEGER PRIMARY KEY, "
+ + INVALIDATED_COLUMN_NAME + " INTEGER NOT NULL DEFAULT 0)";
+
+ @VisibleForTesting
+ static final String RESET_UPDATED_TABLES_SQL = "UPDATE " + UPDATE_TABLE_NAME
+ + " SET " + INVALIDATED_COLUMN_NAME + " = 0 WHERE " + INVALIDATED_COLUMN_NAME + " = 1 ";
+
+ @VisibleForTesting
+ static final String SELECT_UPDATED_TABLES_SQL = "SELECT * FROM " + UPDATE_TABLE_NAME
+ + " WHERE " + INVALIDATED_COLUMN_NAME + " = 1;";
+
+ @NonNull
+ @VisibleForTesting
+ final HashMap mTableIdLookup;
+ final String[] mTableNames;
+
+ @NonNull
+ private Map> mViewTables;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ final RoomDatabase mDatabase;
+
+ AtomicBoolean mPendingRefresh = new AtomicBoolean(false);
+
+ private volatile boolean mInitialized = false;
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ volatile SupportSQLiteStatement mCleanupStatement;
+
+ private ObservedTableTracker mObservedTableTracker;
+
+ private final InvalidationLiveDataContainer mInvalidationLiveDataContainer;
+
+ // should be accessed with synchronization only.
+ @VisibleForTesting
+ @SuppressLint("RestrictedApi")
+ final SafeIterableMap mObserverMap = new SafeIterableMap<>();
+
+ private MultiInstanceInvalidationClient mMultiInstanceInvalidationClient;
+
+ /**
+ * Used by the generated code.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public InvalidationTracker(RoomDatabase database, String... tableNames) {
+ this(database, new HashMap(), Collections.>emptyMap(),
+ tableNames);
+ }
+
+ /**
+ * Used by the generated code.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public InvalidationTracker(RoomDatabase database, Map shadowTablesMap,
+ Map> viewTables, String... tableNames) {
+ mDatabase = database;
+ mObservedTableTracker = new ObservedTableTracker(tableNames.length);
+ mTableIdLookup = new HashMap<>();
+ mViewTables = viewTables;
+ mInvalidationLiveDataContainer = new InvalidationLiveDataContainer(mDatabase);
+ final int size = tableNames.length;
+ mTableNames = new String[size];
+ for (int id = 0; id < size; id++) {
+ final String tableName = tableNames[id].toLowerCase(Locale.US);
+ mTableIdLookup.put(tableName, id);
+ String shadowTableName = shadowTablesMap.get(tableNames[id]);
+ if (shadowTableName != null) {
+ mTableNames[id] = shadowTableName.toLowerCase(Locale.US);
+ } else {
+ mTableNames[id] = tableName;
+ }
+ }
+ // Adjust table id lookup for those tables whose shadow table is another already mapped
+ // table (e.g. external content fts tables).
+ for (Map.Entry shadowTableEntry : shadowTablesMap.entrySet()) {
+ String shadowTableName = shadowTableEntry.getValue().toLowerCase(Locale.US);
+ if (mTableIdLookup.containsKey(shadowTableName)) {
+ String tableName = shadowTableEntry.getKey().toLowerCase(Locale.US);
+ mTableIdLookup.put(tableName, mTableIdLookup.get(shadowTableName));
+ }
+ }
+ }
+
+ /**
+ * Internal method to initialize table tracking.
+ *
+ * You should never call this method, it is called by the generated code.
+ */
+ void internalInit(SupportSQLiteDatabase database) {
+ synchronized (this) {
+ if (mInitialized) {
+ Log.e(Room.LOG_TAG, "Invalidation tracker is initialized twice :/.");
+ return;
+ }
+
+ // These actions are not in a transaction because temp_store is not allowed to be
+ // performed on a transaction, and recursive_triggers is not affected by transactions.
+ database.execSQL("PRAGMA temp_store = MEMORY;");
+ database.execSQL("PRAGMA recursive_triggers='ON';");
+ database.execSQL(CREATE_TRACKING_TABLE_SQL);
+ syncTriggers(database);
+ mCleanupStatement = database.compileStatement(RESET_UPDATED_TABLES_SQL);
+ mInitialized = true;
+ }
+ }
+
+ void startMultiInstanceInvalidation(Context context, String name) {
+ mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this,
+ mDatabase.getQueryExecutor());
+ }
+
+ void stopMultiInstanceInvalidation() {
+ if (mMultiInstanceInvalidationClient != null) {
+ mMultiInstanceInvalidationClient.stop();
+ mMultiInstanceInvalidationClient = null;
+ }
+ }
+
+ private static void appendTriggerName(StringBuilder builder, String tableName,
+ String triggerType) {
+ builder.append("`")
+ .append("room_table_modification_trigger_")
+ .append(tableName)
+ .append("_")
+ .append(triggerType)
+ .append("`");
+ }
+
+ private void stopTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
+ final String tableName = mTableNames[tableId];
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String trigger : TRIGGERS) {
+ stringBuilder.setLength(0);
+ stringBuilder.append("DROP TRIGGER IF EXISTS ");
+ appendTriggerName(stringBuilder, tableName, trigger);
+ writableDb.execSQL(stringBuilder.toString());
+ }
+ }
+
+ private void startTrackingTable(SupportSQLiteDatabase writableDb, int tableId) {
+ writableDb.execSQL(
+ "INSERT OR IGNORE INTO " + UPDATE_TABLE_NAME + " VALUES(" + tableId + ", 0)");
+ final String tableName = mTableNames[tableId];
+ StringBuilder stringBuilder = new StringBuilder();
+ for (String trigger : TRIGGERS) {
+ stringBuilder.setLength(0);
+ stringBuilder.append("CREATE TEMP TRIGGER IF NOT EXISTS ");
+ appendTriggerName(stringBuilder, tableName, trigger);
+ stringBuilder.append(" AFTER ")
+ .append(trigger)
+ .append(" ON `")
+ .append(tableName)
+ .append("` BEGIN UPDATE ")
+ .append(UPDATE_TABLE_NAME)
+ .append(" SET ").append(INVALIDATED_COLUMN_NAME).append(" = 1")
+ .append(" WHERE ").append(TABLE_ID_COLUMN_NAME).append(" = ").append(tableId)
+ .append(" AND ").append(INVALIDATED_COLUMN_NAME).append(" = 0")
+ .append("; END");
+ writableDb.execSQL(stringBuilder.toString());
+ }
+ }
+
+ /**
+ * Adds the given observer to the observers list and it will be notified if any table it
+ * observes changes.
+ *
+ * Database changes are pulled on another thread so in some race conditions, the observer might
+ * be invoked for changes that were done before it is added.
+ *
+ * If the observer already exists, this is a no-op call.
+ *
+ * If one of the tables in the Observer does not exist in the database, this method throws an
+ * {@link IllegalArgumentException}.
+ *
+ * @param observer The observer which listens the database for changes.
+ */
+ @SuppressLint("RestrictedApi")
+ @WorkerThread
+ public void addObserver(@NonNull Observer observer) {
+ final String[] tableNames = resolveViews(observer.mTables);
+ int[] tableIds = new int[tableNames.length];
+ final int size = tableNames.length;
+
+ for (int i = 0; i < size; i++) {
+ Integer tableId = mTableIdLookup.get(tableNames[i].toLowerCase(Locale.US));
+ if (tableId == null) {
+ throw new IllegalArgumentException("There is no table with name " + tableNames[i]);
+ }
+ tableIds[i] = tableId;
+ }
+ ObserverWrapper wrapper = new ObserverWrapper(observer, tableIds, tableNames);
+ ObserverWrapper currentObserver;
+ synchronized (mObserverMap) {
+ currentObserver = mObserverMap.putIfAbsent(observer, wrapper);
+ }
+ if (currentObserver == null && mObservedTableTracker.onAdded(tableIds)) {
+ syncTriggers();
+ }
+ }
+
+ private String[] validateAndResolveTableNames(String[] tableNames) {
+ String[] resolved = resolveViews(tableNames);
+ for (String tableName : resolved) {
+ if (!mTableIdLookup.containsKey(tableName.toLowerCase(Locale.US))) {
+ throw new IllegalArgumentException("There is no table with name " + tableName);
+ }
+ }
+ return resolved;
+ }
+
+ /**
+ * Resolves the list of tables and views into a list of unique tables that are underlying them.
+ *
+ * @param names The names of tables or views.
+ * @return The names of the underlying tables.
+ */
+ private String[] resolveViews(String[] names) {
+ Set tables = new HashSet<>();
+ for (String name : names) {
+ final String lowercase = name.toLowerCase(Locale.US);
+ if (mViewTables.containsKey(lowercase)) {
+ tables.addAll(mViewTables.get(lowercase));
+ } else {
+ tables.add(name);
+ }
+ }
+ return tables.toArray(new String[tables.size()]);
+ }
+
+ /**
+ * Adds an observer but keeps a weak reference back to it.
+ *
+ * Note that you cannot remove this observer once added. It will be automatically removed
+ * when the observer is GC'ed.
+ *
+ * @param observer The observer to which InvalidationTracker will keep a weak reference.
+ * @hide
+ */
+ @SuppressWarnings("unused")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public void addWeakObserver(Observer observer) {
+ addObserver(new WeakObserver(this, observer));
+ }
+
+ /**
+ * Removes the observer from the observers list.
+ *
+ * @param observer The observer to remove.
+ */
+ @SuppressLint("RestrictedApi")
+ @SuppressWarnings("WeakerAccess")
+ @WorkerThread
+ public void removeObserver(@NonNull final Observer observer) {
+ ObserverWrapper wrapper;
+ synchronized (mObserverMap) {
+ wrapper = mObserverMap.remove(observer);
+ }
+ if (wrapper != null && mObservedTableTracker.onRemoved(wrapper.mTableIds)) {
+ syncTriggers();
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ boolean ensureInitialization() {
+ if (!mDatabase.isOpen()) {
+ return false;
+ }
+ if (!mInitialized) {
+ // trigger initialization
+ mDatabase.getOpenHelper().getWritableDatabase();
+ }
+ if (!mInitialized) {
+ Log.e(Room.LOG_TAG, "database is not initialized even though it is open");
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ Runnable mRefreshRunnable = new Runnable() {
+ @Override
+ public void run() {
+ final Lock closeLock = mDatabase.getCloseLock();
+ Set invalidatedTableIds = null;
+ try {
+ closeLock.lock();
+
+ if (!ensureInitialization()) {
+ return;
+ }
+
+ if (!mPendingRefresh.compareAndSet(true, false)) {
+ // no pending refresh
+ return;
+ }
+
+ if (mDatabase.inTransaction()) {
+ // current thread is in a transaction. when it ends, it will invoke
+ // refreshRunnable again. mPendingRefresh is left as false on purpose
+ // so that the last transaction can flip it on again.
+ return;
+ }
+
+ if (mDatabase.mWriteAheadLoggingEnabled) {
+ // This transaction has to be on the underlying DB rather than the RoomDatabase
+ // in order to avoid a recursive loop after endTransaction.
+ SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
+ db.beginTransaction();
+ try {
+ invalidatedTableIds = checkUpdatedTable();
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ } else {
+ invalidatedTableIds = checkUpdatedTable();
+ }
+ } catch (IllegalStateException | SQLiteException exception) {
+ // may happen if db is closed. just log.
+ Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ exception);
+ } finally {
+ closeLock.unlock();
+ }
+ if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
+ synchronized (mObserverMap) {
+ for (Map.Entry entry : mObserverMap) {
+ entry.getValue().notifyByTableInvalidStatus(invalidatedTableIds);
+ }
+ }
+ }
+ }
+
+ private Set checkUpdatedTable() {
+ HashSet invalidatedTableIds = new HashSet<>();
+ Cursor cursor = mDatabase.query(new SimpleSQLiteQuery(SELECT_UPDATED_TABLES_SQL));
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ while (cursor.moveToNext()) {
+ final int tableId = cursor.getInt(0);
+ invalidatedTableIds.add(tableId);
+ }
+ } finally {
+ cursor.close();
+ }
+ if (!invalidatedTableIds.isEmpty()) {
+ mCleanupStatement.executeUpdateDelete();
+ }
+ return invalidatedTableIds;
+ }
+ };
+
+ /**
+ * Enqueues a task to refresh the list of updated tables.
+ *
+ * This method is automatically called when {@link RoomDatabase#endTransaction()} is called but
+ * if you have another connection to the database or directly use {@link
+ * SupportSQLiteDatabase}, you may need to call this manually.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void refreshVersionsAsync() {
+ // TODO we should consider doing this sync instead of async.
+ if (mPendingRefresh.compareAndSet(false, true)) {
+ mDatabase.getQueryExecutor().execute(mRefreshRunnable);
+ }
+ }
+
+ /**
+ * Check versions for tables, and run observers synchronously if tables have been updated.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ @WorkerThread
+ public void refreshVersionsSync() {
+ syncTriggers();
+ mRefreshRunnable.run();
+ }
+
+ /**
+ * Notifies all the registered {@link Observer}s of table changes.
+ *
+ * This can be used for notifying invalidation that cannot be detected by this
+ * {@link InvalidationTracker}, for example, invalidation from another process.
+ *
+ * @param tables The invalidated tables.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public void notifyObserversByTableNames(String... tables) {
+ synchronized (mObserverMap) {
+ for (Map.Entry entry : mObserverMap) {
+ if (!entry.getKey().isRemote()) {
+ entry.getValue().notifyByTableNames(tables);
+ }
+ }
+ }
+ }
+
+ void syncTriggers(SupportSQLiteDatabase database) {
+ if (database.inTransaction()) {
+ // we won't run this inside another transaction.
+ return;
+ }
+ try {
+ // This method runs in a while loop because while changes are synced to db, another
+ // runnable may be skipped. If we cause it to skip, we need to do its work.
+ while (true) {
+ Lock closeLock = mDatabase.getCloseLock();
+ closeLock.lock();
+ try {
+ // there is a potential race condition where another mSyncTriggers runnable
+ // can start running right after we get the tables list to sync.
+ final int[] tablesToSync = mObservedTableTracker.getTablesToSync();
+ if (tablesToSync == null) {
+ return;
+ }
+ final int limit = tablesToSync.length;
+ database.beginTransaction();
+ try {
+ for (int tableId = 0; tableId < limit; tableId++) {
+ switch (tablesToSync[tableId]) {
+ case ObservedTableTracker.ADD:
+ startTrackingTable(database, tableId);
+ break;
+ case ObservedTableTracker.REMOVE:
+ stopTrackingTable(database, tableId);
+ break;
+ }
+ }
+ database.setTransactionSuccessful();
+ } finally {
+ database.endTransaction();
+ }
+ mObservedTableTracker.onSyncCompleted();
+ } finally {
+ closeLock.unlock();
+ }
+ }
+ } catch (IllegalStateException | SQLiteException exception) {
+ // may happen if db is closed. just log.
+ Log.e(Room.LOG_TAG, "Cannot run invalidation tracker. Is the db closed?",
+ exception);
+ }
+ }
+
+ /**
+ * Called by RoomDatabase before each beginTransaction call.
+ *
+ * It is important that pending trigger changes are applied to the database before any query
+ * runs. Otherwise, we may miss some changes.
+ *
+ * This api should eventually be public.
+ */
+ void syncTriggers() {
+ if (!mDatabase.isOpen()) {
+ return;
+ }
+ syncTriggers(mDatabase.getOpenHelper().getWritableDatabase());
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ *
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @deprecated Use {@link #createLiveData(String[], boolean, Callable)}
+ *
+ * @param computeFunction The function that calculates the value
+ * @param tableNames The list of tables to observe
+ * @param The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @Deprecated
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public LiveData createLiveData(String[] tableNames, Callable computeFunction) {
+ return createLiveData(tableNames, false, computeFunction);
+ }
+
+ /**
+ * Creates a LiveData that computes the given function once and for every other invalidation
+ * of the database.
+ *
+ * Holds a strong reference to the created LiveData as long as it is active.
+ *
+ * @param tableNames The list of tables to observe
+ * @param inTransaction True if the computeFunction will be done in a transaction, false
+ * otherwise.
+ * @param computeFunction The function that calculates the value
+ * @param The return type
+ * @return A new LiveData that computes the given function when the given list of tables
+ * invalidates.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public LiveData createLiveData(String[] tableNames, boolean inTransaction,
+ Callable computeFunction) {
+ return mInvalidationLiveDataContainer.create(
+ validateAndResolveTableNames(tableNames), inTransaction, computeFunction);
+ }
+
+ /**
+ * Wraps an observer and keeps the table information.
+ *
+ * Internally table ids are used which may change from database to database so the table
+ * related information is kept here rather than in the Observer.
+ */
+ @SuppressWarnings("WeakerAccess")
+ static class ObserverWrapper {
+ final int[] mTableIds;
+ private final String[] mTableNames;
+ final Observer mObserver;
+ private final Set mSingleTableSet;
+
+ ObserverWrapper(Observer observer, int[] tableIds, String[] tableNames) {
+ mObserver = observer;
+ mTableIds = tableIds;
+ mTableNames = tableNames;
+ if (tableIds.length == 1) {
+ HashSet set = new HashSet<>();
+ set.add(mTableNames[0]);
+ mSingleTableSet = Collections.unmodifiableSet(set);
+ } else {
+ mSingleTableSet = null;
+ }
+ }
+
+ /**
+ * Notifies the underlying {@link #mObserver} if any of the observed tables are invalidated
+ * based on the given invalid status set.
+ *
+ * @param invalidatedTablesIds The table ids of the tables that are invalidated.
+ */
+ void notifyByTableInvalidStatus(Set invalidatedTablesIds) {
+ Set invalidatedTables = null;
+ final int size = mTableIds.length;
+ for (int index = 0; index < size; index++) {
+ final int tableId = mTableIds[index];
+ if (invalidatedTablesIds.contains(tableId)) {
+ if (size == 1) {
+ // Optimization for a single-table observer
+ invalidatedTables = mSingleTableSet;
+ } else {
+ if (invalidatedTables == null) {
+ invalidatedTables = new HashSet<>(size);
+ }
+ invalidatedTables.add(mTableNames[index]);
+ }
+ }
+ }
+ if (invalidatedTables != null) {
+ mObserver.onInvalidated(invalidatedTables);
+ }
+ }
+
+ /**
+ * Notifies the underlying {@link #mObserver} if it observes any of the specified
+ * {@code tables}.
+ *
+ * @param tables The invalidated table names.
+ */
+ void notifyByTableNames(String[] tables) {
+ Set invalidatedTables = null;
+ if (mTableNames.length == 1) {
+ for (String table : tables) {
+ if (table.equalsIgnoreCase(mTableNames[0])) {
+ // Optimization for a single-table observer
+ invalidatedTables = mSingleTableSet;
+ break;
+ }
+ }
+ } else {
+ HashSet set = new HashSet<>();
+ for (String table : tables) {
+ for (String ourTable : mTableNames) {
+ if (ourTable.equalsIgnoreCase(table)) {
+ set.add(ourTable);
+ break;
+ }
+ }
+ }
+ if (set.size() > 0) {
+ invalidatedTables = set;
+ }
+ }
+ if (invalidatedTables != null) {
+ mObserver.onInvalidated(invalidatedTables);
+ }
+ }
+ }
+
+ /**
+ * An observer that can listen for changes in the database.
+ */
+ public abstract static class Observer {
+ final String[] mTables;
+
+ /**
+ * Observes the given list of tables and views.
+ *
+ * @param firstTable The name of the table or view.
+ * @param rest More names of tables or views.
+ */
+ @SuppressWarnings("unused")
+ protected Observer(@NonNull String firstTable, String... rest) {
+ mTables = Arrays.copyOf(rest, rest.length + 1);
+ mTables[rest.length] = firstTable;
+ }
+
+ /**
+ * Observes the given list of tables and views.
+ *
+ * @param tables The list of tables or views to observe for changes.
+ */
+ public Observer(@NonNull String[] tables) {
+ // copy tables in case user modifies them afterwards
+ mTables = Arrays.copyOf(tables, tables.length);
+ }
+
+ /**
+ * Called when one of the observed tables is invalidated in the database.
+ *
+ * @param tables A set of invalidated tables. This is useful when the observer targets
+ * multiple tables and you want to know which table is invalidated. This will
+ * be names of underlying tables when you are observing views.
+ */
+ public abstract void onInvalidated(@NonNull Set tables);
+
+ boolean isRemote() {
+ return false;
+ }
+ }
+
+ /**
+ * Keeps a list of tables we should observe. Invalidation tracker lazily syncs this list w/
+ * triggers in the database.
+ *
+ * This class is thread safe
+ */
+ static class ObservedTableTracker {
+ static final int NO_OP = 0; // don't change trigger state for this table
+ static final int ADD = 1; // add triggers for this table
+ static final int REMOVE = 2; // remove triggers for this table
+
+ // number of observers per table
+ final long[] mTableObservers;
+ // trigger state for each table at last sync
+ // this field is updated when syncAndGet is called.
+ final boolean[] mTriggerStates;
+ // when sync is called, this field is returned. It includes actions as ADD, REMOVE, NO_OP
+ final int[] mTriggerStateChanges;
+
+ boolean mNeedsSync;
+
+ /**
+ * After we return non-null value from getTablesToSync, we expect a onSyncCompleted before
+ * returning any non-null value from getTablesToSync.
+ * This allows us to workaround any multi-threaded state syncing issues.
+ */
+ boolean mPendingSync;
+
+ ObservedTableTracker(int tableCount) {
+ mTableObservers = new long[tableCount];
+ mTriggerStates = new boolean[tableCount];
+ mTriggerStateChanges = new int[tableCount];
+ Arrays.fill(mTableObservers, 0);
+ Arrays.fill(mTriggerStates, false);
+ }
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ boolean onAdded(int... tableIds) {
+ boolean needTriggerSync = false;
+ synchronized (this) {
+ for (int tableId : tableIds) {
+ final long prevObserverCount = mTableObservers[tableId];
+ mTableObservers[tableId] = prevObserverCount + 1;
+ if (prevObserverCount == 0) {
+ mNeedsSync = true;
+ needTriggerSync = true;
+ }
+ }
+ }
+ return needTriggerSync;
+ }
+
+ /**
+ * @return true if # of triggers is affected.
+ */
+ boolean onRemoved(int... tableIds) {
+ boolean needTriggerSync = false;
+ synchronized (this) {
+ for (int tableId : tableIds) {
+ final long prevObserverCount = mTableObservers[tableId];
+ mTableObservers[tableId] = prevObserverCount - 1;
+ if (prevObserverCount == 1) {
+ mNeedsSync = true;
+ needTriggerSync = true;
+ }
+ }
+ }
+ return needTriggerSync;
+ }
+
+ /**
+ * If this returns non-null, you must call onSyncCompleted.
+ *
+ * @return int[] An int array where the index for each tableId has the action for that
+ * table.
+ */
+ @Nullable
+ int[] getTablesToSync() {
+ synchronized (this) {
+ if (!mNeedsSync || mPendingSync) {
+ return null;
+ }
+ final int tableCount = mTableObservers.length;
+ for (int i = 0; i < tableCount; i++) {
+ final boolean newState = mTableObservers[i] > 0;
+ if (newState != mTriggerStates[i]) {
+ mTriggerStateChanges[i] = newState ? ADD : REMOVE;
+ } else {
+ mTriggerStateChanges[i] = NO_OP;
+ }
+ mTriggerStates[i] = newState;
+ }
+ mPendingSync = true;
+ mNeedsSync = false;
+ return mTriggerStateChanges;
+ }
+ }
+
+ /**
+ * if getTablesToSync returned non-null, the called should call onSyncCompleted once it
+ * is done.
+ */
+ void onSyncCompleted() {
+ synchronized (this) {
+ mPendingSync = false;
+ }
+ }
+ }
+
+ /**
+ * An Observer wrapper that keeps a weak reference to the given object.
+ *
+ * This class will automatically unsubscribe when the wrapped observer goes out of memory.
+ */
+ static class WeakObserver extends Observer {
+ final InvalidationTracker mTracker;
+ final WeakReference mDelegateRef;
+
+ WeakObserver(InvalidationTracker tracker, Observer delegate) {
+ super(delegate.mTables);
+ mTracker = tracker;
+ mDelegateRef = new WeakReference<>(delegate);
+ }
+
+ @Override
+ public void onInvalidated(@NonNull Set tables) {
+ final Observer observer = mDelegateRef.get();
+ if (observer == null) {
+ mTracker.removeObserver(this);
+ } else {
+ observer.onInvalidated(tables);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java
new file mode 100644
index 0000000000..3aeddcc66a
--- /dev/null
+++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationClient.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2018 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Handles all the communication from {@link RoomDatabase} and {@link InvalidationTracker} to
+ * {@link MultiInstanceInvalidationService}.
+ */
+class MultiInstanceInvalidationClient {
+
+ /**
+ * The application context.
+ */
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final Context mAppContext;
+
+ /**
+ * The name of the database file.
+ */
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final String mName;
+
+ /**
+ * The client ID assigned by {@link MultiInstanceInvalidationService}.
+ */
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ int mClientId;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final InvalidationTracker mInvalidationTracker;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final InvalidationTracker.Observer mObserver;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ IMultiInstanceInvalidationService mService;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final Executor mExecutor;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final IMultiInstanceInvalidationCallback mCallback =
+ new IMultiInstanceInvalidationCallback.Stub() {
+ @Override
+ public void onInvalidation(final String[] tables) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mInvalidationTracker.notifyObserversByTableNames(tables);
+ }
+ });
+ }
+ };
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final AtomicBoolean mStopped = new AtomicBoolean(false);
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final ServiceConnection mServiceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mService = IMultiInstanceInvalidationService.Stub.asInterface(service);
+ mExecutor.execute(mSetUpRunnable);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mExecutor.execute(mRemoveObserverRunnable);
+ mService = null;
+ }
+
+ };
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final Runnable mSetUpRunnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ final IMultiInstanceInvalidationService service = mService;
+ if (service != null) {
+ mClientId = service.registerCallback(mCallback, mName);
+ mInvalidationTracker.addObserver(mObserver);
+ }
+ } catch (RemoteException e) {
+ Log.w(Room.LOG_TAG, "Cannot register multi-instance invalidation callback", e);
+ }
+ }
+ };
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final Runnable mRemoveObserverRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mInvalidationTracker.removeObserver(mObserver);
+ }
+ };
+
+ private final Runnable mTearDownRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mInvalidationTracker.removeObserver(mObserver);
+ try {
+ final IMultiInstanceInvalidationService service = mService;
+ if (service != null) {
+ service.unregisterCallback(mCallback, mClientId);
+ }
+ } catch (RemoteException e) {
+ Log.w(Room.LOG_TAG, "Cannot unregister multi-instance invalidation callback", e);
+ }
+ mAppContext.unbindService(mServiceConnection);
+ }
+ };
+
+ /**
+ * @param context The Context to be used for binding
+ * {@link IMultiInstanceInvalidationService}.
+ * @param name The name of the database file.
+ * @param invalidationTracker The {@link InvalidationTracker}
+ * @param executor The background executor.
+ */
+ MultiInstanceInvalidationClient(Context context, String name,
+ InvalidationTracker invalidationTracker, Executor executor) {
+ mAppContext = context.getApplicationContext();
+ mName = name;
+ mInvalidationTracker = invalidationTracker;
+ mExecutor = executor;
+ mObserver = new InvalidationTracker.Observer(invalidationTracker.mTableNames) {
+ @Override
+ public void onInvalidated(@NonNull Set tables) {
+ if (mStopped.get()) {
+ return;
+ }
+ try {
+ final IMultiInstanceInvalidationService service = mService;
+ if (service != null) {
+ service.broadcastInvalidation(mClientId, tables.toArray(new String[0]));
+ }
+ } catch (RemoteException e) {
+ Log.w(Room.LOG_TAG, "Cannot broadcast invalidation", e);
+ }
+ }
+
+ @Override
+ boolean isRemote() {
+ return true;
+ }
+ };
+ Intent intent = new Intent(mAppContext, MultiInstanceInvalidationService.class);
+ mAppContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ void stop() {
+ if (mStopped.compareAndSet(false, true)) {
+ mExecutor.execute(mTearDownRunnable);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java
new file mode 100644
index 0000000000..2e98f120ff
--- /dev/null
+++ b/app/src/main/java/androidx/room/MultiInstanceInvalidationService.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2018 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;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.HashMap;
+
+/**
+ * A {@link Service} for remote invalidation among multiple {@link InvalidationTracker} instances.
+ * This service runs in the main app process. All the instances of {@link InvalidationTracker}
+ * (potentially in other processes) has to connect to this service.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class MultiInstanceInvalidationService extends Service {
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ int mMaxClientId = 0;
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final HashMap mClientNames = new HashMap<>();
+
+ // synthetic access
+ @SuppressWarnings("WeakerAccess")
+ final RemoteCallbackList mCallbackList =
+ new RemoteCallbackList() {
+ @Override
+ public void onCallbackDied(IMultiInstanceInvalidationCallback callback,
+ Object cookie) {
+ mClientNames.remove((int) cookie);
+ }
+ };
+
+ private final IMultiInstanceInvalidationService.Stub mBinder =
+ new IMultiInstanceInvalidationService.Stub() {
+
+ // Assigns a client ID to the client.
+ @Override
+ public int registerCallback(IMultiInstanceInvalidationCallback callback,
+ String name) {
+ if (name == null) {
+ return 0;
+ }
+ synchronized (mCallbackList) {
+ int clientId = ++mMaxClientId;
+ // Use the client ID as the RemoteCallbackList cookie.
+ if (mCallbackList.register(callback, clientId)) {
+ mClientNames.put(clientId, name);
+ return clientId;
+ } else {
+ --mMaxClientId;
+ return 0;
+ }
+ }
+ }
+
+ // Explicitly removes the client.
+ // The client can die without calling this. In that case, mCallbackList
+ // .onCallbackDied() can take care of removal.
+ @Override
+ public void unregisterCallback(IMultiInstanceInvalidationCallback callback,
+ int clientId) {
+ synchronized (mCallbackList) {
+ mCallbackList.unregister(callback);
+ mClientNames.remove(clientId);
+ }
+ }
+
+ // Broadcasts table invalidation to other instances of the same database file.
+ // The broadcast is not sent to the caller itself.
+ @Override
+ public void broadcastInvalidation(int clientId, String[] tables) {
+ synchronized (mCallbackList) {
+ String name = mClientNames.get(clientId);
+ if (name == null) {
+ Log.w(Room.LOG_TAG, "Remote invalidation client ID not registered");
+ return;
+ }
+ int count = mCallbackList.beginBroadcast();
+ try {
+ for (int i = 0; i < count; i++) {
+ int targetClientId = (int) mCallbackList.getBroadcastCookie(i);
+ String targetName = mClientNames.get(targetClientId);
+ if (clientId == targetClientId // This is the caller itself.
+ || !name.equals(targetName)) { // Not the same file.
+ continue;
+ }
+ try {
+ IMultiInstanceInvalidationCallback callback =
+ mCallbackList.getBroadcastItem(i);
+ callback.onInvalidation(tables);
+ } catch (RemoteException e) {
+ Log.w(Room.LOG_TAG, "Error invoking a remote callback", e);
+ }
+ }
+ } finally {
+ mCallbackList.finishBroadcast();
+ }
+ }
+ }
+ };
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+}
diff --git a/app/src/main/java/androidx/room/Room.java b/app/src/main/java/androidx/room/Room.java
new file mode 100644
index 0000000000..2e4dedc7e4
--- /dev/null
+++ b/app/src/main/java/androidx/room/Room.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility class for Room.
+ */
+@SuppressWarnings("unused")
+public class Room {
+ static final String LOG_TAG = "ROOM";
+ /**
+ * The master table where room keeps its metadata information.
+ */
+ public static final String MASTER_TABLE_NAME = RoomMasterTable.TABLE_NAME;
+ private static final String CURSOR_CONV_SUFFIX = "_CursorConverter";
+
+ /**
+ * Creates a RoomDatabase.Builder for a persistent database. Once a database is built, you
+ * should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with {@link Database} and extends
+ * {@link RoomDatabase}.
+ * @param name The name of the database file.
+ * @param The type of the database class.
+ * @return A {@code RoomDatabaseBuilder} which you can use to create the database.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public static RoomDatabase.Builder databaseBuilder(
+ @NonNull Context context, @NonNull Class klass, @NonNull String name) {
+ //noinspection ConstantConditions
+ if (name == null || name.trim().length() == 0) {
+ throw new IllegalArgumentException("Cannot build a database with null or empty name."
+ + " If you are trying to create an in memory database, use Room"
+ + ".inMemoryDatabaseBuilder");
+ }
+ return new RoomDatabase.Builder<>(context, klass, name);
+ }
+
+ /**
+ * Creates a RoomDatabase.Builder for an in memory database. Information stored in an in memory
+ * database disappears when the process is killed.
+ * Once a database is built, you should keep a reference to it and re-use it.
+ *
+ * @param context The context for the database. This is usually the Application context.
+ * @param klass The abstract class which is annotated with {@link Database} and extends
+ * {@link RoomDatabase}.
+ * @param The type of the database class.
+ * @return A {@code RoomDatabaseBuilder} which you can use to create the database.
+ */
+ @NonNull
+ public static RoomDatabase.Builder inMemoryDatabaseBuilder(
+ @NonNull Context context, @NonNull Class klass) {
+ return new RoomDatabase.Builder<>(context, klass, null);
+ }
+
+ @SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
+ @NonNull
+ static T getGeneratedImplementation(Class klass, String suffix) {
+ final String fullPackage = klass.getPackage().getName();
+ String name = klass.getCanonicalName();
+ final String postPackageName = fullPackage.isEmpty()
+ ? name
+ : (name.substring(fullPackage.length() + 1));
+ final String implName = postPackageName.replace('.', '_') + suffix;
+ //noinspection TryWithIdenticalCatches
+ try {
+
+ @SuppressWarnings("unchecked")
+ final Class aClass = (Class) Class.forName(
+ fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
+ return aClass.newInstance();
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("cannot find implementation for "
+ + klass.getCanonicalName() + ". " + implName + " does not exist");
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot access the constructor"
+ + klass.getCanonicalName());
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Failed to create an instance of "
+ + klass.getCanonicalName());
+ }
+ }
+
+ /** @deprecated This type should not be instantiated as it contains only static methods. */
+ @Deprecated
+ @SuppressWarnings("PrivateConstructorForUtilityClass")
+ public Room() {
+ }
+}
diff --git a/app/src/main/java/androidx/room/RoomDatabase.java b/app/src/main/java/androidx/room/RoomDatabase.java
new file mode 100644
index 0000000000..2e88a2891d
--- /dev/null
+++ b/app/src/main/java/androidx/room/RoomDatabase.java
@@ -0,0 +1,1065 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build;
+import android.os.Looper;
+import android.util.Log;
+
+import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.room.migration.Migration;
+import androidx.room.util.SneakyThrow;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+import androidx.sqlite.db.SupportSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteStatement;
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * Base class for all Room databases. All classes that are annotated with {@link Database} must
+ * extend this class.
+ *
+ * RoomDatabase provides direct access to the underlying database implementation but you should
+ * prefer using {@link Dao} classes.
+ *
+ * @see Database
+ */
+public abstract class RoomDatabase {
+ private static final String DB_IMPL_SUFFIX = "_Impl";
+ /**
+ * Unfortunately, we cannot read this value so we are only setting it to the SQLite default.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static final int MAX_BIND_PARAMETER_CNT = 999;
+ /**
+ * Set by the generated open helper.
+ *
+ * @deprecated Will be hidden in the next release.
+ */
+ @Deprecated
+ protected volatile SupportSQLiteDatabase mDatabase;
+ private Executor mQueryExecutor;
+ private Executor mTransactionExecutor;
+ private SupportSQLiteOpenHelper mOpenHelper;
+ private final InvalidationTracker mInvalidationTracker;
+ private boolean mAllowMainThreadQueries;
+ boolean mWriteAheadLoggingEnabled;
+
+ /**
+ * @deprecated Will be hidden in the next release.
+ */
+ @Nullable
+ @Deprecated
+ protected List mCallbacks;
+
+ private final ReentrantReadWriteLock mCloseLock = new ReentrantReadWriteLock();
+
+ /**
+ * {@link InvalidationTracker} uses this lock to prevent the database from closing while it is
+ * querying database updates.
+ *
+ * The returned lock is reentrant and will allow multiple threads to acquire the lock
+ * simultaneously until {@link #close()} is invoked in which the lock becomes exclusive as
+ * a way to let the InvalidationTracker finish its work before closing the database.
+ *
+ * @return The lock for {@link #close()}.
+ */
+ Lock getCloseLock() {
+ return mCloseLock.readLock();
+ }
+
+ /**
+ * This id is only set on threads that are used to dispatch coroutines within a suspending
+ * database transaction.
+ */
+ private final ThreadLocal mSuspendingTransactionId = new ThreadLocal<>();
+
+ /**
+ * Gets the suspending transaction id of the current thread.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ ThreadLocal getSuspendingTransactionId() {
+ return mSuspendingTransactionId;
+ }
+
+
+ private final Map mBackingFieldMap = new ConcurrentHashMap<>();
+
+ /**
+ * Gets the map for storing extension properties of Kotlin type.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ Map getBackingFieldMap() {
+ return mBackingFieldMap;
+ }
+
+ /**
+ * Creates a RoomDatabase.
+ *
+ * You cannot create an instance of a database, instead, you should acquire it via
+ * {@link Room#databaseBuilder(Context, Class, String)} or
+ * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
+ */
+ public RoomDatabase() {
+ mInvalidationTracker = createInvalidationTracker();
+ }
+
+ /**
+ * Called by {@link Room} when it is initialized.
+ *
+ * @param configuration The database configuration.
+ */
+ @CallSuper
+ public void init(@NonNull DatabaseConfiguration configuration) {
+ mOpenHelper = createOpenHelper(configuration);
+ if (mOpenHelper instanceof SQLiteCopyOpenHelper) {
+ SQLiteCopyOpenHelper copyOpenHelper = (SQLiteCopyOpenHelper) mOpenHelper;
+ copyOpenHelper.setDatabaseConfiguration(configuration);
+ }
+ boolean wal = false;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING;
+ mOpenHelper.setWriteAheadLoggingEnabled(wal);
+ }
+ mCallbacks = configuration.callbacks;
+ mQueryExecutor = configuration.queryExecutor;
+ mTransactionExecutor = new TransactionExecutor(configuration.transactionExecutor);
+ mAllowMainThreadQueries = configuration.allowMainThreadQueries;
+ mWriteAheadLoggingEnabled = wal;
+ if (configuration.multiInstanceInvalidation) {
+ mInvalidationTracker.startMultiInstanceInvalidation(configuration.context,
+ configuration.name);
+ }
+ }
+
+ /**
+ * Returns the SQLite open helper used by this database.
+ *
+ * @return The SQLite open helper used by this database.
+ */
+ @NonNull
+ public SupportSQLiteOpenHelper getOpenHelper() {
+ return mOpenHelper;
+ }
+
+ /**
+ * Creates the open helper to access the database. Generated class already implements this
+ * method.
+ * Note that this method is called when the RoomDatabase is initialized.
+ *
+ * @param config The configuration of the Room database.
+ * @return A new SupportSQLiteOpenHelper to be used while connecting to the database.
+ */
+ @NonNull
+ protected abstract SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration config);
+
+ /**
+ * Called when the RoomDatabase is created.
+ *
+ * This is already implemented by the generated code.
+ *
+ * @return Creates a new InvalidationTracker.
+ */
+ @NonNull
+ protected abstract InvalidationTracker createInvalidationTracker();
+
+ /**
+ * Deletes all rows from all the tables that are registered to this database as
+ * {@link Database#entities()}.
+ *
+ * This does NOT reset the auto-increment value generated by {@link PrimaryKey#autoGenerate()}.
+ *
+ * After deleting the rows, Room will set a WAL checkpoint and run VACUUM. This means that the
+ * data is completely erased. The space will be reclaimed by the system if the amount surpasses
+ * the threshold of database file size.
+ *
+ * @see Database File Format
+ */
+ @WorkerThread
+ public abstract void clearAllTables();
+
+ /**
+ * Returns true if database connection is open and initialized.
+ *
+ * @return true if the database connection is open, false otherwise.
+ */
+ public boolean isOpen() {
+ final SupportSQLiteDatabase db = mDatabase;
+ return db != null && db.isOpen();
+ }
+
+ /**
+ * Closes the database if it is already open.
+ */
+ public void close() {
+ if (isOpen()) {
+ final Lock closeLock = mCloseLock.writeLock();
+ try {
+ closeLock.lock();
+ mInvalidationTracker.stopMultiInstanceInvalidation();
+ mOpenHelper.close();
+ } finally {
+ closeLock.unlock();
+ }
+ }
+ }
+
+ /**
+ * Asserts that we are not on the main thread.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ // used in generated code
+ public void assertNotMainThread() {
+ if (mAllowMainThreadQueries) {
+ return;
+ }
+ if (isMainThread()) {
+ throw new IllegalStateException("Cannot access database on the main thread since"
+ + " it may potentially lock the UI for a long period of time.");
+ }
+ }
+
+ /**
+ * Asserts that we are not on a suspending transaction.
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ // used in generated code
+ public void assertNotSuspendingTransaction() {
+ if (!inTransaction() && mSuspendingTransactionId.get() != null) {
+ throw new IllegalStateException("Cannot access database on a different coroutine"
+ + " context inherited from a suspending transaction.");
+ }
+ }
+
+ // Below, there are wrapper methods for SupportSQLiteDatabase. This helps us track which
+ // methods we are using and also helps unit tests to mock this class without mocking
+ // all SQLite database methods.
+
+ /**
+ * Convenience method to query the database with arguments.
+ *
+ * @param query The sql query
+ * @param args The bind arguments for the placeholders in the query
+ * @return A Cursor obtained by running the given query in the Room database.
+ */
+ public Cursor query(String query, @Nullable Object[] args) {
+ return mOpenHelper.getWritableDatabase().query(new SimpleSQLiteQuery(query, args));
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
+ *
+ * @param query The Query which includes the SQL and a bind callback for bind arguments.
+ * @return Result of the query.
+ */
+ public Cursor query(SupportSQLiteQuery query) {
+ assertNotMainThread();
+ assertNotSuspendingTransaction();
+ return mOpenHelper.getWritableDatabase().query(query);
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#compileStatement(String)}.
+ *
+ * @param sql The query to compile.
+ * @return The compiled query.
+ */
+ public SupportSQLiteStatement compileStatement(@NonNull String sql) {
+ assertNotMainThread();
+ assertNotSuspendingTransaction();
+ return mOpenHelper.getWritableDatabase().compileStatement(sql);
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#beginTransaction()}.
+ *
+ * @deprecated Use {@link #runInTransaction(Runnable)}
+ */
+ @Deprecated
+ public void beginTransaction() {
+ assertNotMainThread();
+ SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
+ mInvalidationTracker.syncTriggers(database);
+ database.beginTransaction();
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}.
+ *
+ * @deprecated Use {@link #runInTransaction(Runnable)}
+ */
+ @Deprecated
+ public void endTransaction() {
+ mOpenHelper.getWritableDatabase().endTransaction();
+ if (!inTransaction()) {
+ // enqueue refresh only if we are NOT in a transaction. Otherwise, wait for the last
+ // endTransaction call to do it.
+ mInvalidationTracker.refreshVersionsAsync();
+ }
+ }
+
+ /**
+ * @return The Executor in use by this database for async queries.
+ */
+ @NonNull
+ public Executor getQueryExecutor() {
+ return mQueryExecutor;
+ }
+
+ /**
+ * @return The Executor in use by this database for async transactions.
+ */
+ @NonNull
+ public Executor getTransactionExecutor() {
+ return mTransactionExecutor;
+ }
+
+ /**
+ * Wrapper for {@link SupportSQLiteDatabase#setTransactionSuccessful()}.
+ *
+ * @deprecated Use {@link #runInTransaction(Runnable)}
+ */
+ @Deprecated
+ public void setTransactionSuccessful() {
+ mOpenHelper.getWritableDatabase().setTransactionSuccessful();
+ }
+
+ /**
+ * Executes the specified {@link Runnable} in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the {@link Runnable}.
+ *
+ * Room will only perform at most one transaction at a time.
+ *
+ * @param body The piece of code to execute.
+ */
+ @SuppressWarnings("deprecation")
+ public void runInTransaction(@NonNull Runnable body) {
+ beginTransaction();
+ try {
+ body.run();
+ setTransactionSuccessful();
+ } finally {
+ endTransaction();
+ }
+ }
+
+ /**
+ * Executes the specified {@link Callable} in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the {@link Callable}.
+ *
+ * Room will only perform at most one transaction at a time.
+ *
+ * @param body The piece of code to execute.
+ * @param The type of the return value.
+ * @return The value returned from the {@link Callable}.
+ */
+ @SuppressWarnings("deprecation")
+ public V runInTransaction(@NonNull Callable body) {
+ beginTransaction();
+ try {
+ V result = body.call();
+ setTransactionSuccessful();
+ return result;
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ SneakyThrow.reThrow(e);
+ return null; // Unreachable code, but compiler doesn't know it.
+ } finally {
+ endTransaction();
+ }
+ }
+
+ /**
+ * Called by the generated code when database is open.
+ *
+ * You should never call this method manually.
+ *
+ * @param db The database instance.
+ */
+ protected void internalInitInvalidationTracker(@NonNull SupportSQLiteDatabase db) {
+ mInvalidationTracker.internalInit(db);
+ }
+
+ /**
+ * Returns the invalidation tracker for this database.
+ *
+ * You can use the invalidation tracker to get notified when certain tables in the database
+ * are modified.
+ *
+ * @return The invalidation tracker for the database.
+ */
+ @NonNull
+ public InvalidationTracker getInvalidationTracker() {
+ return mInvalidationTracker;
+ }
+
+ /**
+ * Returns true if current thread is in a transaction.
+ *
+ * @return True if there is an active transaction in current thread, false otherwise.
+ * @see SupportSQLiteDatabase#inTransaction()
+ */
+ @SuppressWarnings("WeakerAccess")
+ public boolean inTransaction() {
+ return mOpenHelper.getWritableDatabase().inTransaction();
+ }
+
+ /**
+ * Journal modes for SQLite database.
+ *
+ * @see RoomDatabase.Builder#setJournalMode(JournalMode)
+ */
+ public enum JournalMode {
+
+ /**
+ * Let Room choose the journal mode. This is the default value when no explicit value is
+ * specified.
+ *
+ * The actual value will be {@link #TRUNCATE} when the device runs API Level lower than 16
+ * or it is a low-RAM device. Otherwise, {@link #WRITE_AHEAD_LOGGING} will be used.
+ */
+ AUTOMATIC,
+
+ /**
+ * Truncate journal mode.
+ */
+ TRUNCATE,
+
+ /**
+ * Write-Ahead Logging mode.
+ */
+ @RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
+ WRITE_AHEAD_LOGGING;
+
+ /**
+ * Resolves {@link #AUTOMATIC} to either {@link #TRUNCATE} or
+ * {@link #WRITE_AHEAD_LOGGING}.
+ */
+ @SuppressLint("NewApi")
+ JournalMode resolve(Context context) {
+ if (this != AUTOMATIC) {
+ return this;
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ ActivityManager manager = (ActivityManager)
+ context.getSystemService(Context.ACTIVITY_SERVICE);
+ if (manager != null && !isLowRamDevice(manager)) {
+ return WRITE_AHEAD_LOGGING;
+ }
+ }
+ return TRUNCATE;
+ }
+
+ private static boolean isLowRamDevice(@NonNull ActivityManager activityManager) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return activityManager.isLowRamDevice();
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Builder for RoomDatabase.
+ *
+ * @param The type of the abstract database class.
+ */
+ public static class Builder {
+ private final Class mDatabaseClass;
+ private final String mName;
+ private final Context mContext;
+ private ArrayList mCallbacks;
+
+ /** The Executor used to run database queries. This should be background-threaded. */
+ private Executor mQueryExecutor;
+ /** The Executor used to run database transactions. This should be background-threaded. */
+ private Executor mTransactionExecutor;
+ private SupportSQLiteOpenHelper.Factory mFactory;
+ private boolean mAllowMainThreadQueries;
+ private JournalMode mJournalMode;
+ private boolean mMultiInstanceInvalidation;
+ private boolean mRequireMigration;
+ private boolean mAllowDestructiveMigrationOnDowngrade;
+ /**
+ * Migrations, mapped by from-to pairs.
+ */
+ private final MigrationContainer mMigrationContainer;
+ private Set mMigrationsNotRequiredFrom;
+ /**
+ * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
+ * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
+ * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(int...)}.
+ */
+ private Set mMigrationStartAndEndVersions;
+
+ private String mCopyFromAssetPath;
+ private File mCopyFromFile;
+
+ Builder(@NonNull Context context, @NonNull Class klass, @Nullable String name) {
+ mContext = context;
+ mDatabaseClass = klass;
+ mName = name;
+ mJournalMode = JournalMode.AUTOMATIC;
+ mRequireMigration = true;
+ mMigrationContainer = new MigrationContainer();
+ }
+
+ /**
+ * Configures Room to create and open the database using a pre-packaged database located in
+ * the application 'assets/' folder.
+ *
+ * Room does not open the pre-packaged database, instead it copies it into the internal
+ * app database folder and then opens it. The pre-packaged database file must be located in
+ * the "assets/" folder of your application. For example, the path for a file located in
+ * "assets/databases/products.db" would be "databases/products.db".
+ *
+ * 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}.
+ *
+ * @param databaseFilePath The file path within the 'assets/' directory of where the
+ * database file is located.
+ *
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder createFromAsset(@NonNull String databaseFilePath) {
+ mCopyFromAssetPath = databaseFilePath;
+ return this;
+ }
+
+ /**
+ * Configures Room to create and open the database using a pre-packaged database file.
+ *
+ * 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}.
+ *
+ * @param databaseFile The database file.
+ *
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder createFromFile(@NonNull File databaseFile) {
+ mCopyFromFile = databaseFile;
+ return this;
+ }
+
+ /**
+ * Sets the database factory. If not set, it defaults to
+ * {@link FrameworkSQLiteOpenHelperFactory}.
+ *
+ * @param factory The factory to use to access the database.
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory) {
+ mFactory = factory;
+ return this;
+ }
+
+ /**
+ * Adds a migration to the builder.
+ *
+ * Each Migration has a start and end versions and Room runs these migrations to bring the
+ * database to the latest version.
+ *
+ * If a migration item is missing between current version and the latest version, Room
+ * will clear the database and recreate so even if you have no changes between 2 versions,
+ * you should still provide a Migration object to the builder.
+ *
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is >= 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ *
+ * @param migrations The migration object that can modify the database and to the necessary
+ * changes.
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder addMigrations(@NonNull Migration... migrations) {
+ if (mMigrationStartAndEndVersions == null) {
+ mMigrationStartAndEndVersions = new HashSet<>();
+ }
+ for (Migration migration : migrations) {
+ mMigrationStartAndEndVersions.add(migration.startVersion);
+ mMigrationStartAndEndVersions.add(migration.endVersion);
+ }
+
+ mMigrationContainer.addMigrations(migrations);
+ return this;
+ }
+
+ /**
+ * Disables the main thread query check for Room.
+ *
+ * Room ensures that Database is never accessed on the main thread because it may lock the
+ * main thread and trigger an ANR. If you need to access the database from the main thread,
+ * you should always use async alternatives or manually move the call to a background
+ * thread.
+ *
+ * You may want to turn this check off for testing.
+ *
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder allowMainThreadQueries() {
+ mAllowMainThreadQueries = true;
+ return this;
+ }
+
+ /**
+ * Sets the journal mode for this database.
+ *
+ *
+ * This value is ignored if the builder is initialized with
+ * {@link Room#inMemoryDatabaseBuilder(Context, Class)}.
+ *
+ * The journal mode should be consistent across multiple instances of
+ * {@link RoomDatabase} for a single SQLite database file.
+ *
+ * The default value is {@link JournalMode#AUTOMATIC}.
+ *
+ * @param journalMode The journal mode.
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder setJournalMode(@NonNull JournalMode journalMode) {
+ mJournalMode = journalMode;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous
+ * queries and tasks, including {@code LiveData} invalidation, {@code Flowable} scheduling
+ * and {@code ListenableFuture} tasks.
+ *
+ * When both the query executor and transaction executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the query executor is unset but a
+ * transaction executor was set, then the same {@code Executor} will be used for queries.
+ *
+ * For best performance the given {@code Executor} should be bounded (max number of threads
+ * is limited).
+ *
+ * The input {@code Executor} cannot run tasks on the UI thread.
+ **
+ * @return This {@link Builder} instance.
+ *
+ * @see #setTransactionExecutor(Executor)
+ */
+ @NonNull
+ public Builder setQueryExecutor(@NonNull Executor executor) {
+ mQueryExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets the {@link Executor} that will be used to execute all non-blocking asynchronous
+ * transaction queries and tasks, including {@code LiveData} invalidation, {@code Flowable}
+ * scheduling and {@code ListenableFuture} tasks.
+ *
+ * When both the transaction executor and query executor are unset, then a default
+ * {@code Executor} will be used. The default {@code Executor} allocates and shares threads
+ * amongst Architecture Components libraries. If the transaction executor is unset but a
+ * query executor was set, then the same {@code Executor} will be used for transactions.
+ *
+ * If the given {@code Executor} is shared then it should be unbounded to avoid the
+ * possibility of a deadlock. Room will not use more than one thread at a time from this
+ * executor since only one transaction at a time can be executed, other transactions will
+ * be queued on a first come, first serve order.
+ *
+ * The input {@code Executor} cannot run tasks on the UI thread.
+ *
+ * @return This {@link Builder} instance.
+ *
+ * @see #setQueryExecutor(Executor)
+ */
+ @NonNull
+ public Builder setTransactionExecutor(@NonNull Executor executor) {
+ mTransactionExecutor = executor;
+ return this;
+ }
+
+ /**
+ * Sets whether table invalidation in this instance of {@link RoomDatabase} should be
+ * broadcast and synchronized with other instances of the same {@link RoomDatabase},
+ * including those in a separate process. In order to enable multi-instance invalidation,
+ * this has to be turned on both ends.
+ *
+ * This is not enabled by default.
+ *
+ * This does not work for in-memory databases. This does not work between database instances
+ * targeting different database files.
+ *
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder enableMultiInstanceInvalidation() {
+ mMultiInstanceInvalidation = mName != null;
+ return this;
+ }
+
+ /**
+ * Allows Room to destructively recreate database tables if {@link Migration}s that would
+ * migrate old database schemas to the latest schema version are not found.
+ *
+ * When the database version on the device does not match the latest schema version, Room
+ * runs necessary {@link Migration}s on the database.
+ *
+ * If it cannot find the set of {@link Migration}s that will bring the database to the
+ * current version, it will throw an {@link IllegalStateException}.
+ *
+ * You can call this method to change this behavior to re-create the database instead of
+ * crashing.
+ *
+ * If the database was create from an asset or a file then Room will try to use the same
+ * file to re-create the database, otherwise this will delete all of the data in the
+ * database tables managed by Room.
+ *
+ * To let Room fallback to destructive migration only during a schema downgrade then use
+ * {@link #fallbackToDestructiveMigrationOnDowngrade()}.
+ *
+ * @return This {@link Builder} instance.
+ *
+ * @see #fallbackToDestructiveMigrationOnDowngrade()
+ */
+ @NonNull
+ public Builder fallbackToDestructiveMigration() {
+ mRequireMigration = false;
+ mAllowDestructiveMigrationOnDowngrade = true;
+ return this;
+ }
+
+ /**
+ * Allows Room to destructively recreate database tables if {@link Migration}s are not
+ * available when downgrading to old schema versions.
+ *
+ * @return This {@link Builder} instance.
+ *
+ * @see Builder#fallbackToDestructiveMigration()
+ */
+ @NonNull
+ public Builder fallbackToDestructiveMigrationOnDowngrade() {
+ mRequireMigration = true;
+ mAllowDestructiveMigrationOnDowngrade = true;
+ return this;
+ }
+
+ /**
+ * Informs Room that it is allowed to destructively recreate database tables from specific
+ * starting schema versions.
+ *
+ * This functionality is the same as that provided by
+ * {@link #fallbackToDestructiveMigration()}, except that this method allows the
+ * specification of a set of schema versions for which destructive recreation is allowed.
+ *
+ * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
+ * to allow destructive migrations from some schema versions while still taking advantage
+ * of exceptions being thrown due to unintentionally missing migrations.
+ *
+ * Note: No versions passed to this method may also exist as either starting or ending
+ * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
+ * version passed to this method is found as a starting or ending version in a Migration, an
+ * exception will be thrown.
+ *
+ * @param startVersions The set of schema versions from which Room should use a destructive
+ * migration.
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder fallbackToDestructiveMigrationFrom(int... startVersions) {
+ if (mMigrationsNotRequiredFrom == null) {
+ mMigrationsNotRequiredFrom = new HashSet<>(startVersions.length);
+ }
+ for (int startVersion : startVersions) {
+ mMigrationsNotRequiredFrom.add(startVersion);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a {@link Callback} to this database.
+ *
+ * @param callback The callback.
+ * @return This {@link Builder} instance.
+ */
+ @NonNull
+ public Builder addCallback(@NonNull Callback callback) {
+ if (mCallbacks == null) {
+ mCallbacks = new ArrayList<>();
+ }
+ mCallbacks.add(callback);
+ return this;
+ }
+
+ /**
+ * Creates the databases and initializes it.
+ *
+ * By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
+ * triggers.
+ *
+ * @return A new database instance.
+ */
+ @SuppressLint("RestrictedApi")
+ @NonNull
+ public T build() {
+ //noinspection ConstantConditions
+ if (mContext == null) {
+ throw new IllegalArgumentException("Cannot provide null context for the database.");
+ }
+ //noinspection ConstantConditions
+ if (mDatabaseClass == null) {
+ throw new IllegalArgumentException("Must provide an abstract class that"
+ + " extends RoomDatabase");
+ }
+ if (mQueryExecutor == null && mTransactionExecutor == null) {
+ mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor();
+ } else if (mQueryExecutor != null && mTransactionExecutor == null) {
+ mTransactionExecutor = mQueryExecutor;
+ } else if (mQueryExecutor == null && mTransactionExecutor != null) {
+ mQueryExecutor = mTransactionExecutor;
+ }
+
+ if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
+ for (Integer version : mMigrationStartAndEndVersions) {
+ if (mMigrationsNotRequiredFrom.contains(version)) {
+ throw new IllegalArgumentException(
+ "Inconsistency detected. A Migration was supplied to "
+ + "addMigration(Migration... migrations) that has a start "
+ + "or end version equal to a start version supplied to "
+ + "fallbackToDestructiveMigrationFrom(int... "
+ + "startVersions). Start version: "
+ + version);
+ }
+ }
+ }
+
+ if (mFactory == null) {
+ mFactory = new FrameworkSQLiteOpenHelperFactory();
+ }
+
+ if (mCopyFromAssetPath != null || mCopyFromFile != 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.");
+ }
+ mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
+ mFactory);
+ }
+ DatabaseConfiguration configuration =
+ new DatabaseConfiguration(
+ mContext,
+ mName,
+ mFactory,
+ mMigrationContainer,
+ mCallbacks,
+ mAllowMainThreadQueries,
+ mJournalMode.resolve(mContext),
+ mQueryExecutor,
+ mTransactionExecutor,
+ mMultiInstanceInvalidation,
+ mRequireMigration,
+ mAllowDestructiveMigrationOnDowngrade,
+ mMigrationsNotRequiredFrom,
+ mCopyFromAssetPath,
+ mCopyFromFile);
+ T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
+ db.init(configuration);
+ return db;
+ }
+ }
+
+ /**
+ * A container to hold migrations. It also allows querying its contents to find migrations
+ * between two versions.
+ */
+ public static class MigrationContainer {
+ private HashMap> mMigrations = new HashMap<>();
+
+ /**
+ * Adds the given migrations to the list of available migrations. If 2 migrations have the
+ * same start-end versions, the latter migration overrides the previous one.
+ *
+ * @param migrations List of available migrations.
+ */
+ public void addMigrations(@NonNull Migration... migrations) {
+ for (Migration migration : migrations) {
+ addMigration(migration);
+ }
+ }
+
+ private void addMigration(Migration migration) {
+ final int start = migration.startVersion;
+ final int end = migration.endVersion;
+ TreeMap targetMap = mMigrations.get(start);
+ if (targetMap == null) {
+ targetMap = new TreeMap<>();
+ mMigrations.put(start, targetMap);
+ }
+ Migration existing = targetMap.get(end);
+ if (existing != null) {
+ Log.w(Room.LOG_TAG, "Overriding migration " + existing + " with " + migration);
+ }
+ targetMap.put(end, migration);
+ }
+
+ /**
+ * Finds the list of migrations that should be run to move from {@code start} version to
+ * {@code end} version.
+ *
+ * @param start The current database version
+ * @param end The target database version
+ * @return An ordered list of {@link Migration} objects that should be run to migrate
+ * between the given versions. If a migration path cannot be found, returns {@code null}.
+ */
+ @SuppressWarnings("WeakerAccess")
+ @Nullable
+ public List findMigrationPath(int start, int end) {
+ if (start == end) {
+ return Collections.emptyList();
+ }
+ boolean migrateUp = end > start;
+ List result = new ArrayList<>();
+ return findUpMigrationPath(result, migrateUp, start, end);
+ }
+
+ private List findUpMigrationPath(List result, boolean upgrade,
+ int start, int end) {
+ while (upgrade ? start < end : start > end) {
+ TreeMap targetNodes = mMigrations.get(start);
+ if (targetNodes == null) {
+ return null;
+ }
+ // keys are ordered so we can start searching from one end of them.
+ Set keySet;
+ if (upgrade) {
+ keySet = targetNodes.descendingKeySet();
+ } else {
+ keySet = targetNodes.keySet();
+ }
+ boolean found = false;
+ for (int targetVersion : keySet) {
+ final boolean shouldAddToPath;
+ if (upgrade) {
+ shouldAddToPath = targetVersion <= end && targetVersion > start;
+ } else {
+ shouldAddToPath = targetVersion >= end && targetVersion < start;
+ }
+ if (shouldAddToPath) {
+ result.add(targetNodes.get(targetVersion));
+ start = targetVersion;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return null;
+ }
+ }
+ return result;
+ }
+ }
+
+ /** Returns true if the calling thread is the main thread. */
+ private static boolean isMainThread() {
+ return Looper.getMainLooper().getThread() == Thread.currentThread();
+ }
+
+ /**
+ * Callback for {@link RoomDatabase}.
+ */
+ public abstract static class Callback {
+
+ /**
+ * Called when the database is created for the first time. This is called after all the
+ * tables are created.
+ *
+ * @param db The database.
+ */
+ public void onCreate(@NonNull SupportSQLiteDatabase db) {
+ }
+
+ /**
+ * Called when the database has been opened.
+ *
+ * @param db The database.
+ */
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ }
+
+ /**
+ * Called after the database was destructively migrated
+ *
+ * @param db The database.
+ */
+ public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db){
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/RoomOpenHelper.java b/app/src/main/java/androidx/room/RoomOpenHelper.java
new file mode 100644
index 0000000000..3dbe7ddeac
--- /dev/null
+++ b/app/src/main/java/androidx/room/RoomOpenHelper.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2017 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;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.room.migration.Migration;
+import androidx.sqlite.db.SimpleSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.util.List;
+
+/**
+ * An open helper that holds a reference to the configuration until the database is opened.
+ *
+ * @hide
+ */
+@SuppressWarnings("unused")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
+ @Nullable
+ private DatabaseConfiguration mConfiguration;
+ @NonNull
+ private final Delegate mDelegate;
+ @NonNull
+ private final String mIdentityHash;
+ /**
+ * Room v1 had a bug where the hash was not consistent if fields are reordered.
+ * The new has fixes it but we still need to accept the legacy hash.
+ */
+ @NonNull // b/64290754
+ private final String mLegacyHash;
+
+ public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
+ @NonNull String identityHash, @NonNull String legacyHash) {
+ super(delegate.version);
+ mConfiguration = configuration;
+ mDelegate = delegate;
+ mIdentityHash = identityHash;
+ mLegacyHash = legacyHash;
+ }
+
+ public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
+ @NonNull String legacyHash) {
+ this(configuration, delegate, "", legacyHash);
+ }
+
+ @Override
+ public void onConfigure(SupportSQLiteDatabase db) {
+ super.onConfigure(db);
+ }
+
+ @Override
+ public void onCreate(SupportSQLiteDatabase db) {
+ boolean isEmptyDatabase = hasEmptySchema(db);
+ mDelegate.createAllTables(db);
+ if (!isEmptyDatabase) {
+ // A 0 version pre-populated database goes through the create path because the
+ // framework's SQLiteOpenHelper thinks the database was just created from scratch. If we
+ // find the database not to be empty, then it is a pre-populated, we must validate it to
+ // see if its suitable for usage.
+ ValidationResult result = mDelegate.onValidateSchema(db);
+ if (!result.isValid) {
+ throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ + result.expectedFoundMsg);
+ }
+ }
+ updateIdentity(db);
+ mDelegate.onCreate(db);
+ }
+
+ @Override
+ public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ boolean migrated = false;
+ if (mConfiguration != null) {
+ List migrations = mConfiguration.migrationContainer.findMigrationPath(
+ oldVersion, newVersion);
+ if (migrations != null) {
+ mDelegate.onPreMigrate(db);
+ for (Migration migration : migrations) {
+ migration.migrate(db);
+ }
+ ValidationResult result = mDelegate.onValidateSchema(db);
+ if (!result.isValid) {
+ throw new IllegalStateException("Migration didn't properly handle: "
+ + result.expectedFoundMsg);
+ }
+ mDelegate.onPostMigrate(db);
+ updateIdentity(db);
+ migrated = true;
+ }
+ }
+ if (!migrated) {
+ if (mConfiguration != null
+ && !mConfiguration.isMigrationRequired(oldVersion, newVersion)) {
+ mDelegate.dropAllTables(db);
+ mDelegate.createAllTables(db);
+ } else {
+ throw new IllegalStateException("A migration from " + oldVersion + " to "
+ + newVersion + " was required but not found. Please provide the "
+ + "necessary Migration path via "
+ + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ + "destructive migrations via one of the "
+ + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
+ }
+ }
+ }
+
+ @Override
+ public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
+ onUpgrade(db, oldVersion, newVersion);
+ }
+
+ @Override
+ public void onOpen(SupportSQLiteDatabase db) {
+ super.onOpen(db);
+ checkIdentity(db);
+ mDelegate.onOpen(db);
+ // there might be too many configurations etc, just clear it.
+ mConfiguration = null;
+ }
+
+ private void checkIdentity(SupportSQLiteDatabase db) {
+ if (hasRoomMasterTable(db)) {
+ String identityHash = null;
+ Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ if (cursor.moveToFirst()) {
+ identityHash = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
+ throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ + " you've changed schema but forgot to update the version number. You can"
+ + " simply fix this by increasing the version number.");
+ }
+ } else {
+ // No room_master_table, this might an a pre-populated DB, we must validate to see if
+ // its suitable for usage.
+ ValidationResult result = mDelegate.onValidateSchema(db);
+ if (!result.isValid) {
+ throw new IllegalStateException("Pre-packaged database has an invalid schema: "
+ + result.expectedFoundMsg);
+ }
+ mDelegate.onPostMigrate(db);
+ updateIdentity(db);
+ }
+ }
+
+ private void updateIdentity(SupportSQLiteDatabase db) {
+ createMasterTableIfNotExists(db);
+ db.execSQL(RoomMasterTable.createInsertQuery(mIdentityHash));
+ }
+
+ private void createMasterTableIfNotExists(SupportSQLiteDatabase db) {
+ db.execSQL(RoomMasterTable.CREATE_QUERY);
+ }
+
+ private static boolean hasRoomMasterTable(SupportSQLiteDatabase db) {
+ Cursor cursor = db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name='"
+ + RoomMasterTable.TABLE_NAME + "'");
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ return cursor.moveToFirst() && cursor.getInt(0) != 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private static boolean hasEmptySchema(SupportSQLiteDatabase db) {
+ Cursor cursor = db.query(
+ "SELECT count(*) FROM sqlite_master WHERE name != 'android_metadata'");
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ return cursor.moveToFirst() && cursor.getInt(0) == 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public abstract static class Delegate {
+ public final int version;
+
+ public Delegate(int version) {
+ this.version = version;
+ }
+
+ protected abstract void dropAllTables(SupportSQLiteDatabase database);
+
+ protected abstract void createAllTables(SupportSQLiteDatabase database);
+
+ protected abstract void onOpen(SupportSQLiteDatabase database);
+
+ protected abstract void onCreate(SupportSQLiteDatabase database);
+
+ /**
+ * Called after a migration run to validate database integrity.
+ *
+ * @param db The SQLite database.
+ *
+ * @deprecated Use {@link #onValidateSchema(SupportSQLiteDatabase)}
+ */
+ @Deprecated
+ protected void validateMigration(SupportSQLiteDatabase db) {
+ throw new UnsupportedOperationException("validateMigration is deprecated");
+ }
+
+ /**
+ * Called after a migration run or pre-package database copy to validate database integrity.
+ *
+ * @param db The SQLite database.
+ */
+ @SuppressWarnings("deprecation")
+ @NonNull
+ protected ValidationResult onValidateSchema(@NonNull SupportSQLiteDatabase db) {
+ validateMigration(db);
+ return new ValidationResult(true, null);
+ }
+
+ /**
+ * Called before migrations execute to perform preliminary work.
+ * @param database The SQLite database.
+ */
+ protected void onPreMigrate(SupportSQLiteDatabase database) {
+
+ }
+
+ /**
+ * Called after migrations execute to perform additional work.
+ * @param database The SQLite database.
+ */
+ protected void onPostMigrate(SupportSQLiteDatabase database) {
+
+ }
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static class ValidationResult {
+
+ public final boolean isValid;
+ @Nullable
+ public final String expectedFoundMsg;
+
+ public ValidationResult(boolean isValid, @Nullable String expectedFoundMsg) {
+ this.isValid = isValid;
+ this.expectedFoundMsg = expectedFoundMsg;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/RoomSQLiteQuery.java b/app/src/main/java/androidx/room/RoomSQLiteQuery.java
new file mode 100644
index 0000000000..689352c032
--- /dev/null
+++ b/app/src/main/java/androidx/room/RoomSQLiteQuery.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2017 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;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.sqlite.db.SupportSQLiteProgram;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * This class is used as an intermediate place to keep binding arguments so that we can run
+ * Cursor queries with correct types rather than passing everything as a string.
+ *
+ * Because it is relatively a big object, they are pooled and must be released after each use.
+ *
+ * @hide
+ */
+@SuppressWarnings("unused")
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class RoomSQLiteQuery implements SupportSQLiteQuery, SupportSQLiteProgram {
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ // Maximum number of queries we'll keep cached.
+ static final int POOL_LIMIT = 15;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ // Once we hit POOL_LIMIT, we'll bring the pool size back to the desired number. We always
+ // clear the bigger queries (# of arguments).
+ static final int DESIRED_POOL_SIZE = 10;
+ private volatile String mQuery;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final long[] mLongBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final double[] mDoubleBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final String[] mStringBindings;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final byte[][] mBlobBindings;
+
+ @Binding
+ private final int[] mBindingTypes;
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ final int mCapacity;
+ // number of arguments in the query
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ int mArgCount;
+
+
+ @SuppressWarnings("WeakerAccess")
+ @VisibleForTesting
+ static final TreeMap sQueryPool = new TreeMap<>();
+
+ /**
+ * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
+ *
+ * @param supportSQLiteQuery The query to copy from
+ * @return A new query copied from the provided one.
+ */
+ public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
+ final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
+ supportSQLiteQuery.getSql(),
+ supportSQLiteQuery.getArgCount());
+ supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
+ @Override
+ public void bindNull(int index) {
+ query.bindNull(index);
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ query.bindLong(index, value);
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ query.bindDouble(index, value);
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ query.bindString(index, value);
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ query.bindBlob(index, value);
+ }
+
+ @Override
+ public void clearBindings() {
+ query.clearBindings();
+ }
+
+ @Override
+ public void close() {
+ // ignored.
+ }
+ });
+ return query;
+ }
+
+ /**
+ * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
+ * given query.
+ *
+ * @param query The query to prepare
+ * @param argumentCount The number of query arguments
+ * @return A RoomSQLiteQuery that holds the given query and has space for the given number of
+ * arguments.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static RoomSQLiteQuery acquire(String query, int argumentCount) {
+ synchronized (sQueryPool) {
+ final Map.Entry entry =
+ sQueryPool.ceilingEntry(argumentCount);
+ if (entry != null) {
+ sQueryPool.remove(entry.getKey());
+ final RoomSQLiteQuery sqliteQuery = entry.getValue();
+ sqliteQuery.init(query, argumentCount);
+ return sqliteQuery;
+ }
+ }
+ RoomSQLiteQuery sqLiteQuery = new RoomSQLiteQuery(argumentCount);
+ sqLiteQuery.init(query, argumentCount);
+ return sqLiteQuery;
+ }
+
+ private RoomSQLiteQuery(int capacity) {
+ mCapacity = capacity;
+ // because, 1 based indices... we don't want to offsets everything with 1 all the time.
+ int limit = capacity + 1;
+ //noinspection WrongConstant
+ mBindingTypes = new int[limit];
+ mLongBindings = new long[limit];
+ mDoubleBindings = new double[limit];
+ mStringBindings = new String[limit];
+ mBlobBindings = new byte[limit][];
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ void init(String query, int argCount) {
+ mQuery = query;
+ mArgCount = argCount;
+ }
+
+ /**
+ * Releases the query back to the pool.
+ *
+ * After released, the statement might be returned when {@link #acquire(String, int)} is called
+ * so you should never re-use it after releasing.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public void release() {
+ synchronized (sQueryPool) {
+ sQueryPool.put(mCapacity, this);
+ prunePoolLocked();
+ }
+ }
+
+ private static void prunePoolLocked() {
+ if (sQueryPool.size() > POOL_LIMIT) {
+ int toBeRemoved = sQueryPool.size() - DESIRED_POOL_SIZE;
+ final Iterator iterator = sQueryPool.descendingKeySet().iterator();
+ while (toBeRemoved-- > 0) {
+ iterator.next();
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public String getSql() {
+ return mQuery;
+ }
+
+ @Override
+ public int getArgCount() {
+ return mArgCount;
+ }
+
+ @Override
+ public void bindTo(SupportSQLiteProgram program) {
+ for (int index = 1; index <= mArgCount; index++) {
+ switch (mBindingTypes[index]) {
+ case NULL:
+ program.bindNull(index);
+ break;
+ case LONG:
+ program.bindLong(index, mLongBindings[index]);
+ break;
+ case DOUBLE:
+ program.bindDouble(index, mDoubleBindings[index]);
+ break;
+ case STRING:
+ program.bindString(index, mStringBindings[index]);
+ break;
+ case BLOB:
+ program.bindBlob(index, mBlobBindings[index]);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void bindNull(int index) {
+ mBindingTypes[index] = NULL;
+ }
+
+ @Override
+ public void bindLong(int index, long value) {
+ mBindingTypes[index] = LONG;
+ mLongBindings[index] = value;
+ }
+
+ @Override
+ public void bindDouble(int index, double value) {
+ mBindingTypes[index] = DOUBLE;
+ mDoubleBindings[index] = value;
+ }
+
+ @Override
+ public void bindString(int index, String value) {
+ mBindingTypes[index] = STRING;
+ mStringBindings[index] = value;
+ }
+
+ @Override
+ public void bindBlob(int index, byte[] value) {
+ mBindingTypes[index] = BLOB;
+ mBlobBindings[index] = value;
+ }
+
+ @Override
+ public void close() {
+ // no-op. not calling release because it is internal API.
+ }
+
+ /**
+ * Copies arguments from another RoomSQLiteQuery into this query.
+ *
+ * @param other The other query, which holds the arguments to be copied.
+ */
+ public void copyArgumentsFrom(RoomSQLiteQuery other) {
+ int argCount = other.getArgCount() + 1; // +1 for the binding offsets
+ System.arraycopy(other.mBindingTypes, 0, mBindingTypes, 0, argCount);
+ System.arraycopy(other.mLongBindings, 0, mLongBindings, 0, argCount);
+ System.arraycopy(other.mStringBindings, 0, mStringBindings, 0, argCount);
+ System.arraycopy(other.mBlobBindings, 0, mBlobBindings, 0, argCount);
+ System.arraycopy(other.mDoubleBindings, 0, mDoubleBindings, 0, argCount);
+ }
+
+ @Override
+ public void clearBindings() {
+ Arrays.fill(mBindingTypes, NULL);
+ Arrays.fill(mStringBindings, null);
+ Arrays.fill(mBlobBindings, null);
+ mQuery = null;
+ // no need to clear others
+ }
+
+ private static final int NULL = 1;
+ private static final int LONG = 2;
+ private static final int DOUBLE = 3;
+ private static final int STRING = 4;
+ private static final int BLOB = 5;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({NULL, LONG, DOUBLE, STRING, BLOB})
+ @interface Binding {
+ }
+}
diff --git a/app/src/main/java/androidx/room/RoomTrackingLiveData.java b/app/src/main/java/androidx/room/RoomTrackingLiveData.java
new file mode 100644
index 0000000000..8df1014a41
--- /dev/null
+++ b/app/src/main/java/androidx/room/RoomTrackingLiveData.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright 2018 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;
+
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.arch.core.executor.ArchTaskExecutor;
+import androidx.lifecycle.LiveData;
+
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A LiveData implementation that closely works with {@link InvalidationTracker} to implement
+ * database drive {@link androidx.lifecycle.LiveData} queries that are strongly hold as long
+ * as they are active.
+ *
+ * We need this extra handling for {@link androidx.lifecycle.LiveData} because when they are
+ * observed forever, there is no {@link androidx.lifecycle.Lifecycle} that will keep them in
+ * memory but they should stay. We cannot add-remove observer in {@link LiveData#onActive()},
+ * {@link LiveData#onInactive()} because that would mean missing changes in between or doing an
+ * extra query on every UI rotation.
+ *
+ * This {@link LiveData} keeps a weak observer to the {@link InvalidationTracker} but it is hold
+ * strongly by the {@link InvalidationTracker} as long as it is active.
+ */
+class RoomTrackingLiveData extends LiveData {
+ @SuppressWarnings("WeakerAccess")
+ final RoomDatabase mDatabase;
+
+ @SuppressWarnings("WeakerAccess")
+ final boolean mInTransaction;
+
+ @SuppressWarnings("WeakerAccess")
+ final Callable mComputeFunction;
+
+ private final InvalidationLiveDataContainer mContainer;
+
+ @SuppressWarnings("WeakerAccess")
+ final InvalidationTracker.Observer mObserver;
+
+ @SuppressWarnings("WeakerAccess")
+ final AtomicBoolean mInvalid = new AtomicBoolean(true);
+
+ @SuppressWarnings("WeakerAccess")
+ final AtomicBoolean mComputing = new AtomicBoolean(false);
+
+ @SuppressWarnings("WeakerAccess")
+ final AtomicBoolean mRegisteredObserver = new AtomicBoolean(false);
+
+ @SuppressWarnings("WeakerAccess")
+ final Runnable mRefreshRunnable = new Runnable() {
+ @WorkerThread
+ @Override
+ public void run() {
+ if (mRegisteredObserver.compareAndSet(false, true)) {
+ mDatabase.getInvalidationTracker().addWeakObserver(mObserver);
+ }
+ boolean computed;
+ do {
+ computed = false;
+ // compute can happen only in 1 thread but no reason to lock others.
+ if (mComputing.compareAndSet(false, true)) {
+ // as long as it is invalid, keep computing.
+ try {
+ T value = null;
+ while (mInvalid.compareAndSet(true, false)) {
+ computed = true;
+ try {
+ value = mComputeFunction.call();
+ } catch (Exception e) {
+ throw new RuntimeException("Exception while computing database"
+ + " live data.", e);
+ }
+ }
+ if (computed) {
+ postValue(value);
+ }
+ } finally {
+ // release compute lock
+ mComputing.set(false);
+ }
+ }
+ // check invalid after releasing compute lock to avoid the following scenario.
+ // Thread A runs compute()
+ // Thread A checks invalid, it is false
+ // Main thread sets invalid to true
+ // Thread B runs, fails to acquire compute lock and skips
+ // Thread A releases compute lock
+ // We've left invalid in set state. The check below recovers.
+ } while (computed && mInvalid.get());
+ }
+ };
+
+ @SuppressWarnings("WeakerAccess")
+ final Runnable mInvalidationRunnable = new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ boolean isActive = hasActiveObservers();
+ if (mInvalid.compareAndSet(false, true)) {
+ if (isActive) {
+ getQueryExecutor().execute(mRefreshRunnable);
+ }
+ }
+ }
+ };
+ @SuppressLint("RestrictedApi")
+ RoomTrackingLiveData(
+ RoomDatabase database,
+ InvalidationLiveDataContainer container,
+ boolean inTransaction,
+ Callable computeFunction,
+ String[] tableNames) {
+ mDatabase = database;
+ mInTransaction = inTransaction;
+ mComputeFunction = computeFunction;
+ mContainer = container;
+ mObserver = new InvalidationTracker.Observer(tableNames) {
+ @Override
+ public void onInvalidated(@NonNull Set tables) {
+ ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
+ }
+ };
+ }
+
+ @Override
+ protected void onActive() {
+ super.onActive();
+ mContainer.onActive(this);
+ getQueryExecutor().execute(mRefreshRunnable);
+ }
+
+ @Override
+ protected void onInactive() {
+ super.onInactive();
+ mContainer.onInactive(this);
+ }
+
+ Executor getQueryExecutor() {
+ if (mInTransaction) {
+ return mDatabase.getTransactionExecutor();
+ } else {
+ return mDatabase.getQueryExecutor();
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java
new file mode 100644
index 0000000000..f8240ceed3
--- /dev/null
+++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelper.java
@@ -0,0 +1,205 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.room.util.CopyLock;
+import androidx.room.util.DBUtil;
+import androidx.room.util.FileUtil;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * An open helper that will copy & open a pre-populated database if it doesn't exists in internal
+ * storage.
+ */
+class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
+
+ @NonNull
+ private final Context mContext;
+ @Nullable
+ private final String mCopyFromAssetPath;
+ @Nullable
+ private final File mCopyFromFile;
+ private final int mDatabaseVersion;
+ @NonNull
+ private final SupportSQLiteOpenHelper mDelegate;
+ @Nullable
+ private DatabaseConfiguration mDatabaseConfiguration;
+
+ private boolean mVerified;
+
+ SQLiteCopyOpenHelper(
+ @NonNull Context context,
+ @Nullable String copyFromAssetPath,
+ @Nullable File copyFromFile,
+ int databaseVersion,
+ @NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
+ mContext = context;
+ mCopyFromAssetPath = copyFromAssetPath;
+ mCopyFromFile = copyFromFile;
+ mDatabaseVersion = databaseVersion;
+ mDelegate = supportSQLiteOpenHelper;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return mDelegate.getDatabaseName();
+ }
+
+ @Override
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+ public void setWriteAheadLoggingEnabled(boolean enabled) {
+ mDelegate.setWriteAheadLoggingEnabled(enabled);
+ }
+
+ @Override
+ public synchronized SupportSQLiteDatabase getWritableDatabase() {
+ if (!mVerified) {
+ verifyDatabaseFile();
+ mVerified = true;
+ }
+ return mDelegate.getWritableDatabase();
+ }
+
+ @Override
+ public synchronized SupportSQLiteDatabase getReadableDatabase() {
+ if (!mVerified) {
+ verifyDatabaseFile();
+ mVerified = true;
+ }
+ return mDelegate.getReadableDatabase();
+ }
+
+ @Override
+ public synchronized void close() {
+ mDelegate.close();
+ mVerified = false;
+ }
+
+ // Can't be constructor param because the factory is needed by the database builder which in
+ // turn is the one that actually builds the configuration.
+ void setDatabaseConfiguration(@Nullable DatabaseConfiguration databaseConfiguration) {
+ mDatabaseConfiguration = databaseConfiguration;
+ }
+
+ private void verifyDatabaseFile() {
+ String databaseName = getDatabaseName();
+ File databaseFile = mContext.getDatabasePath(databaseName);
+ boolean processLevelLock = mDatabaseConfiguration == null
+ || mDatabaseConfiguration.multiInstanceInvalidation;
+ CopyLock copyLock = new CopyLock(databaseName, mContext.getFilesDir(), processLevelLock);
+ try {
+ // Acquire a copy lock, this lock works across threads and processes, preventing
+ // concurrent copy attempts from occurring.
+ copyLock.lock();
+
+ if (!databaseFile.exists()) {
+ try {
+ // No database file found, copy and be done.
+ copyDatabaseFile(databaseFile);
+ return;
+ } catch (IOException e) {
+ throw new RuntimeException("Unable to copy database file.", e);
+ }
+ }
+
+ if (mDatabaseConfiguration == null) {
+ return;
+ }
+
+ // A database file is present, check if we need to re-copy it.
+ int currentVersion;
+ try {
+ currentVersion = DBUtil.readVersion(databaseFile);
+ } catch (IOException e) {
+ Log.w(Room.LOG_TAG, "Unable to read database version.", e);
+ return;
+ }
+
+ if (currentVersion == mDatabaseVersion) {
+ return;
+ }
+
+ if (mDatabaseConfiguration.isMigrationRequired(currentVersion, mDatabaseVersion)) {
+ // From the current version to the desired version a migration is required, i.e.
+ // we won't be performing a copy destructive migration.
+ return;
+ }
+
+ if (mContext.deleteDatabase(databaseName)) {
+ try {
+ copyDatabaseFile(databaseFile);
+ } catch (IOException e) {
+ // We are more forgiving copying a database on a destructive migration since
+ // there is already a database file that can be opened.
+ Log.w(Room.LOG_TAG, "Unable to copy database file.", e);
+ }
+ } else {
+ Log.w(Room.LOG_TAG, "Failed to delete database file ("
+ + databaseName + ") for a copy destructive migration.");
+ }
+ } finally {
+ copyLock.unlock();
+ }
+ }
+
+ private void copyDatabaseFile(File destinationFile) throws IOException {
+ ReadableByteChannel input;
+ if (mCopyFromAssetPath != null) {
+ input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
+ } else if (mCopyFromFile != null) {
+ input = new FileInputStream(mCopyFromFile).getChannel();
+ } else {
+ throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
+ }
+
+ // An intermediate file is used so that we never end up with a half-copied database file
+ // in the internal directory.
+ File intermediateFile = File.createTempFile(
+ "room-copy-helper", ".tmp", mContext.getCacheDir());
+ intermediateFile.deleteOnExit();
+ FileChannel output = new FileOutputStream(intermediateFile).getChannel();
+ FileUtil.copy(input, output);
+
+ File parent = destinationFile.getParentFile();
+ if (parent != null && !parent.exists() && !parent.mkdirs()) {
+ throw new IOException("Failed to create directories for "
+ + destinationFile.getAbsolutePath());
+ }
+
+ if (!intermediateFile.renameTo(destinationFile)) {
+ throw new IOException("Failed to move intermediate file ("
+ + intermediateFile.getAbsolutePath() + ") to destination ("
+ + destinationFile.getAbsolutePath() + ").");
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java
new file mode 100644
index 0000000000..22178e80ee
--- /dev/null
+++ b/app/src/main/java/androidx/room/SQLiteCopyOpenHelperFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.sqlite.db.SupportSQLiteOpenHelper;
+
+import java.io.File;
+
+/**
+ * Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
+ * {@link SQLiteCopyOpenHelper}.
+ */
+class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
+
+ @Nullable
+ private final String mCopyFromAssetPath;
+ @Nullable
+ private final File mCopyFromFile;
+ @NonNull
+ private final SupportSQLiteOpenHelper.Factory mDelegate;
+
+ SQLiteCopyOpenHelperFactory(
+ @Nullable String copyFromAssetPath,
+ @Nullable File copyFromFile,
+ @NonNull SupportSQLiteOpenHelper.Factory factory) {
+ mCopyFromAssetPath = copyFromAssetPath;
+ mCopyFromFile = copyFromFile;
+ mDelegate = factory;
+ }
+
+ @Override
+ public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
+ return new SQLiteCopyOpenHelper(
+ configuration.context,
+ mCopyFromAssetPath,
+ mCopyFromFile,
+ configuration.callback.version,
+ mDelegate.create(configuration));
+ }
+}
diff --git a/app/src/main/java/androidx/room/SharedSQLiteStatement.java b/app/src/main/java/androidx/room/SharedSQLiteStatement.java
new file mode 100644
index 0000000000..20c06c8b50
--- /dev/null
+++ b/app/src/main/java/androidx/room/SharedSQLiteStatement.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.room;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteStatement;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Represents a prepared SQLite state that can be re-used multiple times.
+ *
+ * This class is used by generated code. After it is used, {@code release} must be called so that
+ * it can be used by other threads.
+ *
+ * To avoid re-entry even within the same thread, this class allows only 1 time access to the shared
+ * statement until it is released.
+ *
+ * @hide
+ */
+@SuppressWarnings({"WeakerAccess", "unused"})
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class SharedSQLiteStatement {
+ private final AtomicBoolean mLock = new AtomicBoolean(false);
+
+ private final RoomDatabase mDatabase;
+ private volatile SupportSQLiteStatement mStmt;
+
+ /**
+ * Creates an SQLite prepared statement that can be re-used across threads. If it is in use,
+ * it automatically creates a new one.
+ *
+ * @param database The database to create the statement in.
+ */
+ public SharedSQLiteStatement(RoomDatabase database) {
+ mDatabase = database;
+ }
+
+ /**
+ * Create the query.
+ *
+ * @return The SQL query to prepare.
+ */
+ protected abstract String createQuery();
+
+ protected void assertNotMainThread() {
+ mDatabase.assertNotMainThread();
+ }
+
+ private SupportSQLiteStatement createNewStatement() {
+ String query = createQuery();
+ return mDatabase.compileStatement(query);
+ }
+
+ private SupportSQLiteStatement getStmt(boolean canUseCached) {
+ final SupportSQLiteStatement stmt;
+ if (canUseCached) {
+ if (mStmt == null) {
+ mStmt = createNewStatement();
+ }
+ stmt = mStmt;
+ } else {
+ // it is in use, create a one off statement
+ stmt = createNewStatement();
+ }
+ return stmt;
+ }
+
+ /**
+ * Call this to get the statement. Must call {@link #release(SupportSQLiteStatement)} once done.
+ */
+ public SupportSQLiteStatement acquire() {
+ assertNotMainThread();
+ return getStmt(mLock.compareAndSet(false, true));
+ }
+
+ /**
+ * Must call this when statement will not be used anymore.
+ *
+ * @param statement The statement that was returned from acquire.
+ */
+ public void release(SupportSQLiteStatement statement) {
+ if (statement == mStmt) {
+ mLock.set(false);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/TransactionExecutor.java b/app/src/main/java/androidx/room/TransactionExecutor.java
new file mode 100644
index 0000000000..6a6bc1260d
--- /dev/null
+++ b/app/src/main/java/androidx/room/TransactionExecutor.java
@@ -0,0 +1,62 @@
+/*
+ * 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;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayDeque;
+import java.util.concurrent.Executor;
+
+/**
+ * Executor wrapper for performing database transactions serially.
+ *
+ * Since database transactions are exclusive, this executor ensures that transactions are performed
+ * in-order and one at a time, preventing threads from blocking each other when multiple concurrent
+ * transactions are attempted.
+ */
+class TransactionExecutor implements Executor {
+
+ private final Executor mExecutor;
+ private final ArrayDeque mTasks = new ArrayDeque<>();
+ private Runnable mActive;
+
+ TransactionExecutor(@NonNull Executor executor) {
+ mExecutor = executor;
+ }
+
+ public synchronized void execute(final Runnable command) {
+ mTasks.offer(new Runnable() {
+ public void run() {
+ try {
+ command.run();
+ } finally {
+ scheduleNext();
+ }
+ }
+ });
+ if (mActive == null) {
+ scheduleNext();
+ }
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ synchronized void scheduleNext() {
+ if ((mActive = mTasks.poll()) != null) {
+ mExecutor.execute(mActive);
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/migration/Migration.java b/app/src/main/java/androidx/room/migration/Migration.java
new file mode 100644
index 0000000000..4aa7a7e86a
--- /dev/null
+++ b/app/src/main/java/androidx/room/migration/Migration.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 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.migration;
+
+import androidx.annotation.NonNull;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+/**
+ * Base class for a database migration.
+ *
+ * Each migration can move between 2 versions that are defined by {@link #startVersion} and
+ * {@link #endVersion}.
+ *
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is >= 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ *
+ * If there are not enough migrations provided to move from the current version to the latest
+ * version, Room will clear the database and recreate so even if you have no changes between 2
+ * versions, you should still provide a Migration object to the builder.
+ */
+public abstract class Migration {
+ public final int startVersion;
+ public final int endVersion;
+
+ /**
+ * Creates a new migration between {@code startVersion} and {@code endVersion}.
+ *
+ * @param startVersion The start version of the database.
+ * @param endVersion The end version of the database after this migration is applied.
+ */
+ public Migration(int startVersion, int endVersion) {
+ this.startVersion = startVersion;
+ this.endVersion = endVersion;
+ }
+
+ /**
+ * Should run the necessary migrations.
+ *
+ * This class cannot access any generated Dao in this method.
+ *
+ * This method is already called inside a transaction and that transaction might actually be a
+ * composite transaction of all necessary {@code Migration}s.
+ *
+ * @param database The database instance
+ */
+ public abstract void migrate(@NonNull SupportSQLiteDatabase database);
+}
diff --git a/app/src/main/java/androidx/room/package-info.java b/app/src/main/java/androidx/room/package-info.java
new file mode 100644
index 0000000000..3a14e974ff
--- /dev/null
+++ b/app/src/main/java/androidx/room/package-info.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+/**
+ * Room is a Database Object Mapping library that makes it easy to access database on Android
+ * applications.
+ *
+ * Rather than hiding the details of SQLite, Room tries to embrace them by providing convenient APIs
+ * to query the database and also verify such queries at compile time. This allows you to access
+ * the full power of SQLite while having the type safety provided by Java SQL query builders.
+ *
+ * There are 3 major components in Room.
+ *
+ * - {@link androidx.room.Database Database}: This annotation marks a class as a database.
+ * It should be an abstract class that extends {@link androidx.room.RoomDatabase RoomDatabase}.
+ * At runtime, you can acquire an instance of it via {@link androidx.room.Room#databaseBuilder(
+ * android.content.Context,java.lang.Class, java.lang.String) Room.databaseBuilder} or
+ * {@link androidx.room.Room#inMemoryDatabaseBuilder(android.content.Context, java.lang.Class)
+ * Room.inMemoryDatabaseBuilder}.
+ *
+ * The database class defines the list of entities and data access objects in the database.
+ * It is also the main access point for the underlying connection.
+ *
+ * - {@link androidx.room.Entity Entity}: This annotation marks a class as a database row.
+ * For each {@link androidx.room.Entity Entity}, a database table is created to hold the items.
+ * The Entity class must be referenced in the
+ * {@link androidx.room.Database#entities() Database#entities} array. Each field of the Entity
+ * (and its super class) is persisted in the database unless it is denoted otherwise
+ * (see {@link androidx.room.Entity Entity} docs for details).
+ *
+ * - {@link androidx.room.Dao Dao}: This annotation marks a class or interface as a
+ * Data Access Object. Data access objects are the main components of Room that are
+ * responsible for defining the methods that access the database. The class that is annotated
+ * with {@link androidx.room.Database Database} must have an abstract method that has 0
+ * arguments and returns the class that is annotated with Dao. While generating the code at
+ * compile time, Room will generate an implementation of this class.
+ *
+ * Using Dao classes for database access rather than query builders or direct queries allows you
+ * to keep a separation between different components and easily mock the database access while
+ * testing your application.
+ *
+ *
+ * Below is a sample of a simple database.
+ *
+ * // File: Song.java
+ * {@literal @}Entity
+ * public class User {
+ * {@literal @}PrimaryKey
+ * private int id;
+ * private String name;
+ * {@literal @}ColumnInfo(name = "release_year")
+ * private int releaseYear;
+ * // getters and setters are ignored for brevity but they are required for Room to work.
+ * }
+ * // File: SongDao.java
+ * {@literal @}Dao
+ * public interface SongDao {
+ * {@literal @}Query("SELECT * FROM song")
+ * List<Song> loadAll();
+ * {@literal @}Query("SELECT * FROM song WHERE id IN (:songIds)")
+ * List<Song> loadAllBySongId(int... songIds);
+ * {@literal @}Query("SELECT * FROM song WHERE name LIKE :name AND release_year = :year LIMIT 1")
+ * Song loadOneByNameAndReleaseYear(String first, int year);
+ * {@literal @}Insert
+ * void insertAll(Song... songs);
+ * {@literal @}Delete
+ * void delete(Song song);
+ * }
+ * // File: MusicDatabase.java
+ * {@literal @}Database(entities = {Song.java})
+ * public abstract class MusicDatabase extends RoomDatabase {
+ * public abstract SongDao userDao();
+ * }
+ *
+ * You can create an instance of {@code MusicDatabase} as follows:
+ *
+ * MusicDatabase db = Room
+ * .databaseBuilder(getApplicationContext(), MusicDatabase.class, "database-name")
+ * .build();
+ *
+ * Since Room verifies your queries at compile time, it also detects information about which tables
+ * are accessed by the query or what columns are present in the response.
+ *
+ * You can observe a particular table for changes using the
+ * {@link androidx.room.InvalidationTracker InvalidationTracker} class which you can acquire via
+ * {@link androidx.room.RoomDatabase#getInvalidationTracker()
+ * RoomDatabase.getInvalidationTracker}.
+ *
+ * For convenience, Room allows you to return {@link androidx.lifecycle.LiveData LiveData} from
+ * {@link androidx.room.Query Query} methods. It will automatically observe the related tables as
+ * long as the {@code LiveData} has active observers.
+ *
+ * // This live data will automatically dispatch changes as the database changes.
+ * {@literal @}Query("SELECT * FROM song ORDER BY name LIMIT 5")
+ * LiveData<Song> loadFirstFiveSongs();
+ *
+ *
+ * You can also return arbitrary data objects from your query results as long as the fields in the
+ * object match the list of columns in the query response. This makes it very easy to write
+ * applications that drive the UI from persistent storage.
+ *
+ * class IdAndSongHeader {
+ * int id;
+ * {@literal @}ColumnInfo(name = "header")
+ * String header;
+ * }
+ * // DAO
+ * {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user")
+ * public IdAndSongHeader[] loadSongHeaders();
+ *
+ * If there is a mismatch between the query result and the POJO, Room will print a warning during
+ * compilation.
+ *
+ * Please see the documentation of individual classes for details.
+ */
+package androidx.room;
diff --git a/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
new file mode 100644
index 0000000000..156ee24ac7
--- /dev/null
+++ b/app/src/main/java/androidx/room/paging/LimitOffsetDataSource.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2017 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.paging;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.paging.PositionalDataSource;
+import androidx.room.InvalidationTracker;
+import androidx.room.RoomDatabase;
+import androidx.room.RoomSQLiteQuery;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A simple data source implementation that uses Limit & Offset to page the query.
+ *
+ * This is NOT the most efficient way to do paging on SQLite. It is
+ * recommended to use an indexed
+ * ORDER BY statement but that requires a more complex API. This solution is technically equal to
+ * receiving a {@link Cursor} from a large query but avoids the need to manually manage it, and
+ * never returns inconsistent data if it is invalidated.
+ *
+ * @param Data type returned by the data source.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public abstract class LimitOffsetDataSource extends PositionalDataSource {
+ private final RoomSQLiteQuery mSourceQuery;
+ private final String mCountQuery;
+ private final String mLimitOffsetQuery;
+ private final RoomDatabase mDb;
+ @SuppressWarnings("FieldCanBeLocal")
+ private final InvalidationTracker.Observer mObserver;
+ private final boolean mInTransaction;
+
+ protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
+ boolean inTransaction, String... tables) {
+ this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
+ }
+
+ protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
+ boolean inTransaction, String... tables) {
+ mDb = db;
+ mSourceQuery = query;
+ mInTransaction = inTransaction;
+ mCountQuery = "SELECT COUNT(*) FROM ( " + mSourceQuery.getSql() + " )";
+ mLimitOffsetQuery = "SELECT * FROM ( " + mSourceQuery.getSql() + " ) LIMIT ? OFFSET ?";
+ mObserver = new InvalidationTracker.Observer(tables) {
+ @Override
+ public void onInvalidated(@NonNull Set tables) {
+ invalidate();
+ }
+ };
+ db.getInvalidationTracker().addWeakObserver(mObserver);
+ }
+
+ /**
+ * Count number of rows query can return
+ *
+ * @hide
+ */
+ @SuppressWarnings("WeakerAccess")
+ public int countItems() {
+ final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
+ mSourceQuery.getArgCount());
+ sqLiteQuery.copyArgumentsFrom(mSourceQuery);
+ Cursor cursor = mDb.query(sqLiteQuery);
+ try {
+ if (cursor.moveToFirst()) {
+ return cursor.getInt(0);
+ }
+ return 0;
+ } finally {
+ cursor.close();
+ sqLiteQuery.release();
+ }
+ }
+
+ @Override
+ public boolean isInvalid() {
+ mDb.getInvalidationTracker().refreshVersionsSync();
+ return super.isInvalid();
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ protected abstract List convertRows(Cursor cursor);
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback callback) {
+ List list = Collections.emptyList();
+ int totalCount = 0;
+ int firstLoadPosition = 0;
+ RoomSQLiteQuery sqLiteQuery = null;
+ Cursor cursor = null;
+ mDb.beginTransaction();
+ try {
+ totalCount = countItems();
+ if (totalCount != 0) {
+ // bound the size requested, based on known count
+ firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
+
+ sqLiteQuery = getSQLiteQuery(firstLoadPosition, firstLoadSize);
+ cursor = mDb.query(sqLiteQuery);
+ List rows = convertRows(cursor);
+ mDb.setTransactionSuccessful();
+ list = rows;
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ mDb.endTransaction();
+ if (sqLiteQuery != null) {
+ sqLiteQuery.release();
+ }
+ }
+
+ callback.onResult(list, firstLoadPosition, totalCount);
+ }
+
+ @Override
+ public void loadRange(@NonNull LoadRangeParams params,
+ @NonNull LoadRangeCallback callback) {
+ callback.onResult(loadRange(params.startPosition, params.loadSize));
+ }
+
+ /**
+ * Return the rows from startPos to startPos + loadCount
+ *
+ * @hide
+ */
+ @SuppressWarnings("deprecation")
+ @NonNull
+ public List loadRange(int startPosition, int loadCount) {
+ final RoomSQLiteQuery sqLiteQuery = getSQLiteQuery(startPosition, loadCount);
+ if (mInTransaction) {
+ mDb.beginTransaction();
+ Cursor cursor = null;
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ cursor = mDb.query(sqLiteQuery);
+ List rows = convertRows(cursor);
+ mDb.setTransactionSuccessful();
+ return rows;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ mDb.endTransaction();
+ sqLiteQuery.release();
+ }
+ } else {
+ Cursor cursor = mDb.query(sqLiteQuery);
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ return convertRows(cursor);
+ } finally {
+ cursor.close();
+ sqLiteQuery.release();
+ }
+ }
+ }
+
+ private RoomSQLiteQuery getSQLiteQuery(int startPosition, int loadCount) {
+ final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
+ mSourceQuery.getArgCount() + 2);
+ sqLiteQuery.copyArgumentsFrom(mSourceQuery);
+ sqLiteQuery.bindLong(sqLiteQuery.getArgCount() - 1, loadCount);
+ sqLiteQuery.bindLong(sqLiteQuery.getArgCount(), startPosition);
+ return sqLiteQuery;
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/CopyLock.java b/app/src/main/java/androidx/room/util/CopyLock.java
new file mode 100644
index 0000000000..3750315f34
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/CopyLock.java
@@ -0,0 +1,112 @@
+/*
+ * 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.util;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.channels.FileChannel;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Utility class for in-process and multi-process key-based lock mechanism for safely copying
+ * database files.
+ *
+ * Acquiring the lock will be quick if no other thread or process has a lock with the same key.
+ * But if the lock is already held then acquiring it will block, until the other thread or process
+ * releases the lock. Note that the key and lock directory must be the same to achieve
+ * synchronization.
+ *
+ * Locking is done via two levels:
+ *
+ * -
+ * Thread locking within the same JVM process is done via a map of String key to ReentrantLock
+ * objects.
+ *
-
+ * Multi-process locking is done via a dummy file whose name contains the key and FileLock
+ * objects.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class CopyLock {
+
+ // in-process lock map
+ private static final Map sThreadLocks = new HashMap<>();
+
+ private final File mCopyLockFile;
+ private final Lock mThreadLock;
+ private final boolean mFileLevelLock;
+ private FileChannel mLockChannel;
+
+ /**
+ * Creates a lock with {@code name} and using {@code lockDir} as the directory for the
+ * lock files.
+ * @param name the name of this lock.
+ * @param lockDir the directory where the lock files will be located.
+ * @param processLock whether to use file for process level locking or not.
+ */
+ public CopyLock(@NonNull String name, @NonNull File lockDir, boolean processLock) {
+ mCopyLockFile = new File(lockDir, name + ".lck");
+ mThreadLock = getThreadLock(mCopyLockFile.getAbsolutePath());
+ mFileLevelLock = processLock;
+ }
+
+ /**
+ * Attempts to grab the lock, blocking if already held by another thread or process.
+ */
+ public void lock() {
+ mThreadLock.lock();
+ if (mFileLevelLock) {
+ try {
+ mLockChannel = new FileOutputStream(mCopyLockFile).getChannel();
+ mLockChannel.lock();
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to grab copy lock.", e);
+ }
+ }
+ }
+
+ /**
+ * Releases the lock.
+ */
+ public void unlock() {
+ if (mLockChannel != null) {
+ try {
+ mLockChannel.close();
+ } catch (IOException ignored) { }
+ }
+ mThreadLock.unlock();
+ }
+
+ private static Lock getThreadLock(String key) {
+ synchronized (sThreadLocks) {
+ Lock threadLock = sThreadLocks.get(key);
+ if (threadLock == null) {
+ threadLock = new ReentrantLock();
+ sThreadLocks.put(key, threadLock);
+ }
+ return threadLock;
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/CursorUtil.java b/app/src/main/java/androidx/room/util/CursorUtil.java
new file mode 100644
index 0000000000..05b8c59170
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/CursorUtil.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+/**
+ * Cursor utilities for Room
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class CursorUtil {
+
+ /**
+ * Copies the given cursor into a in-memory cursor and then closes it.
+ *
+ * This is useful for iterating over a cursor multiple times without the cost of JNI while
+ * reading or IO while filling the window at the expense of memory consumption.
+ *
+ * @param c the cursor to copy.
+ * @return a new cursor containing the same data as the given cursor.
+ */
+ @NonNull
+ public static Cursor copyAndClose(@NonNull Cursor c) {
+ final MatrixCursor matrixCursor;
+ try {
+ matrixCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
+ while (c.moveToNext()) {
+ final Object[] row = new Object[c.getColumnCount()];
+ for (int i = 0; i < c.getColumnCount(); i++) {
+ switch (c.getType(i)) {
+ case Cursor.FIELD_TYPE_NULL:
+ row[i] = null;
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ row[i] = c.getLong(i);
+ break;
+ case Cursor.FIELD_TYPE_FLOAT:
+ row[i] = c.getDouble(i);
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ row[i] = c.getString(i);
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ row[i] = c.getBlob(i);
+ break;
+ default:
+ throw new IllegalStateException();
+ }
+ }
+ matrixCursor.addRow(row);
+ }
+ } finally {
+ c.close();
+ }
+ return matrixCursor;
+ }
+
+ /**
+ * Patches {@link Cursor#getColumnIndex(String)} to work around issues on older devices.
+ * If the column is not found, it retries with the specified name surrounded by backticks.
+ *
+ * @param c The cursor.
+ * @param name The name of the target column.
+ * @return The index of the column, or -1 if not found.
+ */
+ public static int getColumnIndex(@NonNull Cursor c, @NonNull String name) {
+ final int index = c.getColumnIndex(name);
+ if (index >= 0) {
+ return index;
+ }
+ return c.getColumnIndex("`" + name + "`");
+ }
+
+ /**
+ * Patches {@link Cursor#getColumnIndexOrThrow(String)} to work around issues on older devices.
+ * If the column is not found, it retries with the specified name surrounded by backticks.
+ *
+ * @param c The cursor.
+ * @param name The name of the target column.
+ * @return The index of the column.
+ * @throws IllegalArgumentException if the column does not exist.
+ */
+ public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
+ final int index = c.getColumnIndex(name);
+ if (index >= 0) {
+ return index;
+ }
+ return c.getColumnIndexOrThrow("`" + name + "`");
+ }
+
+ private CursorUtil() {
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/DBUtil.java b/app/src/main/java/androidx/room/util/DBUtil.java
new file mode 100644
index 0000000000..f7afd265a3
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/DBUtil.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import android.database.AbstractWindowedCursor;
+import android.database.Cursor;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.room.RoomDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.sqlite.db.SupportSQLiteQuery;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Database utilities for Room
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class DBUtil {
+
+ /**
+ * Performs the SQLiteQuery on the given database.
+ *
+ * This util method encapsulates copying the cursor if the {@code maybeCopy} parameter is
+ * {@code true} and either the api level is below a certain threshold or the full result of the
+ * query does not fit in a single window.
+ *
+ * @param db The database to perform the query on.
+ * @param sqLiteQuery The query to perform.
+ * @param maybeCopy True if the result cursor should maybe be copied, false otherwise.
+ * @return Result of the query.
+ */
+ @NonNull
+ public static Cursor query(RoomDatabase db, SupportSQLiteQuery sqLiteQuery, boolean maybeCopy) {
+ final Cursor cursor = db.query(sqLiteQuery);
+ if (maybeCopy && cursor instanceof AbstractWindowedCursor) {
+ AbstractWindowedCursor windowedCursor = (AbstractWindowedCursor) cursor;
+ int rowsInCursor = windowedCursor.getCount(); // Should fill the window.
+ int rowsInWindow;
+ if (windowedCursor.hasWindow()) {
+ rowsInWindow = windowedCursor.getWindow().getNumRows();
+ } else {
+ rowsInWindow = rowsInCursor;
+ }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || rowsInWindow < rowsInCursor) {
+ return CursorUtil.copyAndClose(windowedCursor);
+ }
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Drops all FTS content sync triggers created by Room.
+ *
+ * FTS content sync triggers created by Room are those that are found in the sqlite_master table
+ * who's names start with 'room_fts_content_sync_'.
+ *
+ * @param db The database.
+ */
+ public static void dropFtsSyncTriggers(SupportSQLiteDatabase db) {
+ List existingTriggers = new ArrayList<>();
+ Cursor cursor = db.query("SELECT name FROM sqlite_master WHERE type = 'trigger'");
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ while (cursor.moveToNext()) {
+ existingTriggers.add(cursor.getString(0));
+ }
+ } finally {
+ cursor.close();
+ }
+
+ for (String triggerName : existingTriggers) {
+ if (triggerName.startsWith("room_fts_content_sync_")) {
+ db.execSQL("DROP TRIGGER IF EXISTS " + triggerName);
+ }
+ }
+ }
+
+ /**
+ * Reads the user version number out of the database header from the given file.
+ *
+ * @param databaseFile the database file.
+ * @return the database version
+ * @throws IOException if something goes wrong reading the file, such as bad database header or
+ * missing permissions.
+ *
+ * @see User Version
+ * Number.
+ */
+ public static int readVersion(@NonNull File databaseFile) throws IOException {
+ FileChannel input = null;
+ try {
+ ByteBuffer buffer = ByteBuffer.allocate(4);
+ input = new FileInputStream(databaseFile).getChannel();
+ input.tryLock(60, 4, true);
+ input.position(60);
+ int read = input.read(buffer);
+ if (read != 4) {
+ throw new IOException("Bad database header, unable to read 4 bytes at offset 60");
+ }
+ buffer.rewind();
+ return buffer.getInt(); // ByteBuffer is big-endian by default
+ } finally {
+ if (input != null) {
+ input.close();
+ }
+ }
+ }
+
+ private DBUtil() {
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/FileUtil.java b/app/src/main/java/androidx/room/util/FileUtil.java
new file mode 100644
index 0000000000..a221ff741a
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/FileUtil.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util;
+
+import android.annotation.SuppressLint;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.nio.channels.ReadableByteChannel;
+
+/**
+ * File utilities for Room
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FileUtil {
+
+ /**
+ * Copies data from the input channel to the output file channel.
+ *
+ * @param input the input channel to copy.
+ * @param output the output channel to copy.
+ * @throws IOException if there is an I/O error.
+ */
+ @SuppressLint("LambdaLast")
+ public static void copy(@NonNull ReadableByteChannel input, @NonNull FileChannel output)
+ throws IOException {
+ try {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
+ output.transferFrom(input, 0, Long.MAX_VALUE);
+ } else {
+ InputStream inputStream = Channels.newInputStream(input);
+ OutputStream outputStream = Channels.newOutputStream(output);
+ int length;
+ byte[] buffer = new byte[1024 * 4];
+ while ((length = inputStream.read(buffer)) > 0) {
+ outputStream.write(buffer, 0, length);
+ }
+ }
+ output.force(false);
+ } finally {
+ input.close();
+ output.close();
+ }
+ }
+
+ private FileUtil() {
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/FtsTableInfo.java b/app/src/main/java/androidx/room/util/FtsTableInfo.java
new file mode 100644
index 0000000000..c076a0238b
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/FtsTableInfo.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2018 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.util;
+
+import android.database.Cursor;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.VisibleForTesting;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A data class that holds the information about an FTS table.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class FtsTableInfo {
+
+ // A set of valid FTS Options
+ private static final String[] FTS_OPTIONS = new String[] {
+ "tokenize=", "compress=", "content=", "languageid=", "matchinfo=", "notindexed=",
+ "order=", "prefix=", "uncompress="};
+
+ /**
+ * The table name
+ */
+ public final String name;
+
+ /**
+ * The column names
+ */
+ public final Set columns;
+
+ /**
+ * The set of options. Each value in the set contains the option in the following format:
+ * <key>=<value>.
+ */
+ public final Set options;
+
+ public FtsTableInfo(String name, Set columns, Set options) {
+ this.name = name;
+ this.columns = columns;
+ this.options = options;
+ }
+
+ public FtsTableInfo(String name, Set columns, String createSql) {
+ this.name = name;
+ this.columns = columns;
+ this.options = parseOptions(createSql);
+ }
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param database The database to read the information from.
+ * @param tableName The table name.
+ * @return A FtsTableInfo containing the columns and options for the provided table name.
+ */
+ public static FtsTableInfo read(SupportSQLiteDatabase database, String tableName) {
+ Set columns = readColumns(database, tableName);
+ Set options = readOptions(database, tableName);
+
+ return new FtsTableInfo(tableName, columns, options);
+ }
+
+ @SuppressWarnings("TryFinallyCanBeTryWithResources")
+ private static Set readColumns(SupportSQLiteDatabase database, String tableName) {
+ Cursor cursor = database.query("PRAGMA table_info(`" + tableName + "`)");
+ Set columns = new HashSet<>();
+ try {
+ if (cursor.getColumnCount() > 0) {
+ int nameIndex = cursor.getColumnIndex("name");
+ while (cursor.moveToNext()) {
+ columns.add(cursor.getString(nameIndex));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return columns;
+ }
+
+ @SuppressWarnings("TryFinallyCanBeTryWithResources")
+ private static Set readOptions(SupportSQLiteDatabase database, String tableName) {
+ String sql = "";
+ Cursor cursor = database.query(
+ "SELECT * FROM sqlite_master WHERE `name` = '" + tableName + "'");
+ try {
+ if (cursor.moveToFirst()) {
+ sql = cursor.getString(cursor.getColumnIndexOrThrow("sql"));
+ }
+ } finally {
+ cursor.close();
+ }
+ return parseOptions(sql);
+ }
+
+ /**
+ * Parses FTS options from the create statement of an FTS table.
+ *
+ * This method assumes the given create statement is a valid well-formed SQLite statement as
+ * defined in the CREATE VIRTUAL TABLE
+ * syntax diagram.
+ *
+ * @param createStatement the "CREATE VIRTUAL TABLE" statement.
+ * @return the set of FTS option key and values in the create statement.
+ */
+ @VisibleForTesting
+ @SuppressWarnings("WeakerAccess") /* synthetic access */
+ static Set parseOptions(String createStatement) {
+ if (createStatement.isEmpty()) {
+ return new HashSet<>();
+ }
+
+ // Module arguments are within the parenthesis followed by the module name.
+ String argsString = createStatement.substring(
+ createStatement.indexOf('(') + 1,
+ createStatement.lastIndexOf(')'));
+
+ // Split the module argument string by the comma delimiter, keeping track of quotation so
+ // so that if the delimiter is found within a string literal we don't substring at the wrong
+ // index. SQLite supports four ways of quoting keywords, see:
+ // https://www.sqlite.org/lang_keywords.html
+ List args = new ArrayList<>();
+ ArrayDeque quoteStack = new ArrayDeque<>();
+ int lastDelimiterIndex = -1;
+ for (int i = 0; i < argsString.length(); i++) {
+ char c = argsString.charAt(i);
+ switch (c) {
+ case '\'':
+ case '"':
+ case '`':
+ if (quoteStack.isEmpty()) {
+ quoteStack.push(c);
+ } else if (quoteStack.peek() == c) {
+ quoteStack.pop();
+ }
+ break;
+ case '[':
+ if (quoteStack.isEmpty()) {
+ quoteStack.push(c);
+ }
+ break;
+ case ']':
+ if (!quoteStack.isEmpty() && quoteStack.peek() == '[') {
+ quoteStack.pop();
+ }
+ break;
+ case ',':
+ if (quoteStack.isEmpty()) {
+ args.add(argsString.substring(lastDelimiterIndex + 1, i).trim());
+ lastDelimiterIndex = i;
+ }
+ break;
+ }
+ }
+ args.add(argsString.substring(lastDelimiterIndex + 1).trim()); // Add final argument.
+
+ // Match args against valid options, otherwise they are column definitions.
+ HashSet options = new HashSet<>();
+ for (String arg : args) {
+ for (String validOption : FTS_OPTIONS) {
+ if (arg.startsWith(validOption)) {
+ options.add(arg);
+ }
+ }
+ }
+
+ return options;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FtsTableInfo that = (FtsTableInfo) o;
+
+ if (name != null ? !name.equals(that.name) : that.name != null) return false;
+ if (columns != null ? !columns.equals(that.columns) : that.columns != null) return false;
+ return options != null ? options.equals(that.options) : that.options == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (columns != null ? columns.hashCode() : 0);
+ result = 31 * result + (options != null ? options.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "FtsTableInfo{"
+ + "name='" + name + '\''
+ + ", columns=" + columns
+ + ", options=" + options
+ + '}';
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/SneakyThrow.java b/app/src/main/java/androidx/room/util/SneakyThrow.java
new file mode 100644
index 0000000000..76968ab344
--- /dev/null
+++ b/app/src/main/java/androidx/room/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.room.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 checked exception as if it was a runtime exception without wrapping it.
+ *
+ * @param e the exception to re-throw.
+ */
+ public static void reThrow(@NonNull Exception e) {
+ sneakyThrow(e);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void sneakyThrow(@NonNull Throwable e) throws E {
+ throw (E) e;
+ }
+
+ private SneakyThrow() {
+
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/StringUtil.java b/app/src/main/java/androidx/room/util/StringUtil.java
new file mode 100644
index 0000000000..2b30eb64de
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/StringUtil.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.util;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/**
+ * @hide
+ *
+ * String utilities for Room
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class StringUtil {
+
+ @SuppressWarnings("unused")
+ public static final String[] EMPTY_STRING_ARRAY = new String[0];
+ /**
+ * Returns a new StringBuilder to be used while producing SQL queries.
+ *
+ * @return A new or recycled StringBuilder
+ */
+ public static StringBuilder newStringBuilder() {
+ // TODO pool:
+ return new StringBuilder();
+ }
+
+ /**
+ * Adds bind variable placeholders (?) to the given string. Each placeholder is separated
+ * by a comma.
+ *
+ * @param builder The StringBuilder for the query
+ * @param count Number of placeholders
+ */
+ public static void appendPlaceholders(StringBuilder builder, int count) {
+ for (int i = 0; i < count; i++) {
+ builder.append("?");
+ if (i < count - 1) {
+ builder.append(",");
+ }
+ }
+ }
+ /**
+ * Splits a comma separated list of integers to integer list.
+ *
+ * If an input is malformed, it is omitted from the result.
+ *
+ * @param input Comma separated list of integers.
+ * @return A List containing the integers or null if the input is null.
+ */
+ @Nullable
+ public static List splitToIntList(@Nullable String input) {
+ if (input == null) {
+ return null;
+ }
+ List result = new ArrayList<>();
+ StringTokenizer tokenizer = new StringTokenizer(input, ",");
+ while (tokenizer.hasMoreElements()) {
+ final String item = tokenizer.nextToken();
+ try {
+ result.add(Integer.parseInt(item));
+ } catch (NumberFormatException ex) {
+ Log.e("ROOM", "Malformed integer list", ex);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Joins the given list of integers into a comma separated list.
+ *
+ * @param input The list of integers.
+ * @return Comma separated string composed of integers in the list. If the list is null, return
+ * value is null.
+ */
+ @Nullable
+ public static String joinIntoString(@Nullable List input) {
+ if (input == null) {
+ return null;
+ }
+
+ final int size = input.size();
+ if (size == 0) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < size; i++) {
+ sb.append(Integer.toString(input.get(i)));
+ if (i < size - 1) {
+ sb.append(",");
+ }
+ }
+ return sb.toString();
+ }
+
+ private StringUtil() {
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/TableInfo.java b/app/src/main/java/androidx/room/util/TableInfo.java
new file mode 100644
index 0000000000..a28aea49d3
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/TableInfo.java
@@ -0,0 +1,665 @@
+/*
+ * Copyright (C) 2017 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.util;
+
+import android.database.Cursor;
+import android.os.Build;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.room.ColumnInfo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * A data class that holds the information about a table.
+ *
+ * It directly maps to the result of {@code PRAGMA table_info()}. Check the
+ * PRAGMA table_info
+ * documentation for more details.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
+ "SimplifiableIfStatement"})
+// if you change this class, you must change TableInfoWriter.kt
+public class TableInfo {
+
+ /**
+ * Identifies from where the info object was created.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {CREATED_FROM_UNKNOWN, CREATED_FROM_ENTITY, CREATED_FROM_DATABASE})
+ @interface CreatedFrom {
+ }
+
+ /**
+ * Identifier for when the info is created from an unknown source.
+ */
+ public static final int CREATED_FROM_UNKNOWN = 0;
+
+ /**
+ * Identifier for when the info is created from an entity definition, such as generated code
+ * by the compiler or at runtime from a schema bundle, parsed from a schema JSON file.
+ */
+ public static final int CREATED_FROM_ENTITY = 1;
+
+ /**
+ * Identifier for when the info is created from the database itself, reading information from a
+ * PRAGMA, such as table_info.
+ */
+ public static final int CREATED_FROM_DATABASE = 2;
+
+ /**
+ * The table name.
+ */
+ public final String name;
+ /**
+ * Unmodifiable map of columns keyed by column name.
+ */
+ public final Map columns;
+
+ public final Set foreignKeys;
+
+ /**
+ * Sometimes, Index information is not available (older versions). If so, we skip their
+ * verification.
+ */
+ @Nullable
+ public final Set indices;
+
+ @SuppressWarnings("unused")
+ public TableInfo(String name, Map columns, Set foreignKeys,
+ Set indices) {
+ this.name = name;
+ this.columns = Collections.unmodifiableMap(columns);
+ this.foreignKeys = Collections.unmodifiableSet(foreignKeys);
+ this.indices = indices == null ? null : Collections.unmodifiableSet(indices);
+ }
+
+ /**
+ * For backward compatibility with dbs created with older versions.
+ */
+ @SuppressWarnings("unused")
+ public TableInfo(String name, Map columns, Set foreignKeys) {
+ this(name, columns, foreignKeys, Collections.emptySet());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ TableInfo tableInfo = (TableInfo) o;
+
+ if (name != null ? !name.equals(tableInfo.name) : tableInfo.name != null) return false;
+ if (columns != null ? !columns.equals(tableInfo.columns) : tableInfo.columns != null) {
+ return false;
+ }
+ if (foreignKeys != null ? !foreignKeys.equals(tableInfo.foreignKeys)
+ : tableInfo.foreignKeys != null) {
+ return false;
+ }
+ if (indices == null || tableInfo.indices == null) {
+ // if one us is missing index information, seems like we couldn't acquire the
+ // information so we better skip.
+ return true;
+ }
+ return indices.equals(tableInfo.indices);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (columns != null ? columns.hashCode() : 0);
+ result = 31 * result + (foreignKeys != null ? foreignKeys.hashCode() : 0);
+ // skip index, it is not reliable for comparison.
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "TableInfo{"
+ + "name='" + name + '\''
+ + ", columns=" + columns
+ + ", foreignKeys=" + foreignKeys
+ + ", indices=" + indices
+ + '}';
+ }
+
+ /**
+ * Reads the table information from the given database.
+ *
+ * @param database The database to read the information from.
+ * @param tableName The table name.
+ * @return A TableInfo containing the schema information for the provided table name.
+ */
+ @SuppressWarnings("SameParameterValue")
+ public static TableInfo read(SupportSQLiteDatabase database, String tableName) {
+ Map columns = readColumns(database, tableName);
+ Set foreignKeys = readForeignKeys(database, tableName);
+ Set indices = readIndices(database, tableName);
+ return new TableInfo(tableName, columns, foreignKeys, indices);
+ }
+
+ private static Set readForeignKeys(SupportSQLiteDatabase database,
+ String tableName) {
+ Set foreignKeys = new HashSet<>();
+ // this seems to return everything in order but it is not documented so better be safe
+ Cursor cursor = database.query("PRAGMA foreign_key_list(`" + tableName + "`)");
+ try {
+ final int idColumnIndex = cursor.getColumnIndex("id");
+ final int seqColumnIndex = cursor.getColumnIndex("seq");
+ final int tableColumnIndex = cursor.getColumnIndex("table");
+ final int onDeleteColumnIndex = cursor.getColumnIndex("on_delete");
+ final int onUpdateColumnIndex = cursor.getColumnIndex("on_update");
+
+ final List ordered = readForeignKeyFieldMappings(cursor);
+ final int count = cursor.getCount();
+ for (int position = 0; position < count; position++) {
+ cursor.moveToPosition(position);
+ final int seq = cursor.getInt(seqColumnIndex);
+ if (seq != 0) {
+ continue;
+ }
+ final int id = cursor.getInt(idColumnIndex);
+ List myColumns = new ArrayList<>();
+ List refColumns = new ArrayList<>();
+ for (ForeignKeyWithSequence key : ordered) {
+ if (key.mId == id) {
+ myColumns.add(key.mFrom);
+ refColumns.add(key.mTo);
+ }
+ }
+ foreignKeys.add(new ForeignKey(
+ cursor.getString(tableColumnIndex),
+ cursor.getString(onDeleteColumnIndex),
+ cursor.getString(onUpdateColumnIndex),
+ myColumns,
+ refColumns
+ ));
+ }
+ } finally {
+ cursor.close();
+ }
+ return foreignKeys;
+ }
+
+ private static List readForeignKeyFieldMappings(Cursor cursor) {
+ final int idColumnIndex = cursor.getColumnIndex("id");
+ final int seqColumnIndex = cursor.getColumnIndex("seq");
+ final int fromColumnIndex = cursor.getColumnIndex("from");
+ final int toColumnIndex = cursor.getColumnIndex("to");
+ final int count = cursor.getCount();
+ List result = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ cursor.moveToPosition(i);
+ result.add(new ForeignKeyWithSequence(
+ cursor.getInt(idColumnIndex),
+ cursor.getInt(seqColumnIndex),
+ cursor.getString(fromColumnIndex),
+ cursor.getString(toColumnIndex)
+ ));
+ }
+ Collections.sort(result);
+ return result;
+ }
+
+ private static Map readColumns(SupportSQLiteDatabase database,
+ String tableName) {
+ Cursor cursor = database
+ .query("PRAGMA table_info(`" + tableName + "`)");
+ //noinspection TryFinallyCanBeTryWithResources
+ Map columns = new HashMap<>();
+ try {
+ if (cursor.getColumnCount() > 0) {
+ int nameIndex = cursor.getColumnIndex("name");
+ int typeIndex = cursor.getColumnIndex("type");
+ int notNullIndex = cursor.getColumnIndex("notnull");
+ int pkIndex = cursor.getColumnIndex("pk");
+ int defaultValueIndex = cursor.getColumnIndex("dflt_value");
+
+ while (cursor.moveToNext()) {
+ final String name = cursor.getString(nameIndex);
+ final String type = cursor.getString(typeIndex);
+ final boolean notNull = 0 != cursor.getInt(notNullIndex);
+ final int primaryKeyPosition = cursor.getInt(pkIndex);
+ final String defaultValue = cursor.getString(defaultValueIndex);
+ columns.put(name,
+ new Column(name, type, notNull, primaryKeyPosition, defaultValue,
+ CREATED_FROM_DATABASE));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return columns;
+ }
+
+ /**
+ * @return null if we cannot read the indices due to older sqlite implementations.
+ */
+ @Nullable
+ private static Set readIndices(SupportSQLiteDatabase database, String tableName) {
+ Cursor cursor = database.query("PRAGMA index_list(`" + tableName + "`)");
+ try {
+ final int nameColumnIndex = cursor.getColumnIndex("name");
+ final int originColumnIndex = cursor.getColumnIndex("origin");
+ final int uniqueIndex = cursor.getColumnIndex("unique");
+ if (nameColumnIndex == -1 || originColumnIndex == -1 || uniqueIndex == -1) {
+ // we cannot read them so better not validate any index.
+ return null;
+ }
+ HashSet indices = new HashSet<>();
+ while (cursor.moveToNext()) {
+ String origin = cursor.getString(originColumnIndex);
+ if (!"c".equals(origin)) {
+ // Ignore auto-created indices
+ continue;
+ }
+ String name = cursor.getString(nameColumnIndex);
+ boolean unique = cursor.getInt(uniqueIndex) == 1;
+ Index index = readIndex(database, name, unique);
+ if (index == null) {
+ // we cannot read it properly so better not read it
+ return null;
+ }
+ indices.add(index);
+ }
+ return indices;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * @return null if we cannot read the index due to older sqlite implementations.
+ */
+ @Nullable
+ private static Index readIndex(SupportSQLiteDatabase database, String name, boolean unique) {
+ Cursor cursor = database.query("PRAGMA index_xinfo(`" + name + "`)");
+ try {
+ final int seqnoColumnIndex = cursor.getColumnIndex("seqno");
+ final int cidColumnIndex = cursor.getColumnIndex("cid");
+ final int nameColumnIndex = cursor.getColumnIndex("name");
+ if (seqnoColumnIndex == -1 || cidColumnIndex == -1 || nameColumnIndex == -1) {
+ // we cannot read them so better not validate any index.
+ return null;
+ }
+ final TreeMap results = new TreeMap<>();
+
+ while (cursor.moveToNext()) {
+ int cid = cursor.getInt(cidColumnIndex);
+ if (cid < 0) {
+ // Ignore SQLite row ID
+ continue;
+ }
+ int seq = cursor.getInt(seqnoColumnIndex);
+ String columnName = cursor.getString(nameColumnIndex);
+ results.put(seq, columnName);
+ }
+ final List columns = new ArrayList<>(results.size());
+ columns.addAll(results.values());
+ return new Index(name, unique, columns);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Holds the information about a database column.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static class Column {
+ /**
+ * The column name.
+ */
+ public final String name;
+ /**
+ * The column type affinity.
+ */
+ public final String type;
+ /**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ *
+ * This is the value Room uses for equality check.
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ public final int affinity;
+ /**
+ * Whether or not the column can be NULL.
+ */
+ public final boolean notNull;
+ /**
+ * The position of the column in the list of primary keys, 0 if the column is not part
+ * of the primary key.
+ *
+ * This information is only available in API 20+.
+ * (SQLite version 3.7.16.2)
+ * On older platforms, it will be 1 if the column is part of the primary key and 0
+ * otherwise.
+ *
+ * The {@link #equals(Object)} implementation handles this inconsistency based on
+ * API levels os if you are using a custom SQLite deployment, it may return false
+ * positives.
+ */
+ public final int primaryKeyPosition;
+ /**
+ * The default value of this column.
+ */
+ public final String defaultValue;
+
+ @CreatedFrom
+ private final int mCreatedFrom;
+
+ /**
+ * @deprecated Use {@link Column#Column(String, String, boolean, int, String, int)} instead.
+ */
+ @Deprecated
+ public Column(String name, String type, boolean notNull, int primaryKeyPosition) {
+ this(name, type, notNull, primaryKeyPosition, null, CREATED_FROM_UNKNOWN);
+ }
+
+ // if you change this constructor, you must change TableInfoWriter.kt
+ public Column(String name, String type, boolean notNull, int primaryKeyPosition,
+ String defaultValue, @CreatedFrom int createdFrom) {
+ this.name = name;
+ this.type = type;
+ this.notNull = notNull;
+ this.primaryKeyPosition = primaryKeyPosition;
+ this.affinity = findAffinity(type);
+ this.defaultValue = defaultValue;
+ this.mCreatedFrom = createdFrom;
+ }
+
+ /**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ private static int findAffinity(@Nullable String type) {
+ if (type == null) {
+ return ColumnInfo.BLOB;
+ }
+ String uppercaseType = type.toUpperCase(Locale.US);
+ if (uppercaseType.contains("INT")) {
+ return ColumnInfo.INTEGER;
+ }
+ if (uppercaseType.contains("CHAR")
+ || uppercaseType.contains("CLOB")
+ || uppercaseType.contains("TEXT")) {
+ return ColumnInfo.TEXT;
+ }
+ if (uppercaseType.contains("BLOB")) {
+ return ColumnInfo.BLOB;
+ }
+ if (uppercaseType.contains("REAL")
+ || uppercaseType.contains("FLOA")
+ || uppercaseType.contains("DOUB")) {
+ return ColumnInfo.REAL;
+ }
+ // sqlite returns NUMERIC here but it is like a catch all. We already
+ // have UNDEFINED so it is better to use UNDEFINED for consistency.
+ return ColumnInfo.UNDEFINED;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Column column = (Column) o;
+ if (Build.VERSION.SDK_INT >= 20) {
+ if (primaryKeyPosition != column.primaryKeyPosition) return false;
+ } else {
+ if (isPrimaryKey() != column.isPrimaryKey()) return false;
+ }
+
+ if (!name.equals(column.name)) return false;
+ //noinspection SimplifiableIfStatement
+ if (notNull != column.notNull) return false;
+
+ // Only validate default value if it was defined in an entity, i.e. if the info
+ // from the compiler itself has it. b/136019383
+ if (mCreatedFrom == CREATED_FROM_ENTITY
+ && column.mCreatedFrom == CREATED_FROM_DATABASE
+ && (defaultValue != null && !defaultValue.equals(column.defaultValue))) {
+ return false;
+ } else if (mCreatedFrom == CREATED_FROM_DATABASE
+ && column.mCreatedFrom == CREATED_FROM_ENTITY
+ && (column.defaultValue != null && !column.defaultValue.equals(defaultValue))) {
+ return false;
+ } else if (mCreatedFrom != CREATED_FROM_UNKNOWN
+ && mCreatedFrom == column.mCreatedFrom
+ && (defaultValue != null ? !defaultValue.equals(column.defaultValue)
+ : column.defaultValue != null)) {
+ return false;
+ }
+
+ return affinity == column.affinity;
+ }
+
+ /**
+ * Returns whether this column is part of the primary key or not.
+ *
+ * @return True if this column is part of the primary key, false otherwise.
+ */
+ public boolean isPrimaryKey() {
+ return primaryKeyPosition > 0;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + affinity;
+ result = 31 * result + (notNull ? 1231 : 1237);
+ result = 31 * result + primaryKeyPosition;
+ // Default value is not part of the hashcode since we conditionally check it for
+ // equality which would break the equals + hashcode contract.
+ // result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Column{"
+ + "name='" + name + '\''
+ + ", type='" + type + '\''
+ + ", affinity='" + affinity + '\''
+ + ", notNull=" + notNull
+ + ", primaryKeyPosition=" + primaryKeyPosition
+ + ", defaultValue='" + defaultValue + '\''
+ + '}';
+ }
+ }
+
+ /**
+ * Holds the information about an SQLite foreign key
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static class ForeignKey {
+ @NonNull
+ public final String referenceTable;
+ @NonNull
+ public final String onDelete;
+ @NonNull
+ public final String onUpdate;
+ @NonNull
+ public final List columnNames;
+ @NonNull
+ public final List referenceColumnNames;
+
+ public ForeignKey(@NonNull String referenceTable, @NonNull String onDelete,
+ @NonNull String onUpdate,
+ @NonNull List columnNames, @NonNull List referenceColumnNames) {
+ this.referenceTable = referenceTable;
+ this.onDelete = onDelete;
+ this.onUpdate = onUpdate;
+ this.columnNames = Collections.unmodifiableList(columnNames);
+ this.referenceColumnNames = Collections.unmodifiableList(referenceColumnNames);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ForeignKey that = (ForeignKey) o;
+
+ if (!referenceTable.equals(that.referenceTable)) return false;
+ if (!onDelete.equals(that.onDelete)) return false;
+ if (!onUpdate.equals(that.onUpdate)) return false;
+ //noinspection SimplifiableIfStatement
+ if (!columnNames.equals(that.columnNames)) return false;
+ return referenceColumnNames.equals(that.referenceColumnNames);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = referenceTable.hashCode();
+ result = 31 * result + onDelete.hashCode();
+ result = 31 * result + onUpdate.hashCode();
+ result = 31 * result + columnNames.hashCode();
+ result = 31 * result + referenceColumnNames.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ForeignKey{"
+ + "referenceTable='" + referenceTable + '\''
+ + ", onDelete='" + onDelete + '\''
+ + ", onUpdate='" + onUpdate + '\''
+ + ", columnNames=" + columnNames
+ + ", referenceColumnNames=" + referenceColumnNames
+ + '}';
+ }
+ }
+
+ /**
+ * Temporary data holder for a foreign key row in the pragma result. We need this to ensure
+ * sorting in the generated foreign key object.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ static class ForeignKeyWithSequence implements Comparable {
+ final int mId;
+ final int mSequence;
+ final String mFrom;
+ final String mTo;
+
+ ForeignKeyWithSequence(int id, int sequence, String from, String to) {
+ mId = id;
+ mSequence = sequence;
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ public int compareTo(@NonNull ForeignKeyWithSequence o) {
+ final int idCmp = mId - o.mId;
+ if (idCmp == 0) {
+ return mSequence - o.mSequence;
+ } else {
+ return idCmp;
+ }
+ }
+ }
+
+ /**
+ * Holds the information about an SQLite index
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+ public static class Index {
+ // should match the value in Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
+ public final String name;
+ public final boolean unique;
+ public final List columns;
+
+ public Index(String name, boolean unique, List columns) {
+ this.name = name;
+ this.unique = unique;
+ this.columns = columns;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Index index = (Index) o;
+ if (unique != index.unique) {
+ return false;
+ }
+ if (!columns.equals(index.columns)) {
+ return false;
+ }
+ if (name.startsWith(Index.DEFAULT_PREFIX)) {
+ return index.name.startsWith(Index.DEFAULT_PREFIX);
+ } else {
+ return name.equals(index.name);
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ int result;
+ if (name.startsWith(DEFAULT_PREFIX)) {
+ result = DEFAULT_PREFIX.hashCode();
+ } else {
+ result = name.hashCode();
+ }
+ result = 31 * result + (unique ? 1 : 0);
+ result = 31 * result + columns.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Index{"
+ + "name='" + name + '\''
+ + ", unique=" + unique
+ + ", columns=" + columns
+ + '}';
+ }
+ }
+}
diff --git a/app/src/main/java/androidx/room/util/ViewInfo.java b/app/src/main/java/androidx/room/util/ViewInfo.java
new file mode 100644
index 0000000000..5e4b9b2bbe
--- /dev/null
+++ b/app/src/main/java/androidx/room/util/ViewInfo.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2018 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.util;
+
+import android.database.Cursor;
+
+import androidx.annotation.RestrictTo;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+/**
+ * A data class that holds the information about a view.
+ *
+ * This derives information from sqlite_master.
+ *
+ * Even though SQLite column names are case insensitive, this class uses case sensitive matching.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+public class ViewInfo {
+
+ /**
+ * The view name
+ */
+ public final String name;
+
+ /**
+ * The SQL of CREATE VIEW.
+ */
+ public final String sql;
+
+ public ViewInfo(String name, String sql) {
+ this.name = name;
+ this.sql = sql;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ViewInfo viewInfo = (ViewInfo) o;
+ return (name != null ? name.equals(viewInfo.name) : viewInfo.name == null)
+ && (sql != null ? sql.equals(viewInfo.sql) : viewInfo.sql == null);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = name != null ? name.hashCode() : 0;
+ result = 31 * result + (sql != null ? sql.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ViewInfo{"
+ + "name='" + name + '\''
+ + ", sql='" + sql + '\''
+ + '}';
+ }
+
+ /**
+ * Reads the view information from the given database.
+ *
+ * @param database The database to read the information from.
+ * @param viewName The view name.
+ * @return A ViewInfo containing the schema information for the provided view name.
+ */
+ @SuppressWarnings("SameParameterValue")
+ public static ViewInfo read(SupportSQLiteDatabase database, String viewName) {
+ Cursor cursor = database.query("SELECT name, sql FROM sqlite_master "
+ + "WHERE type = 'view' AND name = '" + viewName + "'");
+ //noinspection TryFinallyCanBeTryWithResources
+ try {
+ if (cursor.moveToFirst()) {
+ return new ViewInfo(cursor.getString(0), cursor.getString(1));
+ } else {
+ return new ViewInfo(viewName, null);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+}