Updated ROOM

pull/194/head
M66B 5 years ago
parent 1a359431c6
commit 70a2af170f

@ -235,6 +235,7 @@ configurations.all {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
def annotation_version = "1.1.0-beta01"
def core_version = "1.5.0-alpha05"
def appcompat_version = "1.3.0-beta01"
def activity_version = "1.2.0-rc01"
@ -251,7 +252,7 @@ dependencies {
def lifecycle_version = "2.3.0-rc01"
def lifecycle_extensions_version = "2.2.0"
def sqlite_version = "2.1.0"
def room_version = "2.2.6" // 2.3.0-beta01
def room_version = "2.3.0-beta01"
def paging_version = "2.1.2" // 3.0.0-alpha11
def preference_version = "1.1.1"
def work_version = "2.5.0"
@ -278,6 +279,7 @@ dependencies {
def apache_poi = "3.17"
// https://developer.android.com/jetpack/androidx/releases/
implementation "androidx.annotation:annotation-experimental:$annotation_version"
// https://mvnrepository.com/artifact/androidx.core/core
implementation "androidx.core:core:$core_version"

@ -0,0 +1,311 @@
/*
* Copyright 2020 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.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* AutoCloser is responsible for automatically opening (using
* delegateOpenHelper) and closing (on a timer started when there are no remaining references) a
* SupportSqliteDatabase.
*
* It is important to ensure that the ref count is incremented when using a returned database.
*/
final class AutoCloser {
@Nullable
private SupportSQLiteOpenHelper mDelegateOpenHelper = null;
@NonNull
private final Handler mHandler = new Handler(Looper.getMainLooper());
// Package private for access from mAutoCloser
@Nullable
Runnable mOnAutoCloseCallback = null;
// Package private for access from mAutoCloser
@NonNull
final Object mLock = new Object();
// Package private for access from mAutoCloser
final long mAutoCloseTimeoutInMs;
// Package private for access from mExecuteAutoCloser
@NonNull
final Executor mExecutor;
// Package private for access from mAutoCloser
@GuardedBy("mLock")
int mRefCount = 0;
// Package private for access from mAutoCloser
@GuardedBy("mLock")
long mLastDecrementRefCountTimeStamp = SystemClock.uptimeMillis();
// The unwrapped SupportSqliteDatabase
// Package private for access from mAutoCloser
@GuardedBy("mLock")
@Nullable
SupportSQLiteDatabase mDelegateDatabase;
private boolean mManuallyClosed = false;
private final Runnable mExecuteAutoCloser = new Runnable() {
@Override
public void run() {
mExecutor.execute(mAutoCloser);
}
};
// Package private for access from mExecuteAutoCloser
@NonNull
final Runnable mAutoCloser = new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (SystemClock.uptimeMillis() - mLastDecrementRefCountTimeStamp
< mAutoCloseTimeoutInMs) {
// An increment + decrement beat us to closing the db. We
// will not close the database, and there should be at least
// one more auto-close scheduled.
return;
}
if (mRefCount != 0) {
// An increment beat us to closing the db. We don't close the
// db, and another closer will be scheduled once the ref
// count is decremented.
return;
}
if (mOnAutoCloseCallback != null) {
mOnAutoCloseCallback.run();
} else {
throw new IllegalStateException("mOnAutoCloseCallback is null but it should"
+ " have been set before use. Please file a bug "
+ "against Room at: https://issuetracker.google"
+ ".com/issues/new?component=413107&template=1096568");
}
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
try {
mDelegateDatabase.close();
} catch (IOException e) {
SneakyThrow.reThrow(e);
}
mDelegateDatabase = null;
}
}
}
};
/**
* Construct an AutoCloser.
*
* @param autoCloseTimeoutAmount time for auto close timer
* @param autoCloseTimeUnit time unit for autoCloseTimeoutAmount
* @param autoCloseExecutor the executor on which the auto close operation will happen
*/
AutoCloser(long autoCloseTimeoutAmount,
@NonNull TimeUnit autoCloseTimeUnit,
@NonNull Executor autoCloseExecutor) {
mAutoCloseTimeoutInMs = autoCloseTimeUnit.toMillis(autoCloseTimeoutAmount);
mExecutor = autoCloseExecutor;
}
/**
* Since we need to construct the AutoCloser in the RoomDatabase.Builder, we need to set the
* delegateOpenHelper after construction.
*
* @param delegateOpenHelper the open helper that is used to create
* new SupportSqliteDatabases
*/
public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
if (mDelegateOpenHelper != null) {
Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. Please file a bug against"
+ " room at: https://issuetracker.google"
+ ".com/issues/new?component=413107&template=1096568");
return;
}
this.mDelegateOpenHelper = delegateOpenHelper;
}
/**
* Execute a ref counting function. The function will receive an unwrapped open database and
* this database will stay open until at least after function returns. If there are no more
* references in use for the db once function completes, an auto close operation will be
* scheduled.
*/
@Nullable
public <V> V executeRefCountingFunction(@NonNull Function<SupportSQLiteDatabase, V> function) {
try {
SupportSQLiteDatabase db = incrementCountAndEnsureDbIsOpen();
return function.apply(db);
} finally {
decrementCountAndScheduleClose();
}
}
/**
* Confirms that autoCloser is no longer running and confirms that mDelegateDatabase is set
* and open. mDelegateDatabase will not be auto closed until
* decrementRefCountAndScheduleClose is called. decrementRefCountAndScheduleClose must be
* called once for each call to incrementCountAndEnsureDbIsOpen.
*
* If this throws an exception, decrementCountAndScheduleClose must still be called!
*
* @return the *unwrapped* SupportSQLiteDatabase.
*/
@NonNull
public SupportSQLiteDatabase incrementCountAndEnsureDbIsOpen() {
//TODO(rohitsat): avoid synchronized(mLock) when possible. We should be able to avoid it
// when refCount is not hitting zero or if there is no auto close scheduled if we use
// Atomics.
synchronized (mLock) {
// If there is a scheduled autoclose operation, we should remove it from the handler.
mHandler.removeCallbacks(mExecuteAutoCloser);
mRefCount++;
if (mManuallyClosed) {
throw new IllegalStateException("Attempting to open already closed database.");
}
if (mDelegateDatabase != null && mDelegateDatabase.isOpen()) {
return mDelegateDatabase;
}
// Get the database while holding `mLock` so no other threads try to create it or
// destroy it.
if (mDelegateOpenHelper != null) {
mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase();
} else {
throw new IllegalStateException("AutoCloser has not been initialized. Please file "
+ "a bug against Room at: "
+ "https://issuetracker.google.com/issues/new?component=413107&template=1096568");
}
return mDelegateDatabase;
}
}
/**
* Decrements the ref count and schedules a close if there are no other references to the db.
* This must only be called after a corresponding incrementCountAndEnsureDbIsOpen call.
*/
public void decrementCountAndScheduleClose() {
//TODO(rohitsat): avoid synchronized(mLock) when possible
synchronized (mLock) {
if (mRefCount <= 0) {
throw new IllegalStateException("ref count is 0 or lower but we're supposed to "
+ "decrement");
}
// decrement refCount
mRefCount--;
// if refcount is zero, schedule close operation
if (mRefCount == 0) {
if (mDelegateDatabase == null) {
// No db to close, this can happen due to exceptions when creating db...
return;
}
mHandler.postDelayed(mExecuteAutoCloser, mAutoCloseTimeoutInMs);
}
}
}
/**
* Returns the underlying database. This does not ensure that the database is open; the
* caller is responsible for ensuring that the database is open and the ref count is non-zero.
*
* This is primarily meant for use cases where we don't want to open the database (isOpen) or
* we know that the database is already open (KeepAliveCursor).
*/
@Nullable // Since the db might be closed
public SupportSQLiteDatabase getDelegateDatabase() {
synchronized (mLock) {
return mDelegateDatabase;
}
}
/**
* Close the database if it is still active.
*
* @throws IOException if an exception is encountered when closing the underlying db.
*/
public void closeDatabaseIfOpen() throws IOException {
synchronized (mLock) {
mManuallyClosed = true;
if (mDelegateDatabase != null) {
mDelegateDatabase.close();
}
mDelegateDatabase = null;
}
}
/**
* The auto closer is still active if the database has not been closed. This means that
* whether or not the underlying database is closed, when active we will re-open it on the
* next access.
*
* @return a boolean indicating whether the auto closer is still active
*/
public boolean isActive() {
return !mManuallyClosed;
}
/**
* Returns the current ref count for this auto closer. This is only visible for testing.
*
* @return current ref count
*/
@VisibleForTesting
public int getRefCountForTest() {
synchronized (mLock) {
return mRefCount;
}
}
/**
* Sets a callback that will be run every time the database is auto-closed. This callback
* needs to be lightweight since it is run while holding a lock.
*
* @param onAutoClose the callback to run
*/
public void setAutoCloseCallback(Runnable onAutoClose) {
mOnAutoCloseCallback = onAutoClose;
}
}

@ -0,0 +1,875 @@
/*
* Copyright (C) 2020 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.ContentResolver;
import android.content.ContentValues;
import android.database.CharArrayBuffer;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.database.SQLException;
import android.database.sqlite.SQLiteTransactionListener;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.arch.core.util.Function;
import androidx.room.util.SneakyThrow;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* A SupportSQLiteOpenHelper that has autoclose enabled for database connections.
*/
final class AutoClosingRoomOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
@NonNull
private final SupportSQLiteOpenHelper mDelegateOpenHelper;
@NonNull
private final AutoClosingSupportSQLiteDatabase mAutoClosingDb;
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingRoomOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
@NonNull AutoCloser autoCloser) {
mDelegateOpenHelper = supportSQLiteOpenHelper;
mAutoCloser = autoCloser;
autoCloser.init(mDelegateOpenHelper);
mAutoClosingDb = new AutoClosingSupportSQLiteDatabase(mAutoCloser);
}
@Nullable
@Override
public String getDatabaseName() {
return mDelegateOpenHelper.getDatabaseName();
}
@Override
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegateOpenHelper.setWriteAheadLoggingEnabled(enabled);
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getWritableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@NonNull
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public SupportSQLiteDatabase getReadableDatabase() {
// Note we don't differentiate between writable db and readable db
// We try to open the db so the open callbacks run
mAutoClosingDb.pokeOpen();
return mAutoClosingDb;
}
@Override
public void close() {
try {
mAutoClosingDb.close();
} catch (IOException e) {
SneakyThrow.reThrow(e);
}
}
/**
* package protected to pass it to invalidation tracker...
*/
@NonNull
AutoCloser getAutoCloser() {
return this.mAutoCloser;
}
@NonNull
SupportSQLiteDatabase getAutoClosingDb() {
return this.mAutoClosingDb;
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegateOpenHelper;
}
/**
* SupportSQLiteDatabase that also keeps refcounts and autocloses the database
*/
static final class AutoClosingSupportSQLiteDatabase implements SupportSQLiteDatabase {
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingSupportSQLiteDatabase(@NonNull AutoCloser autoCloser) {
mAutoCloser = autoCloser;
}
void pokeOpen() {
mAutoCloser.executeRefCountingFunction(db -> null);
}
@Override
public SupportSQLiteStatement compileStatement(String sql) {
return new AutoClosingSupportSqliteStatement(sql, mAutoCloser);
}
@Override
public void beginTransaction() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransaction();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionNonExclusive() {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionNonExclusive();
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *must* be a
// endTransaction() call.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListener(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void beginTransactionWithListenerNonExclusive(
SQLiteTransactionListener transactionListener) {
// We assume that after every successful beginTransaction() call there *will* always
// be a corresponding endTransaction() call. Without a corresponding
// endTransactionCall we will never close the db.
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
try {
db.beginTransactionWithListenerNonExclusive(transactionListener);
} catch (Throwable t) {
// Note: we only want to decrement the ref count if the beginTransaction call
// fails since there won't be a corresponding endTransaction call.
mAutoCloser.decrementCountAndScheduleClose();
throw t;
}
}
@Override
public void endTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
// This should never happen.
throw new IllegalStateException("End transaction called but delegateDb is null");
}
try {
mAutoCloser.getDelegateDatabase().endTransaction();
} finally {
mAutoCloser.decrementCountAndScheduleClose();
}
}
@Override
public void setTransactionSuccessful() {
SupportSQLiteDatabase delegate = mAutoCloser.getDelegateDatabase();
if (delegate == null) {
// This should never happen.
throw new IllegalStateException("setTransactionSuccessful called but delegateDb "
+ "is null");
}
delegate.setTransactionSuccessful();
}
@Override
public boolean inTransaction() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::inTransaction);
}
@Override
public boolean isDbLockedByCurrentThread() {
if (mAutoCloser.getDelegateDatabase() == null) {
return false;
}
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDbLockedByCurrentThread);
}
@Override
public boolean yieldIfContendedSafely() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::yieldIfContendedSafely);
}
@Override
public int getVersion() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getVersion);
}
@Override
public void setVersion(int version) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setVersion(version);
return null;
});
}
@Override
public long getMaximumSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getMaximumSize);
}
@Override
public long setMaximumSize(long numBytes) {
return mAutoCloser.executeRefCountingFunction(db -> db.setMaximumSize(numBytes));
}
@Override
public long getPageSize() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPageSize);
}
@Override
public void setPageSize(long numBytes) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setPageSize(numBytes);
return null;
});
}
@Override
public Cursor query(String query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(String query, Object[] bindArgs) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, bindArgs);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public Cursor query(SupportSQLiteQuery query) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) {
Cursor result;
try {
SupportSQLiteDatabase db = mAutoCloser.incrementCountAndEnsureDbIsOpen();
result = db.query(query, cancellationSignal);
} catch (Throwable throwable) {
mAutoCloser.decrementCountAndScheduleClose();
throw throwable;
}
return new KeepAliveCursor(result, mAutoCloser);
}
@Override
public long insert(String table, int conflictAlgorithm, ContentValues values)
throws SQLException {
return mAutoCloser.executeRefCountingFunction(db -> db.insert(table, conflictAlgorithm,
values));
}
@Override
public int delete(String table, String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(
db -> db.delete(table, whereClause, whereArgs));
}
@Override
public int update(String table, int conflictAlgorithm, ContentValues values,
String whereClause, Object[] whereArgs) {
return mAutoCloser.executeRefCountingFunction(db -> db.update(table, conflictAlgorithm,
values, whereClause, whereArgs));
}
@Override
public void execSQL(String sql) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql);
return null;
});
}
@Override
public void execSQL(String sql, Object[] bindArgs) throws SQLException {
mAutoCloser.executeRefCountingFunction(db -> {
db.execSQL(sql, bindArgs);
return null;
});
}
@Override
public boolean isReadOnly() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::isReadOnly);
}
@Override
public boolean isOpen() {
// Get the db without incrementing the reference cause we don't want to open
// the db for an isOpen call.
SupportSQLiteDatabase localDelegate = mAutoCloser.getDelegateDatabase();
if (localDelegate == null) {
return false;
}
return localDelegate.isOpen();
}
@Override
public boolean needUpgrade(int newVersion) {
return mAutoCloser.executeRefCountingFunction(db -> db.needUpgrade(newVersion));
}
@Override
public String getPath() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getPath);
}
@Override
public void setLocale(Locale locale) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setLocale(locale);
return null;
});
}
@Override
public void setMaxSqlCacheSize(int cacheSize) {
mAutoCloser.executeRefCountingFunction(db -> {
db.setMaxSqlCacheSize(cacheSize);
return null;
});
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setForeignKeyConstraintsEnabled(boolean enable) {
mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
db.setForeignKeyConstraintsEnabled(enable);
}
return null;
});
}
@Override
public boolean enableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@Override
public void disableWriteAheadLogging() {
throw new UnsupportedOperationException("Enable/disable write ahead logging on the "
+ "OpenHelper instead of on the database directly.");
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public boolean isWriteAheadLoggingEnabled() {
return mAutoCloser.executeRefCountingFunction(db -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return db.isWriteAheadLoggingEnabled();
}
return false;
});
}
@Override
public List<Pair<String, String>> getAttachedDbs() {
return mAutoCloser.executeRefCountingFunction(SupportSQLiteDatabase::getAttachedDbs);
}
@Override
public boolean isDatabaseIntegrityOk() {
return mAutoCloser.executeRefCountingFunction(
SupportSQLiteDatabase::isDatabaseIntegrityOk);
}
@Override
public void close() throws IOException {
mAutoCloser.closeDatabaseIfOpen();
}
}
/**
* We need to keep the db alive until the cursor is closed, so we can't decrement our
* reference count until the cursor is closed. The underlying database will not close until
* this cursor is closed.
*/
private static final class KeepAliveCursor implements Cursor {
private final Cursor mDelegate;
private final AutoCloser mAutoCloser;
KeepAliveCursor(Cursor delegate, AutoCloser autoCloser) {
mDelegate = delegate;
mAutoCloser = autoCloser;
}
// close is the only important/changed method here:
@Override
public void close() {
mDelegate.close();
mAutoCloser.decrementCountAndScheduleClose();
}
@Override
public boolean isClosed() {
return mDelegate.isClosed();
}
@Override
public int getCount() {
return mDelegate.getCount();
}
@Override
public int getPosition() {
return mDelegate.getPosition();
}
@Override
public boolean move(int offset) {
return mDelegate.move(offset);
}
@Override
public boolean moveToPosition(int position) {
return mDelegate.moveToPosition(position);
}
@Override
public boolean moveToFirst() {
return mDelegate.moveToFirst();
}
@Override
public boolean moveToLast() {
return mDelegate.moveToLast();
}
@Override
public boolean moveToNext() {
return mDelegate.moveToNext();
}
@Override
public boolean moveToPrevious() {
return mDelegate.moveToPrevious();
}
@Override
public boolean isFirst() {
return mDelegate.isFirst();
}
@Override
public boolean isLast() {
return mDelegate.isLast();
}
@Override
public boolean isBeforeFirst() {
return mDelegate.isBeforeFirst();
}
@Override
public boolean isAfterLast() {
return mDelegate.isAfterLast();
}
@Override
public int getColumnIndex(String columnName) {
return mDelegate.getColumnIndex(columnName);
}
@Override
public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
return mDelegate.getColumnIndexOrThrow(columnName);
}
@Override
public String getColumnName(int columnIndex) {
return mDelegate.getColumnName(columnIndex);
}
@Override
public String[] getColumnNames() {
return mDelegate.getColumnNames();
}
@Override
public int getColumnCount() {
return mDelegate.getColumnCount();
}
@Override
public byte[] getBlob(int columnIndex) {
return mDelegate.getBlob(columnIndex);
}
@Override
public String getString(int columnIndex) {
return mDelegate.getString(columnIndex);
}
@Override
public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
mDelegate.copyStringToBuffer(columnIndex, buffer);
}
@Override
public short getShort(int columnIndex) {
return mDelegate.getShort(columnIndex);
}
@Override
public int getInt(int columnIndex) {
return mDelegate.getInt(columnIndex);
}
@Override
public long getLong(int columnIndex) {
return mDelegate.getLong(columnIndex);
}
@Override
public float getFloat(int columnIndex) {
return mDelegate.getFloat(columnIndex);
}
@Override
public double getDouble(int columnIndex) {
return mDelegate.getDouble(columnIndex);
}
@Override
public int getType(int columnIndex) {
return mDelegate.getType(columnIndex);
}
@Override
public boolean isNull(int columnIndex) {
return mDelegate.isNull(columnIndex);
}
/**
* @deprecated see Cursor.deactivate
*/
@Override
@Deprecated
public void deactivate() {
mDelegate.deactivate();
}
/**
* @deprecated see Cursor.requery
*/
@Override
@Deprecated
public boolean requery() {
return mDelegate.requery();
}
@Override
public void registerContentObserver(ContentObserver observer) {
mDelegate.registerContentObserver(observer);
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
mDelegate.unregisterContentObserver(observer);
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
mDelegate.registerDataSetObserver(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
mDelegate.unregisterDataSetObserver(observer);
}
@Override
public void setNotificationUri(ContentResolver cr, Uri uri) {
mDelegate.setNotificationUri(cr, uri);
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.Q)
@Override
public void setNotificationUris(@NonNull ContentResolver cr,
@NonNull List<Uri> uris) {
mDelegate.setNotificationUris(cr, uris);
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Override
public Uri getNotificationUri() {
return mDelegate.getNotificationUri();
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.Q)
@Nullable
@Override
public List<Uri> getNotificationUris() {
return mDelegate.getNotificationUris();
}
@Override
public boolean getWantsAllOnMoveCalls() {
return mDelegate.getWantsAllOnMoveCalls();
}
@SuppressLint("UnsafeNewApiCall")
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void setExtras(Bundle extras) {
mDelegate.setExtras(extras);
}
@Override
public Bundle getExtras() {
return mDelegate.getExtras();
}
@Override
public Bundle respond(Bundle extras) {
return mDelegate.respond(extras);
}
}
/**
* We can't close our db if the SupportSqliteStatement is open.
*
* Each of these that are created need to be registered with RefCounter.
*
* On auto-close, RefCounter needs to close each of these before closing the db that these
* were constructed from.
*
* Each of the methods here need to get
*/
//TODO(rohitsat) cache the prepared statement... I'm not sure what the performance implications
// are for the way it's done here, but caching the prepared statement would definitely be more
// complicated since we need to invalidate any of the PreparedStatements that were created
// with this db
private static class AutoClosingSupportSqliteStatement implements SupportSQLiteStatement {
private final String mSql;
private final ArrayList<Object> mBinds = new ArrayList<>();
private final AutoCloser mAutoCloser;
AutoClosingSupportSqliteStatement(
String sql, AutoCloser autoCloser) {
mSql = sql;
mAutoCloser = autoCloser;
}
private <T> T executeSqliteStatementWithRefCount(Function<SupportSQLiteStatement, T> func) {
return mAutoCloser.executeRefCountingFunction(
db -> {
SupportSQLiteStatement statement = db.compileStatement(mSql);
doBinds(statement);
return func.apply(statement);
}
);
}
private void doBinds(SupportSQLiteStatement supportSQLiteStatement) {
// Replay the binds
for (int i = 0; i < mBinds.size(); i++) {
int bindIndex = i + 1; // Bind indices are 1 based so we start at 1 not 0
Object bind = mBinds.get(i);
if (bind == null) {
supportSQLiteStatement.bindNull(bindIndex);
} else if (bind instanceof Long) {
supportSQLiteStatement.bindLong(bindIndex, (Long) bind);
} else if (bind instanceof Double) {
supportSQLiteStatement.bindDouble(bindIndex, (Double) bind);
} else if (bind instanceof String) {
supportSQLiteStatement.bindString(bindIndex, (String) bind);
} else if (bind instanceof byte[]) {
supportSQLiteStatement.bindBlob(bindIndex, (byte[]) bind);
}
}
}
private void saveBinds(int bindIndex, Object value) {
int index = bindIndex - 1;
if (index >= mBinds.size()) {
// Add null entries to the list until we have the desired # of indices
for (int i = mBinds.size(); i <= index; i++) {
mBinds.add(null);
}
}
mBinds.set(index, value);
}
@Override
public void close() throws IOException {
// Nothing to do here since we re-compile the statement each time.
}
@Override
public void execute() {
executeSqliteStatementWithRefCount(statement -> {
statement.execute();
return null;
});
}
@Override
public int executeUpdateDelete() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeUpdateDelete);
}
@Override
public long executeInsert() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::executeInsert);
}
@Override
public long simpleQueryForLong() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForLong);
}
@Override
public String simpleQueryForString() {
return executeSqliteStatementWithRefCount(SupportSQLiteStatement::simpleQueryForString);
}
@Override
public void bindNull(int index) {
saveBinds(index, null);
}
@Override
public void bindLong(int index, long value) {
saveBinds(index, value);
}
@Override
public void bindDouble(int index, double value) {
saveBinds(index, value);
}
@Override
public void bindString(int index, String value) {
saveBinds(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
saveBinds(index, value);
}
@Override
public void clearBindings() {
mBinds.clear();
}
}
}

@ -0,0 +1,48 @@
/*
* Copyright 2020 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.sqlite.db.SupportSQLiteOpenHelper;
/**
* Factory class for AutoClosingRoomOpenHelper
*/
final class AutoClosingRoomOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
@NonNull
private final AutoCloser mAutoCloser;
AutoClosingRoomOpenHelperFactory(
@NonNull SupportSQLiteOpenHelper.Factory factory,
@NonNull AutoCloser autoCloser) {
mDelegate = factory;
mAutoCloser = autoCloser;
}
/**
* @return AutoClosingRoomOpenHelper instances.
*/
@Override
@NonNull
public AutoClosingRoomOpenHelper create(
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
return new AutoClosingRoomOpenHelper(mDelegate.create(configuration), mAutoCloser);
}
}

@ -16,6 +16,7 @@
package androidx.room;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
@ -24,8 +25,11 @@ import androidx.annotation.RestrictTo;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.io.InputStream;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
/**
@ -59,6 +63,12 @@ public class DatabaseConfiguration {
@Nullable
public final List<RoomDatabase.Callback> callbacks;
@Nullable
public final RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback;
@NonNull
public final List<Object> typeConverters;
/**
* Whether Room should throw an exception for queries run on the main thread.
*/
@ -116,13 +126,21 @@ public class DatabaseConfiguration {
@Nullable
public final File copyFromFile;
/**
* The callable to get the input stream from which a pre-package database file will be copied
* from.
*/
@Nullable
public final Callable<InputStream> copyFromInputStream;
/**
* 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)}
* RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List<Object>)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
@ -152,7 +170,7 @@ public class DatabaseConfiguration {
@Nullable Set<Integer> migrationNotRequiredFrom) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, queryExecutor, false,
requireMigration, false, migrationNotRequiredFrom, null, null);
requireMigration, false, migrationNotRequiredFrom, null, null, null, null, null);
}
/**
@ -160,7 +178,8 @@ public class DatabaseConfiguration {
*
* @deprecated Use {@link #DatabaseConfiguration(Context, String,
* SupportSQLiteOpenHelper.Factory, RoomDatabase.MigrationContainer, List, boolean,
* RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File)}
* RoomDatabase.JournalMode, Executor, Executor, boolean, boolean, boolean, Set, String, File,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List<Object>)}
*
* @param context The application context.
* @param name Name of the database, can be null if it is in memory.
@ -197,12 +216,17 @@ public class DatabaseConfiguration {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, null, null);
migrationNotRequiredFrom, null, null, null, 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,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List<Object>)}
*
* @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.
@ -223,6 +247,7 @@ public class DatabaseConfiguration {
*
* @hide
*/
@Deprecated
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@ -238,6 +263,169 @@ public class DatabaseConfiguration {
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, null, 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,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List<Object>)}
*
* @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.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
*
* @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<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
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,
* Callable, RoomDatabase.PrepackagedDatabaseCallback, List<Object>)}
*
* @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.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
*
* @hide
*/
@Deprecated
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback) {
this(context, name, sqliteOpenHelperFactory, migrationContainer, callbacks,
allowMainThreadQueries, journalMode, queryExecutor, transactionExecutor,
multiInstanceInvalidation, requireMigration, allowDestructiveMigrationOnDowngrade,
migrationNotRequiredFrom, copyFromAssetPath, copyFromFile, copyFromInputStream,
prepackagedDatabaseCallback, 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.
* @param copyFromInputStream The callable to get the input stream from which a
* pre-package database file will be copied from.
* @param prepackagedDatabaseCallback The pre-packaged callback.
* @param typeConverters The type converters.
*
* @hide
*/
@SuppressLint("LambdaLast")
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public DatabaseConfiguration(@NonNull Context context, @Nullable String name,
@NonNull SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory,
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
@NonNull RoomDatabase.JournalMode journalMode,
@NonNull Executor queryExecutor,
@NonNull Executor transactionExecutor,
boolean multiInstanceInvalidation,
boolean requireMigration,
boolean allowDestructiveMigrationOnDowngrade,
@Nullable Set<Integer> migrationNotRequiredFrom,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@Nullable RoomDatabase.PrepackagedDatabaseCallback prepackagedDatabaseCallback,
@Nullable List<Object> typeConverters) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
@ -253,6 +441,9 @@ public class DatabaseConfiguration {
this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
this.copyFromAssetPath = copyFromAssetPath;
this.copyFromFile = copyFromFile;
this.copyFromInputStream = copyFromInputStream;
this.prepackagedDatabaseCallback = prepackagedDatabaseCallback;
this.typeConverters = typeConverters == null ? Collections.emptyList() : typeConverters;
}
/**

@ -0,0 +1,36 @@
/*
* Copyright 2020 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.sqlite.db.SupportSQLiteOpenHelper;
/**
* Package private interface for OpenHelpers which delegate to other open helpers.
*
* TODO(b/175612939): delete this interface once implementations are merged.
*/
interface DelegatingOpenHelper {
/**
* Returns the delegate open helper (which may itself be a DelegatingOpenHelper) so
* configurations on specific instances can be applied.
*
* @return the delegate
*/
@NonNull
SupportSQLiteOpenHelper getDelegate();
}

@ -0,0 +1,29 @@
/*
* Copyright 2020 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.RequiresOptIn;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
/**
* APIs marked with ExperimentalRoomApi are experimental and may change.
*/
@Target({ElementType.METHOD})
@RequiresOptIn()
@interface ExperimentalRoomApi {}

@ -20,6 +20,7 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
@ -89,6 +90,9 @@ public class InvalidationTracker {
@NonNull
private Map<String, Set<String>> mViewTables;
@Nullable
AutoCloser mAutoCloser = null;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final RoomDatabase mDatabase;
@ -159,6 +163,23 @@ public class InvalidationTracker {
}
}
/**
* Sets the auto closer for this invalidation tracker so that the invalidation tracker can
* ensure that the database is not closed if there are pending invalidations that haven't yet
* been flushed.
*
* This also adds a callback to the autocloser to ensure that the InvalidationTracker is in
* an ok state once the table is invalidated.
*
* This must be called before the database is used.
*
* @param autoCloser the autocloser associated with the db
*/
void setAutoCloser(AutoCloser autoCloser) {
this.mAutoCloser = autoCloser;
mAutoCloser.setAutoCloseCallback(this::onAutoCloseCallback);
}
/**
* Internal method to initialize table tracking.
* <p>
@ -182,6 +203,13 @@ public class InvalidationTracker {
}
}
void onAutoCloseCallback() {
synchronized (this) {
mInitialized = false;
mObservedTableTracker.resetTriggerState();
}
}
void startMultiInstanceInvalidation(Context context, String name) {
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this,
mDatabase.getQueryExecutor());
@ -249,6 +277,9 @@ public class InvalidationTracker {
* <p>
* If one of the tables in the Observer does not exist in the database, this method throws an
* {@link IllegalArgumentException}.
* <p>
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer which listens the database for changes.
*/
@ -305,6 +336,15 @@ public class InvalidationTracker {
return tables.toArray(new String[tables.size()]);
}
private static void beginTransactionInternal(SupportSQLiteDatabase database) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& database.isWriteAheadLoggingEnabled()) {
database.beginTransactionNonExclusive();
} else {
database.beginTransaction();
}
}
/**
* Adds an observer but keeps a weak reference back to it.
* <p>
@ -322,6 +362,9 @@ public class InvalidationTracker {
/**
* Removes the observer from the observers list.
* <p>
* This method should be called on a background/worker thread as it performs database
* operations.
*
* @param observer The observer to remove.
*/
@ -360,8 +403,8 @@ public class InvalidationTracker {
public void run() {
final Lock closeLock = mDatabase.getCloseLock();
Set<Integer> invalidatedTableIds = null;
closeLock.lock();
try {
closeLock.lock();
if (!ensureInitialization()) {
return;
@ -383,7 +426,7 @@ public class InvalidationTracker {
// 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();
db.beginTransactionNonExclusive();
try {
invalidatedTableIds = checkUpdatedTable();
db.setTransactionSuccessful();
@ -399,6 +442,10 @@ public class InvalidationTracker {
exception);
} finally {
closeLock.unlock();
if (mAutoCloser != null) {
mAutoCloser.decrementCountAndScheduleClose();
}
}
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
synchronized (mObserverMap) {
@ -439,6 +486,13 @@ public class InvalidationTracker {
public void refreshVersionsAsync() {
// TODO we should consider doing this sync instead of async.
if (mPendingRefresh.compareAndSet(false, true)) {
if (mAutoCloser != null) {
// refreshVersionsAsync is called with the ref count incremented from
// RoomDatabase, so the db can't be closed here, but we need to be sure that our
// db isn't closed until refresh is completed. This increment call must be
// matched with a corresponding call in mRefreshRunnable.
mAutoCloser.incrementCountAndEnsureDbIsOpen();
}
mDatabase.getQueryExecutor().execute(mRefreshRunnable);
}
}
@ -451,6 +505,10 @@ public class InvalidationTracker {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@WorkerThread
public void refreshVersionsSync() {
if (mAutoCloser != null) {
// This increment call must be matched with a corresponding call in mRefreshRunnable.
mAutoCloser.incrementCountAndEnsureDbIsOpen();
}
syncTriggers();
mRefreshRunnable.run();
}
@ -495,7 +553,7 @@ public class InvalidationTracker {
return;
}
final int limit = tablesToSync.length;
database.beginTransaction();
beginTransactionInternal(database);
try {
for (int tableId = 0; tableId < limit; tableId++) {
switch (tablesToSync[tableId]) {
@ -785,6 +843,17 @@ public class InvalidationTracker {
return needTriggerSync;
}
/**
* If we are re-opening the db we'll need to add all the triggers that we need so change
* the current state to false for all.
*/
void resetTriggerState() {
synchronized (this) {
Arrays.fill(mTriggerStates, false);
mNeedsSync = true;
}
}
/**
* If this returns non-null, you must call onSyncCompleted.
*

@ -138,22 +138,6 @@ class MultiInstanceInvalidationClient {
}
};
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}.
@ -196,7 +180,16 @@ class MultiInstanceInvalidationClient {
void stop() {
if (mStopped.compareAndSet(false, true)) {
mExecutor.execute(mTearDownRunnable);
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);
}
}
}

@ -0,0 +1,302 @@
/*
* Copyright 2020 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.ContentValues;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteTransactionListener;
import android.os.Build;
import android.os.CancellationSignal;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
/**
* Implements {@link SupportSQLiteDatabase} for SQLite queries.
*/
final class QueryInterceptorDatabase implements SupportSQLiteDatabase {
private final SupportSQLiteDatabase mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
QueryInterceptorDatabase(@NonNull SupportSQLiteDatabase supportSQLiteDatabase,
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
queryCallbackExecutor) {
mDelegate = supportSQLiteDatabase;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@NonNull
@Override
public SupportSQLiteStatement compileStatement(@NonNull String sql) {
return new QueryInterceptorStatement(mDelegate.compileStatement(sql),
mQueryCallback, sql, mQueryCallbackExecutor);
}
@Override
public void beginTransaction() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransaction();
}
@Override
public void beginTransactionNonExclusive() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionNonExclusive();
}
@Override
public void beginTransactionWithListener(@NonNull SQLiteTransactionListener
transactionListener) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN EXCLUSIVE TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionWithListener(transactionListener);
}
@Override
public void beginTransactionWithListenerNonExclusive(
@NonNull SQLiteTransactionListener transactionListener) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("BEGIN DEFERRED TRANSACTION",
Collections.emptyList()));
mDelegate.beginTransactionWithListenerNonExclusive(transactionListener);
}
@Override
public void endTransaction() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("END TRANSACTION",
Collections.emptyList()));
mDelegate.endTransaction();
}
@Override
public void setTransactionSuccessful() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery("TRANSACTION SUCCESSFUL",
Collections.emptyList()));
mDelegate.setTransactionSuccessful();
}
@Override
public boolean inTransaction() {
return mDelegate.inTransaction();
}
@Override
public boolean isDbLockedByCurrentThread() {
return mDelegate.isDbLockedByCurrentThread();
}
@Override
public boolean yieldIfContendedSafely() {
return mDelegate.yieldIfContendedSafely();
}
@Override
public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) {
return mDelegate.yieldIfContendedSafely(sleepAfterYieldDelay);
}
@Override
public int getVersion() {
return mDelegate.getVersion();
}
@Override
public void setVersion(int version) {
mDelegate.setVersion(version);
}
@Override
public long getMaximumSize() {
return mDelegate.getMaximumSize();
}
@Override
public long setMaximumSize(long numBytes) {
return mDelegate.setMaximumSize(numBytes);
}
@Override
public long getPageSize() {
return mDelegate.getPageSize();
}
@Override
public void setPageSize(long numBytes) {
mDelegate.setPageSize(numBytes);
}
@NonNull
@Override
public Cursor query(@NonNull String query) {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
Collections.emptyList()));
return mDelegate.query(query);
}
@NonNull
@Override
public Cursor query(@NonNull String query, @NonNull Object[] bindArgs) {
List<Object> inputArguments = new ArrayList<>();
inputArguments.addAll(Arrays.asList(bindArgs));
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query,
inputArguments));
return mDelegate.query(query, bindArgs);
}
@NonNull
@Override
public Cursor query(@NonNull SupportSQLiteQuery query) {
QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
query.bindTo(queryInterceptorProgram);
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
queryInterceptorProgram.getBindArgs()));
return mDelegate.query(query);
}
@NonNull
@Override
public Cursor query(@NonNull SupportSQLiteQuery query,
@NonNull CancellationSignal cancellationSignal) {
QueryInterceptorProgram queryInterceptorProgram = new QueryInterceptorProgram();
query.bindTo(queryInterceptorProgram);
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(query.getSql(),
queryInterceptorProgram.getBindArgs()));
return mDelegate.query(query);
}
@Override
public long insert(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values)
throws SQLException {
return mDelegate.insert(table, conflictAlgorithm, values);
}
@Override
public int delete(@NonNull String table, @NonNull String whereClause,
@NonNull Object[] whereArgs) {
return mDelegate.delete(table, whereClause, whereArgs);
}
@Override
public int update(@NonNull String table, int conflictAlgorithm, @NonNull ContentValues values,
@NonNull String whereClause,
@NonNull Object[] whereArgs) {
return mDelegate.update(table, conflictAlgorithm, values, whereClause,
whereArgs);
}
@Override
public void execSQL(@NonNull String sql) throws SQLException {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, new ArrayList<>(0)));
mDelegate.execSQL(sql);
}
@Override
public void execSQL(@NonNull String sql, @NonNull Object[] bindArgs) throws SQLException {
List<Object> inputArguments = new ArrayList<>();
inputArguments.addAll(Arrays.asList(bindArgs));
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(sql, inputArguments));
mDelegate.execSQL(sql, inputArguments.toArray());
}
@Override
public boolean isReadOnly() {
return mDelegate.isReadOnly();
}
@Override
public boolean isOpen() {
return mDelegate.isOpen();
}
@Override
public boolean needUpgrade(int newVersion) {
return mDelegate.needUpgrade(newVersion);
}
@NonNull
@Override
public String getPath() {
return mDelegate.getPath();
}
@Override
public void setLocale(@NonNull Locale locale) {
mDelegate.setLocale(locale);
}
@Override
public void setMaxSqlCacheSize(int cacheSize) {
mDelegate.setMaxSqlCacheSize(cacheSize);
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setForeignKeyConstraintsEnabled(boolean enable) {
mDelegate.setForeignKeyConstraintsEnabled(enable);
}
@Override
public boolean enableWriteAheadLogging() {
return mDelegate.enableWriteAheadLogging();
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void disableWriteAheadLogging() {
mDelegate.disableWriteAheadLogging();
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public boolean isWriteAheadLoggingEnabled() {
return mDelegate.isWriteAheadLoggingEnabled();
}
@NonNull
@Override
public List<Pair<String, String>> getAttachedDbs() {
return mDelegate.getAttachedDbs();
}
@Override
public boolean isDatabaseIntegrityOk() {
return mDelegate.isDatabaseIntegrityOk();
}
@Override
public void close() throws IOException {
mDelegate.close();
}
}

@ -0,0 +1,78 @@
/*
* Copyright 2020 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.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.util.concurrent.Executor;
final class QueryInterceptorOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
private final SupportSQLiteOpenHelper mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
QueryInterceptorOpenHelper(@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper,
@NonNull RoomDatabase.QueryCallback queryCallback, @NonNull Executor
queryCallbackExecutor) {
mDelegate = supportSQLiteOpenHelper;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@Nullable
@Override
public String getDatabaseName() {
return mDelegate.getDatabaseName();
}
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
@Override
public void setWriteAheadLoggingEnabled(boolean enabled) {
mDelegate.setWriteAheadLoggingEnabled(enabled);
}
@Override
public SupportSQLiteDatabase getWritableDatabase() {
return new QueryInterceptorDatabase(mDelegate.getWritableDatabase(), mQueryCallback,
mQueryCallbackExecutor);
}
@Override
public SupportSQLiteDatabase getReadableDatabase() {
return new QueryInterceptorDatabase(mDelegate.getReadableDatabase(), mQueryCallback,
mQueryCallbackExecutor);
}
@Override
public void close() {
mDelegate.close();
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegate;
}
}

@ -0,0 +1,50 @@
/*
* Copyright 2020 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.sqlite.db.SupportSQLiteOpenHelper;
import java.util.concurrent.Executor;
/**
* Implements {@link SupportSQLiteOpenHelper.Factory} to wrap QueryInterceptorOpenHelper.
*/
@SuppressWarnings("AcronymName")
final class QueryInterceptorOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
private final SupportSQLiteOpenHelper.Factory mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final Executor mQueryCallbackExecutor;
@SuppressWarnings("LambdaLast")
QueryInterceptorOpenHelperFactory(@NonNull SupportSQLiteOpenHelper.Factory factory,
@NonNull RoomDatabase.QueryCallback queryCallback,
@NonNull Executor queryCallbackExecutor) {
mDelegate = factory;
mQueryCallback = queryCallback;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@NonNull
@Override
public SupportSQLiteOpenHelper create(
@NonNull SupportSQLiteOpenHelper.Configuration configuration) {
return new QueryInterceptorOpenHelper(mDelegate.create(configuration), mQueryCallback,
mQueryCallbackExecutor);
}
}

@ -0,0 +1,82 @@
/*
* Copyright 2020 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.sqlite.db.SupportSQLiteProgram;
import java.util.ArrayList;
import java.util.List;
/**
* A program implementing an {@link SupportSQLiteProgram} API to record bind arguments.
*/
final class QueryInterceptorProgram implements SupportSQLiteProgram {
private List<Object> mBindArgsCache = new ArrayList<>();
@Override
public void bindNull(int index) {
saveArgsToCache(index, null);
}
@Override
public void bindLong(int index, long value) {
saveArgsToCache(index, value);
}
@Override
public void bindDouble(int index, double value) {
saveArgsToCache(index, value);
}
@Override
public void bindString(int index, String value) {
saveArgsToCache(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
saveArgsToCache(index, value);
}
@Override
public void clearBindings() {
mBindArgsCache.clear();
}
@Override
public void close() { }
private void saveArgsToCache(int bindIndex, Object value) {
// The index into bind methods are 1...n
int index = bindIndex - 1;
if (index >= mBindArgsCache.size()) {
for (int i = mBindArgsCache.size(); i <= index; i++) {
mBindArgsCache.add(null);
}
}
mBindArgsCache.set(index, value);
}
/**
* Returns the list of arguments associated with the query.
*
* @return argument list.
*/
List<Object> getBindArgs() {
return mBindArgsCache;
}
}

@ -0,0 +1,128 @@
/*
* Copyright 2020 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.sqlite.db.SupportSQLiteStatement;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
/**
* Implements an instance of {@link SupportSQLiteStatement} for SQLite queries.
*/
final class QueryInterceptorStatement implements SupportSQLiteStatement {
private final SupportSQLiteStatement mDelegate;
private final RoomDatabase.QueryCallback mQueryCallback;
private final String mSqlStatement;
private final List<Object> mBindArgsCache = new ArrayList<>();
private final Executor mQueryCallbackExecutor;
QueryInterceptorStatement(@NonNull SupportSQLiteStatement compileStatement,
@NonNull RoomDatabase.QueryCallback queryCallback, String sqlStatement,
@NonNull Executor queryCallbackExecutor) {
mDelegate = compileStatement;
mQueryCallback = queryCallback;
mSqlStatement = sqlStatement;
mQueryCallbackExecutor = queryCallbackExecutor;
}
@Override
public void execute() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
mDelegate.execute();
}
@Override
public int executeUpdateDelete() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
return mDelegate.executeUpdateDelete();
}
@Override
public long executeInsert() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
return mDelegate.executeInsert();
}
@Override
public long simpleQueryForLong() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
return mDelegate.simpleQueryForLong();
}
@Override
public String simpleQueryForString() {
mQueryCallbackExecutor.execute(() -> mQueryCallback.onQuery(mSqlStatement, mBindArgsCache));
return mDelegate.simpleQueryForString();
}
@Override
public void bindNull(int index) {
saveArgsToCache(index, mBindArgsCache.toArray());
mDelegate.bindNull(index);
}
@Override
public void bindLong(int index, long value) {
saveArgsToCache(index, value);
mDelegate.bindLong(index, value);
}
@Override
public void bindDouble(int index, double value) {
saveArgsToCache(index, value);
mDelegate.bindDouble(index, value);
}
@Override
public void bindString(int index, String value) {
saveArgsToCache(index, value);
mDelegate.bindString(index, value);
}
@Override
public void bindBlob(int index, byte[] value) {
saveArgsToCache(index, value);
mDelegate.bindBlob(index, value);
}
@Override
public void clearBindings() {
mBindArgsCache.clear();
mDelegate.clearBindings();
}
@Override
public void close() throws IOException {
mDelegate.close();
}
private void saveArgsToCache(int bindIndex, Object value) {
int index = bindIndex - 1;
if (index >= mBindArgsCache.size()) {
// Add null entries to the list until we have the desired # of indices
for (int i = mBindArgsCache.size(); i <= index; i++) {
mBindArgsCache.add(null);
}
}
mBindArgsCache.set(index, value);
}
}

@ -80,14 +80,17 @@ public class Room {
String name = klass.getCanonicalName();
final String postPackageName = fullPackage.isEmpty()
? name
: (name.substring(fullPackage.length() + 1));
: name.substring(fullPackage.length() + 1);
final String implName = postPackageName.replace('.', '_') + suffix;
//noinspection TryWithIdenticalCatches
try {
final String fullClassName = fullPackage.isEmpty()
? implName
: fullPackage + "." + implName;
@SuppressWarnings("unchecked")
final Class<T> aClass = (Class<T>) Class.forName(
fullPackage.isEmpty() ? implName : fullPackage + "." + implName);
fullClassName, true, klass.getClassLoader());
return aClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find implementation for "

@ -26,6 +26,7 @@ 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;
@ -42,7 +43,9 @@ 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;
@ -51,8 +54,8 @@ 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.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@ -89,14 +92,18 @@ public abstract class RoomDatabase {
boolean mWriteAheadLoggingEnabled;
/**
* @deprecated Will be hidden in the next release.
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@Nullable
@Deprecated
protected List<Callback> mCallbacks;
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.
@ -127,8 +134,8 @@ public abstract class RoomDatabase {
return mSuspendingTransactionId;
}
private final Map<String, Object> mBackingFieldMap = new ConcurrentHashMap<>();
private final Map<String, Object> mBackingFieldMap =
Collections.synchronizedMap(new HashMap<>());
/**
* Gets the map for storing extension properties of Kotlin type.
@ -140,6 +147,23 @@ public abstract class RoomDatabase {
return mBackingFieldMap;
}
// Updated later to an unmodifiable map when init is called.
private final Map<Class<?>, Object> mTypeConverters;
/**
* Gets the instance of the given Type Converter.
*
* @param klass The Type Converter class.
* @param <T> 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> T getTypeConverter(@NonNull Class<T> klass) {
return (T) mTypeConverters.get(klass);
}
/**
* Creates a RoomDatabase.
* <p>
@ -149,6 +173,7 @@ public abstract class RoomDatabase {
*/
public RoomDatabase() {
mInvalidationTracker = createInvalidationTracker();
mTypeConverters = new HashMap<>();
}
/**
@ -159,10 +184,23 @@ public abstract class RoomDatabase {
@CallSuper
public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
if (mOpenHelper instanceof SQLiteCopyOpenHelper) {
SQLiteCopyOpenHelper copyOpenHelper = (SQLiteCopyOpenHelper) mOpenHelper;
// 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;
@ -177,6 +215,65 @@ public abstract class RoomDatabase {
mInvalidationTracker.startMultiInstanceInvalidation(configuration.context,
configuration.name);
}
Map<Class<?>, List<Class<?>>> 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<Class<?>, List<Class<?>>> 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.");
}
}
}
/**
* 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 <T> the type of clazz
* @return the instance of clazz, otherwise null
*/
@Nullable
@SuppressWarnings("unchecked")
private <T> T unwrapOpenHelper(Class<T> clazz, SupportSQLiteOpenHelper openHelper) {
if (clazz.isInstance(openHelper)) {
return (T) openHelper;
}
if (openHelper instanceof DelegatingOpenHelper) {
return unwrapOpenHelper(clazz, ((DelegatingOpenHelper) openHelper).getDelegate());
}
return null;
}
/**
@ -210,6 +307,21 @@ public abstract class RoomDatabase {
@NonNull
protected abstract InvalidationTracker createInvalidationTracker();
/**
* Returns a Map of String -> List&lt;Class&gt; 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.
* <p>
* 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<Class<?>, List<Class<?>>> getRequiredTypeConverters() {
return Collections.emptyMap();
}
/**
* Deletes all rows from all the tables that are registered to this database as
* {@link Database#entities()}.
@ -231,6 +343,12 @@ public abstract class RoomDatabase {
* @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();
}
@ -241,8 +359,8 @@ public abstract class RoomDatabase {
public void close() {
if (isOpen()) {
final Lock closeLock = mCloseLock.writeLock();
closeLock.lock();
try {
closeLock.lock();
mInvalidationTracker.stopMultiInstanceInvalidation();
mOpenHelper.close();
} finally {
@ -348,10 +466,27 @@ public abstract class RoomDatabase {
*/
@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);
database.beginTransaction();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& database.isWriteAheadLoggingEnabled()) {
database.beginTransactionNonExclusive();
} else {
database.beginTransaction();
}
}
/**
@ -361,6 +496,17 @@ public abstract class RoomDatabase {
*/
@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
@ -540,6 +686,10 @@ public abstract class RoomDatabase {
private final String mName;
private final Context mContext;
private ArrayList<Callback> mCallbacks;
private PrepackagedDatabaseCallback mPrepackagedDatabaseCallback;
private QueryCallback mQueryCallback;
private Executor mQueryCallbackExecutor;
private List<Object> mTypeConverters;
/** The Executor used to run database queries. This should be background-threaded. */
private Executor mQueryExecutor;
@ -551,6 +701,10 @@ public abstract class RoomDatabase {
private boolean mMultiInstanceInvalidation;
private boolean mRequireMigration;
private boolean mAllowDestructiveMigrationOnDowngrade;
private long mAutoCloseTimeout = -1L;
private TimeUnit mAutoCloseTimeUnit;
/**
* Migrations, mapped by from-to pairs.
*/
@ -565,6 +719,7 @@ public abstract class RoomDatabase {
private String mCopyFromAssetPath;
private File mCopyFromFile;
private Callable<InputStream> mCopyFromInputStream;
Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
mContext = context;
@ -601,6 +756,37 @@ public abstract class RoomDatabase {
return this;
}
/**
* Configures Room to create and open the database using a pre-packaged database located in
* the application 'assets/' folder.
* <p>
* 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".
* <p>
* 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.
* <p>
* 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<T> 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.
* <p>
@ -612,6 +798,9 @@ public abstract class RoomDatabase {
* pre-packaged database schema utilizing the exported schema files generated when
* {@link Database#exportSchema()} is enabled.
* <p>
* 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.
* <p>
* This method is not supported for an in memory database {@link Builder}.
*
* @param databaseFile The database file.
@ -624,6 +813,108 @@ public abstract class RoomDatabase {
return this;
}
/**
* Configures Room to create and open the database using a pre-packaged database file.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<T> 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}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<T> createFromInputStream(
@NonNull Callable<InputStream> inputStreamCallable) {
mCopyFromInputStream = inputStreamCallable;
return this;
}
/**
* Configures Room to create and open the database using a pre-packaged database via an
* {@link InputStream}.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<T> createFromInputStream(
@NonNull Callable<InputStream> inputStreamCallable,
@NonNull PrepackagedDatabaseCallback callback) {
mPrepackagedDatabaseCallback = callback;
mCopyFromInputStream = inputStreamCallable;
return this;
}
/**
* Sets the database factory. If not set, it defaults to
* {@link FrameworkSQLiteOpenHelperFactory}.
@ -873,6 +1164,87 @@ public abstract class RoomDatabase {
return this;
}
/**
* Sets a {@link QueryCallback} to be invoked when queries are executed.
* <p>
* 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.
* <p>
* 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<T> 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<T> 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.
* <p>
* Auto-closing is not compatible with in-memory databases since the data will be lost
* when the database is auto-closed.
* <p>
* 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}.
* <p>
* 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.
* <p>
* The auto-closing database operation runs on the query executor.
* <p>
* 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<T> 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.
* <p>
@ -915,28 +1287,59 @@ public abstract class RoomDatabase {
}
}
SupportSQLiteOpenHelper.Factory factory;
AutoCloser autoCloser = null;
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
factory = new FrameworkSQLiteOpenHelperFactory();
} else {
factory = mFactory;
}
if (mCopyFromAssetPath != null || mCopyFromFile != null) {
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.");
}
if (mCopyFromAssetPath != null && mCopyFromFile != null) {
throw new IllegalArgumentException("Both createFromAsset() and "
+ "createFromFile() was called on this Builder but the database can "
+ "only be created using one of the two configurations.");
final int copyConfigurations = (mCopyFromAssetPath == null ? 0 : 1) +
(mCopyFromFile == null ? 0 : 1) +
(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.");
}
mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
mFactory);
factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
mCopyFromInputStream, factory);
}
if (mQueryCallback != null) {
factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback,
mQueryCallbackExecutor);
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(
mContext,
mName,
mFactory,
factory,
mMigrationContainer,
mCallbacks,
mAllowMainThreadQueries,
@ -948,7 +1351,10 @@ public abstract class RoomDatabase {
mAllowDestructiveMigrationOnDowngrade,
mMigrationsNotRequiredFrom,
mCopyFromAssetPath,
mCopyFromFile);
mCopyFromFile,
mCopyFromInputStream,
mPrepackagedDatabaseCallback,
mTypeConverters);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
@ -1081,4 +1487,41 @@ public abstract class RoomDatabase {
public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db){
}
}
/**
* Callback for {@link Builder#createFromAsset(String)}, {@link Builder#createFromFile(File)}
* and {@link Builder#createFromInputStream(Callable)}
* <p>
* 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<Object>
bindArgs);
}
}

@ -28,20 +28,23 @@ import androidx.room.util.DBUtil;
import androidx.room.util.FileUtil;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.util.concurrent.Callable;
/**
* An open helper that will copy & open a pre-populated database if it doesn't exists in internal
* storage.
*/
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper, DelegatingOpenHelper {
@NonNull
private final Context mContext;
@ -49,6 +52,8 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
@Nullable
private final Callable<InputStream> mCopyFromInputStream;
private final int mDatabaseVersion;
@NonNull
private final SupportSQLiteOpenHelper mDelegate;
@ -61,11 +66,13 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
@NonNull Context context,
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
int databaseVersion,
@NonNull SupportSQLiteOpenHelper supportSQLiteOpenHelper) {
mContext = context;
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
mCopyFromInputStream = copyFromInputStream;
mDatabaseVersion = databaseVersion;
mDelegate = supportSQLiteOpenHelper;
}
@ -84,7 +91,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
@Override
public synchronized SupportSQLiteDatabase getWritableDatabase() {
if (!mVerified) {
verifyDatabaseFile();
verifyDatabaseFile(true);
mVerified = true;
}
return mDelegate.getWritableDatabase();
@ -93,7 +100,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
@Override
public synchronized SupportSQLiteDatabase getReadableDatabase() {
if (!mVerified) {
verifyDatabaseFile();
verifyDatabaseFile(false);
mVerified = true;
}
return mDelegate.getReadableDatabase();
@ -105,13 +112,19 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
mVerified = false;
}
@Override
@NonNull
public SupportSQLiteOpenHelper getDelegate() {
return mDelegate;
}
// 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() {
private void verifyDatabaseFile(boolean writable) {
String databaseName = getDatabaseName();
File databaseFile = mContext.getDatabasePath(databaseName);
boolean processLevelLock = mDatabaseConfiguration == null
@ -125,7 +138,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
if (!databaseFile.exists()) {
try {
// No database file found, copy and be done.
copyDatabaseFile(databaseFile);
copyDatabaseFile(databaseFile, writable);
return;
} catch (IOException e) {
throw new RuntimeException("Unable to copy database file.", e);
@ -157,7 +170,7 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
if (mContext.deleteDatabase(databaseName)) {
try {
copyDatabaseFile(databaseFile);
copyDatabaseFile(databaseFile, writable);
} 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.
@ -172,14 +185,23 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
}
}
private void copyDatabaseFile(File destinationFile) throws IOException {
private void copyDatabaseFile(File destinationFile, boolean writable) throws IOException {
ReadableByteChannel input;
if (mCopyFromAssetPath != null) {
input = Channels.newChannel(mContext.getAssets().open(mCopyFromAssetPath));
} else if (mCopyFromFile != null) {
input = new FileInputStream(mCopyFromFile).getChannel();
} else if (mCopyFromInputStream != null) {
final InputStream inputStream;
try {
inputStream = mCopyFromInputStream.call();
} catch (Exception e) {
throw new IOException("inputStreamCallable exception on call", e);
}
input = Channels.newChannel(inputStream);
} else {
throw new IllegalStateException("copyFromAssetPath and copyFromFile == null!");
throw new IllegalStateException("copyFromAssetPath, copyFromFile and "
+ "copyFromInputStream are all null!");
}
// An intermediate file is used so that we never end up with a half-copied database file
@ -196,10 +218,58 @@ class SQLiteCopyOpenHelper implements SupportSQLiteOpenHelper {
+ destinationFile.getAbsolutePath());
}
// Temporarily open intermediate database file using FrameworkSQLiteOpenHelper and dispatch
// the open pre-packaged callback. If it fails then intermediate file won't be copied making
// invoking pre-packaged callback a transactional operation.
dispatchOnOpenPrepackagedDatabase(intermediateFile, writable);
if (!intermediateFile.renameTo(destinationFile)) {
throw new IOException("Failed to move intermediate file ("
+ intermediateFile.getAbsolutePath() + ") to destination ("
+ destinationFile.getAbsolutePath() + ").");
}
}
private void dispatchOnOpenPrepackagedDatabase(File databaseFile, boolean writable) {
if (mDatabaseConfiguration == null
|| mDatabaseConfiguration.prepackagedDatabaseCallback == null) {
return;
}
SupportSQLiteOpenHelper helper = createFrameworkOpenHelper(databaseFile);
try {
SupportSQLiteDatabase db = writable ? helper.getWritableDatabase() :
helper.getReadableDatabase();
mDatabaseConfiguration.prepackagedDatabaseCallback.onOpenPrepackagedDatabase(db);
} finally {
// Close the db and let Room re-open it through a normal path
helper.close();
}
}
private SupportSQLiteOpenHelper createFrameworkOpenHelper(File databaseFile) {
String databaseName = databaseFile.getName();
int version;
try {
version = DBUtil.readVersion(databaseFile);
} catch (IOException e) {
throw new RuntimeException("Malformed database file, unable to read version.", e);
}
FrameworkSQLiteOpenHelperFactory factory = new FrameworkSQLiteOpenHelperFactory();
Configuration configuration = Configuration.builder(mContext)
.name(databaseName)
.callback(new Callback(version) {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
}
@Override
public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion,
int newVersion) {
}
})
.build();
return factory.create(configuration);
}
}

@ -21,6 +21,8 @@ import androidx.annotation.Nullable;
import androidx.sqlite.db.SupportSQLiteOpenHelper;
import java.io.File;
import java.io.InputStream;
import java.util.concurrent.Callable;
/**
* Implementation of {@link SupportSQLiteOpenHelper.Factory} that creates
@ -32,24 +34,30 @@ class SQLiteCopyOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
private final String mCopyFromAssetPath;
@Nullable
private final File mCopyFromFile;
@Nullable
private final Callable<InputStream> mCopyFromInputStream;
@NonNull
private final SupportSQLiteOpenHelper.Factory mDelegate;
SQLiteCopyOpenHelperFactory(
@Nullable String copyFromAssetPath,
@Nullable File copyFromFile,
@Nullable Callable<InputStream> copyFromInputStream,
@NonNull SupportSQLiteOpenHelper.Factory factory) {
mCopyFromAssetPath = copyFromAssetPath;
mCopyFromFile = copyFromFile;
mCopyFromInputStream = copyFromInputStream;
mDelegate = factory;
}
@NonNull
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new SQLiteCopyOpenHelper(
configuration.context,
mCopyFromAssetPath,
mCopyFromFile,
mCopyFromInputStream,
configuration.callback.version,
mDelegate.create(configuration));
}

@ -38,8 +38,10 @@ class TransactionExecutor implements Executor {
mExecutor = executor;
}
@Override
public synchronized void execute(final Runnable command) {
mTasks.offer(new Runnable() {
@Override
public void run() {
try {
command.run();

@ -57,7 +57,7 @@
* <pre>
* // File: Song.java
* {@literal @}Entity
* public class User {
* public class Song {
* {@literal @}PrimaryKey
* private int id;
* private String name;
@ -82,7 +82,7 @@
* // File: MusicDatabase.java
* {@literal @}Database(entities = {Song.java})
* public abstract class MusicDatabase extends RoomDatabase {
* public abstract SongDao userDao();
* public abstract SongDao songDao();
* }
* </pre>
* You can create an instance of {@code MusicDatabase} as follows:
@ -118,7 +118,7 @@
* String header;
* }
* // DAO
* {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM user")
* {@literal @}Query("SELECT id, name || '-' || release_year AS header FROM song")
* public IdAndSongHeader[] loadSongHeaders();
* </pre>
* If there is a mismatch between the query result and the POJO, Room will print a warning during

@ -43,7 +43,7 @@ import java.util.concurrent.locks.ReentrantLock;
* Thread locking within the same JVM process is done via a map of String key to ReentrantLock
* objects.
* <li>
* Multi-process locking is done via a dummy file whose name contains the key and FileLock
* Multi-process locking is done via a lock file whose name contains the key and FileLock
* objects.
*
* @hide

@ -18,9 +18,14 @@ package androidx.room.util;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.os.Build;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import java.util.Arrays;
/**
* Cursor utilities for Room
@ -84,11 +89,15 @@ public class CursorUtil {
* @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);
int index = c.getColumnIndex(name);
if (index >= 0) {
return index;
}
return c.getColumnIndex("`" + name + "`");
index = c.getColumnIndex("`" + name + "`");
if (index >= 0) {
return index;
}
return findColumnIndexBySuffix(c, name);
}
/**
@ -101,11 +110,56 @@ public class CursorUtil {
* @throws IllegalArgumentException if the column does not exist.
*/
public static int getColumnIndexOrThrow(@NonNull Cursor c, @NonNull String name) {
final int index = c.getColumnIndex(name);
final int index = getColumnIndex(c, name);
if (index >= 0) {
return index;
}
return c.getColumnIndexOrThrow("`" + name + "`");
String availableColumns = "";
try {
availableColumns = Arrays.toString(c.getColumnNames());
} catch (Exception e) {
Log.d("RoomCursorUtil", "Cannot collect column names for debug purposes", e);
}
throw new IllegalArgumentException("column '" + name
+ "' does not exist. Available columns: " + availableColumns);
}
/**
* Finds a column by name by appending `.` in front of it and checking by suffix match.
* Also checks for the version wrapped with `` (backticks).
* workaround for b/157261134 for API levels 25 and below
*
* e.g. "foo" will match "any.foo" and "`any.foo`"
*/
private static int findColumnIndexBySuffix(@NonNull Cursor cursor, @NonNull String name) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1) {
// we need this workaround only on APIs < 26. So just return not found on newer APIs
return -1;
}
if (name.length() == 0) {
return -1;
}
final String[] columnNames = cursor.getColumnNames();
return findColumnIndexBySuffix(columnNames, name);
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
static int findColumnIndexBySuffix(String[] columnNames, String name) {
String dotSuffix = "." + name;
String backtickSuffix = "." + name + "`";
for (int index = 0; index < columnNames.length; index++) {
String columnName = columnNames[index];
// do not check if column name is not long enough. 1 char for table name, 1 char for '.'
if (columnName.length() >= name.length() + 2) {
if (columnName.endsWith(dotSuffix)) {
return index;
} else if (columnName.charAt(0) == '`'
&& columnName.endsWith(backtickSuffix)) {
return index;
}
}
}
return -1;
}
private CursorUtil() {

@ -34,7 +34,7 @@ import java.util.Set;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class FtsTableInfo {
public final class FtsTableInfo {
// A set of valid FTS Options
private static final String[] FTS_OPTIONS = new String[] {
@ -192,7 +192,7 @@ public class FtsTableInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof FtsTableInfo)) return false;
FtsTableInfo that = (FtsTableInfo) o;

@ -52,8 +52,8 @@ import java.util.TreeMap;
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
"SimplifiableIfStatement"})
// if you change this class, you must change TableInfoWriter.kt
public class TableInfo {
// if you change this class, you must change TableInfoValidationWriter.kt
public final class TableInfo {
/**
* Identifies from where the info object was created.
@ -118,7 +118,7 @@ public class TableInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof TableInfo)) return false;
TableInfo tableInfo = (TableInfo) o;
@ -340,7 +340,7 @@ public class TableInfo {
* Holds the information about a database column.
*/
@SuppressWarnings("WeakerAccess")
public static class Column {
public static final class Column {
/**
* The column name.
*/
@ -439,7 +439,7 @@ public class TableInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof Column)) return false;
Column column = (Column) o;
if (Build.VERSION.SDK_INT >= 20) {
@ -512,7 +512,7 @@ public class TableInfo {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class ForeignKey {
public static final class ForeignKey {
@NonNull
public final String referenceTable;
@NonNull
@ -537,7 +537,7 @@ public class TableInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof ForeignKey)) return false;
ForeignKey that = (ForeignKey) o;
@ -608,7 +608,7 @@ public class TableInfo {
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public static class Index {
public static final class Index {
// should match the value in Index.kt
public static final String DEFAULT_PREFIX = "index_";
public final String name;
@ -624,7 +624,7 @@ public class TableInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof Index)) return false;
Index index = (Index) o;
if (unique != index.unique) {

@ -31,7 +31,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
public class ViewInfo {
public final class ViewInfo {
/**
* The view name
@ -51,7 +51,7 @@ public class ViewInfo {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!(o instanceof ViewInfo)) 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);

Loading…
Cancel
Save