Add InvalidationTracker and RoomDataBase.Builder option.
Test: AutoClosingRoomOpenHelperTest
Bug: 137223459
Relnote: This last piece enables the auto-closing database feature.
Change-Id: Ife5cf28937b4142aa8e5ace8d5c67446d66c2f85
diff --git a/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
new file mode 100644
index 0000000..a2d0f71
--- /dev/null
+++ b/room/integration-tests/testapp/src/androidTest/java/androidx/room/integration/testapp/test/AutoClosingRoomOpenHelperTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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.integration.testapp.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.testing.TestLifecycleOwner;
+import androidx.room.InvalidationTracker;
+import androidx.room.Room;
+import androidx.room.RoomDatabase;
+import androidx.room.integration.testapp.TestDatabase;
+import androidx.room.integration.testapp.dao.UserDao;
+import androidx.room.integration.testapp.vo.User;
+import androidx.room.util.SneakyThrow;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.MediumTest;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class AutoClosingRoomOpenHelperTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+ private UserDao mUserDao;
+ private TestDatabase mDb;
+ private final DatabaseCallbackTest.TestDatabaseCallback mCallback =
+ new DatabaseCallbackTest.TestDatabaseCallback();
+
+ @Before
+ public void createDb() throws TimeoutException, InterruptedException {
+ Context context = ApplicationProvider.getApplicationContext();
+ mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
+ .addCallback(mCallback).build();
+ mUserDao = mDb.getUserDao();
+ drain();
+ }
+
+ @After
+ public void cleanUp() throws Exception {
+ mDb.clearAllTables();
+ }
+
+ @Test
+ @MediumTest
+ public void inactiveConnection_shouldAutoClose() throws Exception {
+ assertFalse(mCallback.mOpened);
+ User user = TestUtil.createUser(1);
+ user.setName("bob");
+ mUserDao.insert(user);
+ assertTrue(mCallback.mOpened);
+ assertTrue(mDb.isOpen());
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+
+ User readUser = mUserDao.load(1);
+ assertEquals(readUser.getName(), user.getName());
+ }
+
+ @Test
+ @MediumTest
+ public void slowTransaction_keepsDbAlive() throws Exception {
+ assertFalse(mCallback.mOpened);
+
+ User user = TestUtil.createUser(1);
+ user.setName("bob");
+ mUserDao.insert(user);
+ assertTrue(mCallback.mOpened);
+ Thread.sleep(30);
+ mUserDao.load(1);
+ assertTrue(mDb.isOpen());
+
+ mDb.runInTransaction(
+ () -> {
+ try {
+ Thread.sleep(100);
+ assertTrue(mDb.isOpen());
+ } catch (InterruptedException e) {
+ SneakyThrow.reThrow(e);
+ }
+ }
+ );
+
+ assertTrue(mDb.isOpen());
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+ }
+
+ @Test
+ @MediumTest
+ public void slowCursorClosing_keepsDbAlive() throws Exception {
+ assertFalse(mCallback.mOpened);
+ User user = TestUtil.createUser(1);
+ user.setName("bob");
+ mUserDao.insert(user);
+ assertTrue(mCallback.mOpened);
+ mUserDao.load(1);
+ assertTrue(mDb.isOpen());
+
+ Cursor cursor = mDb.query("select * from user", null);
+
+ assertTrue(mDb.isOpen());
+ Thread.sleep(100);
+ assertTrue(mDb.isOpen());
+ cursor.close();
+
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+ }
+
+ @Test
+ @MediumTest
+ public void autoClosedConnection_canReopen() throws Exception {
+ User user1 = TestUtil.createUser(1);
+ user1.setName("bob");
+ mUserDao.insert(user1);
+
+ assertTrue(mDb.isOpen());
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+
+ User user2 = TestUtil.createUser(2);
+ user2.setName("bob2");
+ mUserDao.insert(user2);
+ assertTrue(mDb.isOpen());
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+ }
+
+ @Test
+ @MediumTest
+ public void liveDataTriggers_shouldApplyOnReopen() throws Exception {
+ LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(1);
+
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ final TestObserver<Boolean> observer = new AutoClosingRoomOpenHelperTest
+ .MyTestObserver<>();
+ TestUtil.observeOnMainThread(adminLiveData, lifecycleOwner, observer);
+ assertNull(observer.get());
+
+ User user = TestUtil.createUser(1);
+ user.setAdmin(true);
+ mUserDao.insert(user);
+
+ assertNotNull(observer.get());
+ assertTrue(observer.get());
+
+ user.setAdmin(false);
+ mUserDao.insertOrReplace(user);
+ assertNotNull(observer.get());
+ assertFalse(observer.get());
+
+ Thread.sleep(100);
+ assertFalse(mDb.isOpen());
+
+ user.setAdmin(true);
+ mUserDao.insertOrReplace(user);
+ assertNotNull(observer.get());
+ assertTrue(observer.get());
+ }
+
+ @Test
+ @MediumTest
+ public void testCanExecSqlInCallback() throws Exception {
+ Context context = ApplicationProvider.getApplicationContext();
+
+ mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS)
+ .addCallback(new ExecSqlInCallback())
+ .build();
+
+ mDb.getUserDao().insert(TestUtil.createUser(1));
+ }
+
+ @Test
+ @MediumTest
+ public void invalidationObserver_isCalledOnEachInvalidation()
+ throws TimeoutException, InterruptedException {
+ AtomicInteger invalidationCount = new AtomicInteger(0);
+
+ UserTableObserver userTableObserver =
+ new UserTableObserver(invalidationCount::getAndIncrement);
+
+ mDb.getInvalidationTracker().addObserver(userTableObserver);
+
+ mUserDao.insert(TestUtil.createUser(1));
+
+ drain();
+ assertEquals(1, invalidationCount.get());
+
+ User user1 = TestUtil.createUser(1);
+ user1.setAge(123);
+ mUserDao.insertOrReplace(user1);
+
+ drain();
+ assertEquals(2, invalidationCount.get());
+
+ Thread.sleep(15);
+ assertFalse(mDb.isOpen());
+
+ mUserDao.insert(TestUtil.createUser(2));
+
+ drain();
+ assertEquals(3, invalidationCount.get());
+ }
+
+ @Test
+ @MediumTest
+ public void invalidationObserver_canRequeryDb() throws TimeoutException, InterruptedException {
+ Context context = ApplicationProvider.getApplicationContext();
+
+ context.deleteDatabase("testDb");
+ mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ // create contention for callback
+ .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
+ .addCallback(mCallback).build();
+
+ AtomicInteger userCount = new AtomicInteger(0);
+
+ UserTableObserver userTableObserver = new UserTableObserver(
+ () -> userCount.set(mUserDao.count()));
+
+ mDb.getInvalidationTracker().addObserver(userTableObserver);
+
+ mDb.getUserDao().insert(TestUtil.createUser(1));
+ mDb.getUserDao().insert(TestUtil.createUser(2));
+ mDb.getUserDao().insert(TestUtil.createUser(3));
+ mDb.getUserDao().insert(TestUtil.createUser(4));
+ mDb.getUserDao().insert(TestUtil.createUser(5));
+ mDb.getUserDao().insert(TestUtil.createUser(6));
+ mDb.getUserDao().insert(TestUtil.createUser(7));
+
+ drain();
+ assertEquals(7, userCount.get());
+ }
+
+ @Test
+ @MediumTest
+ public void invalidationObserver_notifiedByTableName() throws TimeoutException,
+ InterruptedException {
+ Context context = ApplicationProvider.getApplicationContext();
+
+ context.deleteDatabase("testDb");
+ mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb")
+ // create contention for callback
+ .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS)
+ .addCallback(mCallback).build();
+
+ AtomicInteger invalidationCount = new AtomicInteger(0);
+
+ UserTableObserver userTableObserver =
+ new UserTableObserver(invalidationCount::getAndIncrement);
+
+ mDb.getInvalidationTracker().addObserver(userTableObserver);
+
+
+ mDb.getUserDao().insert(TestUtil.createUser(1));
+
+ drain();
+ assertEquals(1, invalidationCount.get());
+
+ Thread.sleep(100); // Let db auto close
+
+ mDb.getInvalidationTracker().notifyObserversByTableNames("user");
+
+ drain();
+ assertEquals(2, invalidationCount.get());
+
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+
+ private class MyTestObserver<T> extends TestObserver<T> {
+ @Override
+ protected void drain() throws TimeoutException, InterruptedException {
+ AutoClosingRoomOpenHelperTest.this.drain();
+ }
+ }
+
+ private static class ExecSqlInCallback extends RoomDatabase.Callback {
+ @Override
+ public void onOpen(@NonNull SupportSQLiteDatabase db) {
+ db.query("select * from user").close();
+ }
+ }
+
+ private static class UserTableObserver extends InvalidationTracker.Observer {
+
+ private final Runnable mInvalidationCallback;
+
+ UserTableObserver(Runnable invalidationCallback) {
+ super("user");
+ mInvalidationCallback = invalidationCallback;
+ }
+
+ @Override
+ public void onInvalidated(@NonNull @NotNull Set<String> tables) {
+ mInvalidationCallback.run();
+ }
+ }
+}
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
index 87e9c17..fb88f3b 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoCloserTest.kt
@@ -79,6 +79,7 @@
autoCloseExecutor
).also {
it.init(delegateOpenHelper)
+ it.setAutoCloseCallback { }
}
}
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt
index febc98c..9603a47 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperFactoryTest.kt
@@ -45,6 +45,7 @@
return AutoClosingRoomOpenHelperFactory(
delegateOpenHelperFactory,
AutoCloser(timeoutMillis, TimeUnit.MILLISECONDS, Executors.newSingleThreadExecutor())
+ .also { it.mOnAutoCloseCallback = Runnable {} }
)
}
diff --git a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt
index c601d2e..d673e4c 100644
--- a/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt
+++ b/room/runtime/src/androidTest/java/androidx/room/AutoClosingRoomOpenHelperTest.kt
@@ -69,7 +69,10 @@
return AutoClosingRoomOpenHelper(
delegateOpenHelper,
- AutoCloser(timeoutMillis, TimeUnit.MILLISECONDS, autoCloseExecutor)
+ AutoCloser(timeoutMillis, TimeUnit.MILLISECONDS, autoCloseExecutor).apply {
+ init(delegateOpenHelper)
+ setAutoCloseCallback { }
+ }
)
}
diff --git a/room/runtime/src/main/java/androidx/room/AutoCloser.java b/room/runtime/src/main/java/androidx/room/AutoCloser.java
index eda2363..45b0921 100644
--- a/room/runtime/src/main/java/androidx/room/AutoCloser.java
+++ b/room/runtime/src/main/java/androidx/room/AutoCloser.java
@@ -50,6 +50,10 @@
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();
@@ -102,6 +106,15 @@
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) {
try {
mDelegateDatabase.close();
@@ -138,8 +151,9 @@
*/
public void init(@NonNull SupportSQLiteOpenHelper delegateOpenHelper) {
if (mDelegateOpenHelper != null) {
- Log.e(Room.LOG_TAG, "AutoCloser initialized multiple times. This is probably a bug in"
- + " the room code.");
+ 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;
@@ -194,8 +208,9 @@
if (mDelegateOpenHelper != null) {
mDelegateDatabase = mDelegateOpenHelper.getWritableDatabase();
} else {
- throw new IllegalStateException("AutoCloser has not beeninitialized. This "
- + "shouldn't happen, but if it does it means there's a bug in our code");
+ 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;
@@ -253,4 +268,14 @@
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;
+ }
}
diff --git a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
index 927d6ae..7dda1da 100644
--- a/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
+++ b/room/runtime/src/main/java/androidx/room/InvalidationTracker.java
@@ -90,6 +90,9 @@
@NonNull
private Map<String, Set<String>> mViewTables;
+ @Nullable
+ AutoCloser mAutoCloser = null;
+
@SuppressWarnings("WeakerAccess") /* synthetic access */
final RoomDatabase mDatabase;
@@ -161,6 +164,23 @@
}
/**
+ * 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>
* You should never call this method, it is called by the generated code.
@@ -183,6 +203,13 @@
}
}
+ void onAutoCloseCallback() {
+ synchronized (this) {
+ mInitialized = false;
+ mObservedTableTracker.resetTriggerState();
+ }
+ }
+
void startMultiInstanceInvalidation(Context context, String name) {
mMultiInstanceInvalidationClient = new MultiInstanceInvalidationClient(context, name, this,
mDatabase.getQueryExecutor());
@@ -415,6 +442,10 @@
exception);
} finally {
closeLock.unlock();
+
+ if (mAutoCloser != null) {
+ mAutoCloser.decrementCountAndScheduleClose();
+ }
}
if (invalidatedTableIds != null && !invalidatedTableIds.isEmpty()) {
synchronized (mObserverMap) {
@@ -455,6 +486,13 @@
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);
}
}
@@ -467,6 +505,10 @@
@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();
}
@@ -802,6 +844,17 @@
}
/**
+ * 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.
*
* @return int[] An int array where the index for each tableId has the action for that
diff --git a/room/runtime/src/main/java/androidx/room/RoomDatabase.java b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
index 0fa15d8..8bbedd4 100644
--- a/room/runtime/src/main/java/androidx/room/RoomDatabase.java
+++ b/room/runtime/src/main/java/androidx/room/RoomDatabase.java
@@ -54,6 +54,7 @@
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -99,6 +100,9 @@
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.
@@ -187,6 +191,15 @@
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;
@@ -447,6 +460,18 @@
@Deprecated
public void beginTransaction() {
assertNotMainThread();
+ if (mAutoCloser == null) {
+ internalBeginTransaction();
+ } else {
+ mAutoCloser.executeRefCountingFunction(db -> {
+ internalBeginTransaction();
+ return null;
+ });
+ }
+ }
+
+ private void internalBeginTransaction() {
+ assertNotMainThread();
SupportSQLiteDatabase database = mOpenHelper.getWritableDatabase();
mInvalidationTracker.syncTriggers(database);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
@@ -464,6 +489,17 @@
*/
@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
@@ -658,6 +694,10 @@
private boolean mMultiInstanceInvalidation;
private boolean mRequireMigration;
private boolean mAllowDestructiveMigrationOnDowngrade;
+
+ private long mAutoCloseTimeout = -1L;
+ private TimeUnit mAutoCloseTimeUnit;
+
/**
* Migrations, mapped by from-to pairs.
*/
@@ -1155,6 +1195,44 @@
}
/**
+ * 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.
+ *
+ * @param autoCloseTimeout the amount of time after the last usage before closing the
+ * database
+ * @param autoCloseTimeUnit the timeunit for autoCloseTimeout.
+ * @return This {@link Builder} instance
+ *
+ * @hide until it's ready for use
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public Builder<T> setAutoCloseTimeout(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>
* By default, all RoomDatabases use in memory storage for TEMP tables and enables recursive
@@ -1198,12 +1276,26 @@
SupportSQLiteOpenHelper.Factory factory;
+ AutoCloser autoCloser = null;
+
if (mFactory == null) {
factory = new FrameworkSQLiteOpenHelperFactory();
} else {
factory = mFactory;
}
+ if (mAutoCloseTimeout > 0) {
+ if (mName == null) {
+ throw new IllegalArgumentException("Cannot create auto-closing database for "
+ + "an in-memory database.");
+ }
+
+ autoCloser = new AutoCloser(mAutoCloseTimeout, mAutoCloseTimeUnit,
+ mTransactionExecutor);
+
+ factory = new AutoClosingRoomOpenHelperFactory(factory, autoCloser);
+ }
+
if (mCopyFromAssetPath != null
|| mCopyFromFile != null
|| mCopyFromInputStream != null) {