/* * 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.content.Intent; import android.database.Cursor; import android.os.Build; import android.os.CancellationSignal; import android.os.Looper; import android.util.Log; import androidx.annotation.CallSuper; import androidx.annotation.IntRange; 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.AutoMigrationSpec; import androidx.room.migration.Migration; import androidx.room.util.SneakyThrow; import androidx.sqlite.db.SimpleSQLiteQuery; import androidx.sqlite.db.SupportSQLiteCompat; 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.io.InputStream; import java.util.ArrayList; import java.util.BitSet; 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.Executor; import java.util.concurrent.TimeUnit; 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; /** * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @Nullable @Deprecated protected List mCallbacks; /** * A map of auto migration spec classes to their provided instance. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) @NonNull protected Map, AutoMigrationSpec> mAutoMigrationSpecs; private final ReentrantReadWriteLock mCloseLock = new ReentrantReadWriteLock(); @Nullable private AutoCloser mAutoCloser; /** * {@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 = Collections.synchronizedMap(new HashMap<>()); /** * Gets the map for storing extension properties of Kotlin type. * * @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) Map getBackingFieldMap() { return mBackingFieldMap; } // Updated later to an unmodifiable map when init is called. private final Map, Object> mTypeConverters; /** * Gets the instance of the given Type Converter. * * @param klass The Type Converter class. * @param The type of the expected Type Converter subclass. * @return An instance of T if it is provided in the builder. */ @SuppressWarnings("unchecked") @Nullable public T getTypeConverter(@NonNull Class klass) { return (T) mTypeConverters.get(klass); } /** * 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(); mTypeConverters = new HashMap<>(); mAutoMigrationSpecs = new HashMap<>(); } /** * Called by {@link Room} when it is initialized. * * @param configuration The database configuration. */ @CallSuper public void init(@NonNull DatabaseConfiguration configuration) { mOpenHelper = createOpenHelper(configuration); Set> requiredAutoMigrationSpecs = getRequiredAutoMigrationSpecs(); BitSet usedSpecs = new BitSet(); for (Class spec : requiredAutoMigrationSpecs) { int foundIndex = -1; for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; providedIndex >= 0; providedIndex-- ) { Object provided = configuration.autoMigrationSpecs.get(providedIndex); if (spec.isAssignableFrom(provided.getClass())) { foundIndex = providedIndex; usedSpecs.set(foundIndex); break; } } if (foundIndex < 0) { throw new IllegalArgumentException( "A required auto migration spec (" + spec.getCanonicalName() + ") is missing in the database configuration."); } mAutoMigrationSpecs.put(spec, configuration.autoMigrationSpecs.get(foundIndex)); } for (int providedIndex = configuration.autoMigrationSpecs.size() - 1; providedIndex >= 0; providedIndex--) { if (!usedSpecs.get(providedIndex)) { throw new IllegalArgumentException("Unexpected auto migration specs found. " + "Annotate AutoMigrationSpec implementation with " + "@ProvidedAutoMigrationSpec annotation or remove this spec from the " + "builder."); } } List autoMigrations = getAutoMigrations(mAutoMigrationSpecs); for (Migration autoMigration : autoMigrations) { boolean migrationExists = configuration.migrationContainer.getMigrations() .containsKey(autoMigration.startVersion); if (!migrationExists) { configuration.migrationContainer.addMigrations(autoMigration); } } // Configure SqliteCopyOpenHelper if it is available: SQLiteCopyOpenHelper copyOpenHelper = unwrapOpenHelper(SQLiteCopyOpenHelper.class, mOpenHelper); if (copyOpenHelper != null) { copyOpenHelper.setDatabaseConfiguration(configuration); } AutoClosingRoomOpenHelper autoClosingRoomOpenHelper = unwrapOpenHelper(AutoClosingRoomOpenHelper.class, mOpenHelper); if (autoClosingRoomOpenHelper != null) { mAutoCloser = autoClosingRoomOpenHelper.getAutoCloser(); mInvalidationTracker.setAutoCloser(mAutoCloser); } 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.multiInstanceInvalidationServiceIntent != null) { mInvalidationTracker.startMultiInstanceInvalidation(configuration.context, configuration.name, configuration.multiInstanceInvalidationServiceIntent); } Map, List>> requiredFactories = getRequiredTypeConverters(); // indices for each converter on whether it is used or not so that we can throw an exception // if developer provides an unused converter. It is not necessarily an error but likely // to be because why would developer add a converter if it won't be used? BitSet used = new BitSet(); for (Map.Entry, List>> entry : requiredFactories.entrySet()) { Class daoName = entry.getKey(); for (Class converter : entry.getValue()) { int foundIndex = -1; // traverse provided converters in reverse so that newer one overrides for (int providedIndex = configuration.typeConverters.size() - 1; providedIndex >= 0; providedIndex--) { Object provided = configuration.typeConverters.get(providedIndex); if (converter.isAssignableFrom(provided.getClass())) { foundIndex = providedIndex; used.set(foundIndex); break; } } if (foundIndex < 0) { throw new IllegalArgumentException( "A required type converter (" + converter + ") for" + " " + daoName.getCanonicalName() + " is missing in the database configuration."); } mTypeConverters.put(converter, configuration.typeConverters.get(foundIndex)); } } // now, make sure all provided factories are used for (int providedIndex = configuration.typeConverters.size() - 1; providedIndex >= 0; providedIndex--) { if (!used.get(providedIndex)) { Object converter = configuration.typeConverters.get(providedIndex); throw new IllegalArgumentException("Unexpected type converter " + converter + ". " + "Annotate TypeConverter class with @ProvidedTypeConverter annotation " + "or remove this converter from the builder."); } } } /** * Returns a list of {@link Migration} of a database that have been automatically generated. * * @return A list of migration instances each of which is a generated autoMigration * @param autoMigrationSpecs * * @hide */ @NonNull @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public List getAutoMigrations( @NonNull Map, AutoMigrationSpec> autoMigrationSpecs ) { return Collections.emptyList(); } /** * Unwraps (delegating) open helpers until it finds clazz, otherwise returns null. * * @param clazz the open helper type to search for * @param openHelper the open helper to search through * @param the type of clazz * @return the instance of clazz, otherwise null */ @Nullable @SuppressWarnings("unchecked") private T unwrapOpenHelper(Class clazz, SupportSQLiteOpenHelper openHelper) { if (clazz.isInstance(openHelper)) { return (T) openHelper; } if (openHelper instanceof DelegatingOpenHelper) { return unwrapOpenHelper(clazz, ((DelegatingOpenHelper) openHelper).getDelegate()); } return null; } /** * 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(); /** * Returns a Map of String -> List<Class> where each entry has the `key` as the DAO name * and `value` as the list of type converter classes that are necessary for the database to * function. *

* This is implemented by the generated code. * * @return Creates a map that will include all required type converters for this database. */ @NonNull @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) protected Map, List>> getRequiredTypeConverters() { return Collections.emptyMap(); } /** * Returns a Set of required AutoMigrationSpec classes. *

* This is implemented by the generated code. * * @return Creates a set that will include all required auto migration specs for this database. * * @hide */ @NonNull @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) public Set> getRequiredAutoMigrationSpecs() { return Collections.emptySet(); } /** * 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() { // We need to special case for the auto closing database because mDatabase is the // underlying database and not the wrapped database. if (mAutoCloser != null) { return mAutoCloser.isActive(); } 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(); closeLock.lock(); try { 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. */ @NonNull public Cursor query(@NonNull 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. */ @NonNull public Cursor query(@NonNull SupportSQLiteQuery query) { return query(query, null); } /** * Wrapper for {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}. * * @param query The Query which includes the SQL and a bind callback for bind arguments. * @param signal The cancellation signal to be attached to the query. * @return Result of the query. */ @NonNull public Cursor query(@NonNull SupportSQLiteQuery query, @Nullable CancellationSignal signal) { assertNotMainThread(); assertNotSuspendingTransaction(); if (signal != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return mOpenHelper.getWritableDatabase().query(query, signal); } else { 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(); if (mAutoCloser == null) { internalBeginTransaction(); } else { mAutoCloser.executeRefCountingFunction(db -> { internalBeginTransaction(); return null; }); } } private void internalBeginTransaction() { assertNotMainThread(); SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase(); mInvalidationTracker.syncTriggers(database); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && database.isWriteAheadLoggingEnabled()) { database.beginTransactionNonExclusive(); } else { database.beginTransaction(); } } /** * Wrapper for {@link SupportSQLiteDatabase#endTransaction()}. * * @deprecated Use {@link #runInTransaction(Runnable)} */ @Deprecated public void endTransaction() { if (mAutoCloser == null) { internalEndTransaction(); } else { mAutoCloser.executeRefCountingFunction(db -> { internalEndTransaction(); return null; }); } } private void internalEndTransaction() { 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 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}. */ 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 SupportSQLiteCompat.Api19Impl.isLowRamDevice(activityManager); } 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; private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback; private QueryCallback mQueryCallback; private Executor mQueryCallbackExecutor; private List mTypeConverters; private List mAutoMigrationSpecs; /** 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 Intent mMultiInstanceInvalidationIntent; private boolean mRequireMigration; private boolean mAllowDestructiveMigrationOnDowngrade; private long mAutoCloseTimeout = -1L; private TimeUnit mAutoCloseTimeUnit; /** * 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; private Callable mCopyFromInputStream; 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 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. * @param callback The pre-packaged callback. * * @return This {@link Builder} instance. */ @NonNull @SuppressLint("BuilderSetStyle") // To keep naming consistency. public Builder createFromAsset( @NonNull String databaseFilePath, @NonNull PrepackagedDatabaseCallback callback) { mPrepackagedDatabaseCallback = callback; 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. *

* The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator * that the pre-packaged database was successfully opened by Room and can be cleaned up. *

* 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; } /** * 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. *

* The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator * that the pre-packaged database was successfully opened by Room and can be cleaned up. *

* This method is not supported for an in memory database {@link Builder}. * * @param databaseFile The database file. * @param callback The pre-packaged callback. * * @return This {@link Builder} instance. */ @NonNull @SuppressLint({"BuilderSetStyle", "StreamFiles"}) // To keep naming consistency. public Builder createFromFile( @NonNull File databaseFile, @NonNull PrepackagedDatabaseCallback callback) { mPrepackagedDatabaseCallback = callback; mCopyFromFile = databaseFile; return this; } /** * Configures Room to create and open the database using a pre-packaged database via an * {@link InputStream}. *

* This is useful for processing compressed database files. Room does not open the * pre-packaged database, instead it copies it into the internal app database folder, and * then open it. The {@link InputStream} will be closed once Room is done consuming it. *

* 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. *

* The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator * that the pre-packaged database was successfully opened by Room and can be cleaned up. *

* This method is not supported for an in memory database {@link Builder}. * * @param inputStreamCallable A callable that returns an InputStream from which to copy * the database. The callable will be invoked in a thread from * the Executor set via {@link #setQueryExecutor(Executor)}. The * callable is only invoked if Room needs to create and open the * database from the pre-package database, usually the first time * it is created or during a destructive migration. * * @return This {@link Builder} instance. */ @NonNull @SuppressLint("BuilderSetStyle") // To keep naming consistency. public Builder createFromInputStream( @NonNull Callable inputStreamCallable) { mCopyFromInputStream = inputStreamCallable; return this; } /** * Configures Room to create and open the database using a pre-packaged database via an * {@link InputStream}. *

* This is useful for processing compressed database files. Room does not open the * pre-packaged database, instead it copies it into the internal app database folder, and * then open it. The {@link InputStream} will be closed once Room is done consuming it. *

* 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. *

* The {@link Callback#onOpen(SupportSQLiteDatabase)} method can be used as an indicator * that the pre-packaged database was successfully opened by Room and can be cleaned up. *

* This method is not supported for an in memory database {@link Builder}. * * @param inputStreamCallable A callable that returns an InputStream from which to copy * the database. The callable will be invoked in a thread from * the Executor set via {@link #setQueryExecutor(Executor)}. The * callable is only invoked if Room needs to create and open the * database from the pre-package database, usually the first time * it is created or during a destructive migration. * @param callback The pre-packaged callback. * * @return This {@link Builder} instance. */ @NonNull @SuppressLint({"BuilderSetStyle", "LambdaLast"}) // To keep naming consistency. public Builder createFromInputStream( @NonNull Callable inputStreamCallable, @NonNull PrepackagedDatabaseCallback callback) { mPrepackagedDatabaseCallback = callback; mCopyFromInputStream = inputStreamCallable; 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; } /** * Adds an auto migration spec to the builder. * * @param autoMigrationSpec The auto migration object that is annotated with * {@link AutoMigrationSpec} and is declared in an {@link AutoMigration} annotation. * @return This {@link Builder} instance. */ @NonNull @SuppressWarnings("MissingGetterMatchingBuilder") public Builder addAutoMigrationSpec(@NonNull AutoMigrationSpec autoMigrationSpec) { if (mAutoMigrationSpecs == null) { mAutoMigrationSpecs = new ArrayList<>(); } mAutoMigrationSpecs.add(autoMigrationSpec); 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() { mMultiInstanceInvalidationIntent = mName != null ? new Intent(mContext, MultiInstanceInvalidationService.class) : null; 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 and need to point to the same * {@link MultiInstanceInvalidationService}. *

* 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. * @param invalidationServiceIntent Intent to bind to the * {@link MultiInstanceInvalidationService}. */ @SuppressWarnings("MissingGetterMatchingBuilder") @NonNull @ExperimentalRoomApi public Builder setMultiInstanceInvalidationServiceIntent( @NonNull Intent invalidationServiceIntent) { mMultiInstanceInvalidationIntent = mName != null ? invalidationServiceIntent : 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; } /** * Sets a {@link QueryCallback} to be invoked when queries are executed. *

* The callback is invoked whenever a query is executed, note that adding this callback * has a small cost and should be avoided in production builds unless needed. *

* A use case for providing a callback is to allow logging executed queries. When the * callback implementation logs then it is recommended to use an immediate executor. * * @param queryCallback The query callback. * @param executor The executor on which the query callback will be invoked. */ @SuppressWarnings("MissingGetterMatchingBuilder") @NonNull public Builder setQueryCallback(@NonNull QueryCallback queryCallback, @NonNull Executor executor) { mQueryCallback = queryCallback; mQueryCallbackExecutor = executor; return this; } /** * Adds a type converter instance to this database. * * @param typeConverter The converter. It must be an instance of a class annotated with * {@link ProvidedTypeConverter} otherwise Room will throw an exception. * @return This {@link Builder} instance. */ @NonNull public Builder addTypeConverter(@NonNull Object typeConverter) { if (mTypeConverters == null) { mTypeConverters = new ArrayList<>(); } mTypeConverters.add(typeConverter); return this; } /** * Enables auto-closing for the database to free up unused resources. The underlying * database will be closed after it's last use after the specified {@code * autoCloseTimeout} has elapsed since its last usage. The database will be automatically * re-opened the next time it is accessed. *

* Auto-closing is not compatible with in-memory databases since the data will be lost * when the database is auto-closed. *

* Also, temp tables and temp triggers will be cleared each time the database is * auto-closed. If you need to use them, please include them in your * {@link RoomDatabase.Callback#onOpen callback}. *

* All configuration should happen in your {@link RoomDatabase.Callback#onOpen} * callback so it is re-applied every time the database is re-opened. Note that the * {@link RoomDatabase.Callback#onOpen} will be called every time the database is re-opened. *

* The auto-closing database operation runs on the query executor. *

* The database will not be reopened if the RoomDatabase or the * SupportSqliteOpenHelper is closed manually (by calling * {@link RoomDatabase#close()} or {@link SupportSQLiteOpenHelper#close()}. If the * database is closed manually, you must create a new database using * {@link RoomDatabase.Builder#build()}. * * @param autoCloseTimeout the amount of time after the last usage before closing the * database. Must greater or equal to zero. * @param autoCloseTimeUnit the timeunit for autoCloseTimeout. * @return This {@link Builder} instance */ @NonNull @SuppressWarnings("MissingGetterMatchingBuilder") @ExperimentalRoomApi // When experimental is removed, add these parameters to // DatabaseConfiguration public Builder setAutoCloseTimeout( @IntRange(from = 0) long autoCloseTimeout, @NonNull TimeUnit autoCloseTimeUnit) { if (autoCloseTimeout < 0) { throw new IllegalArgumentException("autoCloseTimeout must be >= 0"); } mAutoCloseTimeout = autoCloseTimeout; mAutoCloseTimeUnit = autoCloseTimeUnit; 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); } } } SupportSQLiteOpenHelper.Factory factory; AutoCloser autoCloser = null; if (mFactory == null) { factory = new FrameworkSQLiteOpenHelperFactory(); } else { factory = mFactory; } if (mAutoCloseTimeout > 0) { if (mName == null) { throw new IllegalArgumentException("Cannot create auto-closing database for " + "an in-memory database."); } autoCloser = new AutoCloser(mAutoCloseTimeout, mAutoCloseTimeUnit, mTransactionExecutor); factory = new AutoClosingRoomOpenHelperFactory(factory, autoCloser); } if (mCopyFromAssetPath != null || mCopyFromFile != null || mCopyFromInputStream != null) { if (mName == null) { throw new IllegalArgumentException("Cannot create from asset or file for an " + "in-memory database."); } final int copyConfigurations = (mCopyFromAssetPath == null ? 0 : 1) + (mCopyFromFile == null ? 0 : 1) + (mCopyFromInputStream == null ? 0 : 1); if (copyConfigurations != 1) { throw new IllegalArgumentException("More than one of createFromAsset(), " + "createFromInputStream(), and createFromFile() were called on this " + "Builder, but the database can only be created using one of the " + "three configurations."); } factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile, mCopyFromInputStream, factory); } if (mQueryCallback != null) { factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback, mQueryCallbackExecutor); } DatabaseConfiguration configuration = new DatabaseConfiguration( mContext, mName, factory, mMigrationContainer, mCallbacks, mAllowMainThreadQueries, mJournalMode.resolve(mContext), mQueryExecutor, mTransactionExecutor, mMultiInstanceInvalidationIntent, mRequireMigration, mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom, mCopyFromAssetPath, mCopyFromFile, mCopyFromInputStream, mPrepackagedDatabaseCallback, mTypeConverters, mAutoMigrationSpecs); 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); } } /** * 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 List 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); } /** * Returns the map of available migrations where the key is the start version of the * migration, and the value is a map of (end version -> Migration). * * @return Map of migrations keyed by the start version */ @NonNull public Map> getMigrations() { return Collections.unmodifiableMap(mMigrations); } /** * 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){ } } /** * Callback for {@link Builder#createFromAsset(String)}, {@link Builder#createFromFile(File)} * and {@link Builder#createFromInputStream(Callable)} *

* This callback will be invoked after the pre-package DB is copied but before Room had * a chance to open it and therefore before the {@link RoomDatabase.Callback} methods are * invoked. This callback can be useful for updating the pre-package DB schema to satisfy * Room's schema validation. */ public abstract static class PrepackagedDatabaseCallback { /** * Called when the pre-packaged database has been copied. * * @param db The database. */ public void onOpenPrepackagedDatabase(@NonNull SupportSQLiteDatabase db) { } } /** * Callback interface for when SQLite queries are executed. * * @see RoomDatabase.Builder#setQueryCallback */ public interface QueryCallback { /** * Called when a SQL query is executed. * * @param sqlQuery The SQLite query statement. * @param bindArgs Arguments of the query if available, empty list otherwise. */ void onQuery(@NonNull String sqlQuery, @NonNull List bindArgs); } }