/*
* 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 extends AutoMigrationSpec> 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