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) {