Merge cherrypicks of ['android-review.googlesource.com/2275508', 'android-review.googlesource.com/2487133', 'android-review.googlesource.com/2360579', 'android-review.googlesource.com/2402712', 'android-review.googlesource.com/2458711', 'android-review.googlesource.com/2472624', 'android-review.googlesource.com/2479955'] into androidx-sqlite-release.

Change-Id: Id4e73a4c8d1e410c05ad19734bdd43f87fabd608
diff --git a/libraryversions.toml b/libraryversions.toml
index 234f1ad..b5c6a70 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -95,7 +95,7 @@
 RECYCLERVIEW_SELECTION = "1.2.0-alpha02"
 REMOTECALLBACK = "1.0.0-alpha02"
 RESOURCEINSPECTION = "1.1.0-alpha01"
-ROOM = "2.5.0"
+ROOM = "2.5.1"
 SAVEDSTATE = "1.3.0-alpha01"
 SECURITY = "1.1.0-alpha04"
 SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
@@ -108,7 +108,7 @@
 SLICE_BUILDERS_KTX = "1.0.0-alpha08"
 SLICE_REMOTECALLBACK = "1.0.0-alpha01"
 SLIDINGPANELAYOUT = "1.3.0-alpha01"
-SQLITE = "2.3.0"
+SQLITE = "2.3.1"
 SQLITE_INSPECTOR = "2.1.0-alpha01"
 STARTUP = "1.2.0-alpha02"
 SWIPEREFRESHLAYOUT = "1.2.0-alpha01"
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt
index b3cb2b9..589d690 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/TestDatabase.kt
@@ -18,7 +18,9 @@
 
 import androidx.room.Database
 import androidx.room.RoomDatabase
+import androidx.room.androidx.room.integration.kotlintestapp.dao.CounterDao
 import androidx.room.androidx.room.integration.kotlintestapp.dao.UsersDao
+import androidx.room.androidx.room.integration.kotlintestapp.vo.Counter
 import androidx.room.androidx.room.integration.kotlintestapp.vo.User
 import androidx.room.integration.kotlintestapp.dao.AbstractDao
 import androidx.room.integration.kotlintestapp.dao.BooksDao
@@ -37,7 +39,7 @@
     entities = [
         Book::class, Author::class, Publisher::class, BookAuthor::class,
         NoArgClass::class, DataClassFromDependency::class, JavaEntity::class,
-        EntityWithJavaPojoList::class, User::class
+        EntityWithJavaPojoList::class, User::class, Counter::class
     ],
     version = 1,
     exportSchema = false
@@ -53,4 +55,6 @@
     abstract fun dependencyDao(): DependencyDao
 
     abstract fun abstractDao(): AbstractDao
+
+    abstract fun counterDao(): CounterDao
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
index d53132c..7ec25bd 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/BooksDao.kt
@@ -136,6 +136,9 @@
     @Query("SELECT * FROM book")
     suspend fun getBooksSuspend(): List<Book>
 
+    @Query("SELECT * FROM publisher")
+    suspend fun getPublishersSuspend(): List<Publisher>
+
     @Query("UPDATE book SET salesCnt = salesCnt + 1 WHERE bookId = :bookId")
     fun increaseBookSales(bookId: String)
 
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/CounterDao.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/CounterDao.kt
new file mode 100644
index 0000000..62e7709
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/dao/CounterDao.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2022 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.androidx.room.integration.kotlintestapp.dao
+
+import androidx.room.Dao
+import androidx.room.Query
+import androidx.room.Upsert
+import androidx.room.androidx.room.integration.kotlintestapp.vo.Counter
+
+@Dao
+interface CounterDao {
+    @Upsert
+    suspend fun upsert(c: Counter)
+
+    @Query("SELECT * FROM Counter WHERE id = :id")
+    suspend fun getCounter(id: Long): Counter
+}
\ No newline at end of file
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index 48efc91..34f72b7 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -16,10 +16,15 @@
 
 package androidx.room.integration.kotlintestapp.test
 
+import android.content.Context
 import android.os.Build
+import android.os.StrictMode
+import android.os.StrictMode.ThreadPolicy
 import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.TaskExecutor
 import androidx.room.Room
 import androidx.room.RoomDatabase
+import androidx.room.androidx.room.integration.kotlintestapp.vo.Counter
 import androidx.room.integration.kotlintestapp.NewThreadDispatcher
 import androidx.room.integration.kotlintestapp.TestDatabase
 import androidx.room.integration.kotlintestapp.vo.Book
@@ -30,10 +35,25 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.io.IOException
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
+import kotlin.coroutines.intrinsics.intercepted
+import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
+import kotlin.coroutines.resume
+import kotlinx.coroutines.DelicateCoroutinesApi
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ObsoleteCoroutinesApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Runnable
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.async
 import kotlinx.coroutines.cancelAndJoin
 import kotlinx.coroutines.coroutineScope
@@ -41,18 +61,13 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.newSingleThreadContext
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assert.fail
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.io.IOException
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicInteger
-import kotlinx.coroutines.DelicateCoroutinesApi
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -140,6 +155,57 @@
     }
 
     @Test
+    fun allBookSuspend_autoClose() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        context.deleteDatabase("autoClose.db")
+        val db = Room.databaseBuilder(
+            context = context,
+            klass = TestDatabase::class.java,
+            name = "test.db"
+        ).setAutoCloseTimeout(10, TimeUnit.MILLISECONDS).build()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            StrictMode.setThreadPolicy(
+                ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .penaltyDeath()
+                    .build()
+            )
+            runBlocking {
+                db.booksDao().getBooksSuspend()
+                delay(100) // let db auto-close
+                db.booksDao().getBooksSuspend()
+            }
+        }
+    }
+
+    @Test
+    fun allBookSuspend_closed() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        context.deleteDatabase("autoClose.db")
+        val db = Room.databaseBuilder(
+            context = context,
+            klass = TestDatabase::class.java,
+            name = "test.db"
+        ).build()
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            StrictMode.setThreadPolicy(
+                ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .penaltyDeath()
+                    .build()
+            )
+            runBlocking {
+                // Opens DB, isOpen && inTransaction check should not cause violation
+                db.booksDao().getBooksSuspend()
+                // DB is open, isOpen && inTransaction check should not cause violation
+                db.booksDao().getBooksSuspend()
+            }
+        }
+    }
+
+    @Test
     @Suppress("DEPRECATION")
     fun suspendingBlock_beginEndTransaction() {
         runBlocking {
@@ -158,9 +224,7 @@
                 database.endTransaction()
             }
         }
-        runBlocking {
-            assertThat(booksDao.getBooksSuspend()).isEqualTo(listOf(TestUtil.BOOK_2))
-        }
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
     }
 
     @Test
@@ -182,9 +246,7 @@
                 database.endTransaction()
             }
         }
-        runBlocking {
-            assertThat(booksDao.getBooksSuspend()).isEqualTo(listOf(TestUtil.BOOK_2))
-        }
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
     }
 
     @Test
@@ -206,9 +268,7 @@
                 database.endTransaction()
             }
         }
-        runBlocking(NewThreadDispatcher()) {
-            assertThat(booksDao.getBooksSuspend()).isEqualTo(listOf(TestUtil.BOOK_2))
-        }
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
     }
 
     @Test
@@ -256,10 +316,25 @@
                 booksDao.deleteUnsoldBooks()
             }
         }
-        runBlocking(NewThreadDispatcher()) {
-            assertThat(booksDao.getBooksSuspend())
-                .isEqualTo(listOf(TestUtil.BOOK_2))
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
+    }
+
+    @Test
+    fun withTransaction_withContext_newThreadDispatcher() {
+        runBlocking {
+            withContext(NewThreadDispatcher()) {
+                database.withTransaction {
+                    booksDao.insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                    booksDao.insertBookSuspend(TestUtil.BOOK_1.copy(salesCnt = 0))
+                    booksDao.insertBookSuspend(TestUtil.BOOK_2)
+                    booksDao.deleteUnsoldBooks()
+                }
+            }
         }
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
     }
 
     @Test
@@ -275,10 +350,7 @@
                 booksDao.deleteUnsoldBooks()
             }
         }
-        runBlocking(NewThreadDispatcher()) {
-            assertThat(booksDao.getBooksSuspend())
-                .isEqualTo(listOf(TestUtil.BOOK_2))
-        }
+        assertThat(booksDao.allBooks).isEqualTo(listOf(TestUtil.BOOK_2))
     }
 
     @Test
@@ -301,6 +373,31 @@
     }
 
     @Test
+    fun withTransaction_contextSwitch_exception() {
+        runBlocking {
+            try {
+                database.withTransaction {
+                    booksDao.insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                    withContext(Dispatchers.IO) {
+                        booksDao.insertBookSuspend(TestUtil.BOOK_1.copy(salesCnt = 0))
+                        booksDao.insertBookSuspend(TestUtil.BOOK_2)
+                    }
+                    booksDao.deleteUnsoldBooks()
+                    throw IOException("Boom!")
+                }
+            } catch (ex: IOException) {
+                assertThat(ex).hasMessageThat()
+                    .contains("Boom")
+            }
+            assertThat(booksDao.getPublishersSuspend()).isEmpty()
+            assertThat(booksDao.getBooksSuspend()).isEmpty()
+        }
+    }
+
+    @Test
     fun withTransaction_exception() {
         runBlocking {
             database.withTransaction {
@@ -314,6 +411,7 @@
             try {
                 database.withTransaction {
                     booksDao.insertBookSuspend(TestUtil.BOOK_2)
+                    booksDao.insertBookSuspend(TestUtil.BOOK_3)
                     throw IOException("Boom!")
                 }
                 @Suppress("UNREACHABLE_CODE")
@@ -369,8 +467,8 @@
                 }
             }
 
-            assertThat(booksDao.getBooksSuspend())
-                .isEqualTo(emptyList<Book>())
+            assertThat(booksDao.getPublishersSuspend()).isEmpty()
+            assertThat(booksDao.getBooksSuspend()).isEmpty()
         }
     }
 
@@ -464,6 +562,7 @@
 
     @Test
     fun withTransaction_cancelCoroutine() {
+
         runBlocking {
             booksDao.insertPublisherSuspend(
                 TestUtil.PUBLISHER.publisherId,
@@ -496,6 +595,42 @@
     }
 
     @Test
+    fun withTransaction_busyExecutor_cancelCoroutine() {
+        val executorService = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executorService)
+            .build()
+
+        // Simulate a busy executor, no thread to acquire for transaction.
+        val busyLatch = CountDownLatch(1)
+        executorService.execute {
+            busyLatch.await()
+        }
+        runBlocking {
+            val startedRunning = CountDownLatch(1)
+            val job = launch(Dispatchers.IO) {
+                startedRunning.countDown()
+                delay(200) // yield and delay to queue the runnable in transaction executor
+                localDatabase.withTransaction {
+                    fail("Transaction block should have never run!")
+                }
+            }
+
+            assertThat(startedRunning.await(1, TimeUnit.SECONDS)).isTrue()
+            job.cancelAndJoin()
+        }
+
+        // free busy thread
+        busyLatch.countDown()
+        executorService.shutdown()
+        assertThat(executorService.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+
+        assertThat(localDatabase.booksDao().getPublishers()).isEmpty()
+    }
+
+    @Test
     fun withTransaction_blockingDaoMethods() {
         runBlocking {
             database.withTransaction {
@@ -655,15 +790,12 @@
             }
         }
 
-        runBlocking {
-            // as Set since insertion order is undefined
-            assertThat(booksDao.getBooksSuspend().toSet())
-                .isEqualTo(setOf(TestUtil.BOOK_1, TestUtil.BOOK_2))
-        }
+        // as Set since insertion order is undefined
+        assertThat(booksDao.allBooks.toSet())
+            .isEqualTo(setOf(TestUtil.BOOK_1, TestUtil.BOOK_2))
     }
 
     @Test
-    @ObsoleteCoroutinesApi
     @Suppress("DeferredResultUnused")
     fun withTransaction_multipleTransactions_multipleThreads() {
         runBlocking {
@@ -689,31 +821,16 @@
             }
         }
 
-        runBlocking {
-            // as Set since insertion order is undefined
-            assertThat(booksDao.getBooksSuspend().toSet())
-                .isEqualTo(setOf(TestUtil.BOOK_1, TestUtil.BOOK_2))
-        }
+        // as Set since insertion order is undefined
+        assertThat(booksDao.allBooks.toSet())
+            .isEqualTo(setOf(TestUtil.BOOK_1, TestUtil.BOOK_2))
     }
 
     @Test
     @Suppress("DeferredResultUnused")
     fun withTransaction_multipleTransactions_verifyThreadUsage() {
         val busyThreadsCount = AtomicInteger()
-        // Executor wrapper that counts threads that are busy executing commands.
-        class WrappedService(val delegate: ExecutorService) : ExecutorService by delegate {
-            override fun execute(command: Runnable) {
-                delegate.execute {
-                    busyThreadsCount.incrementAndGet()
-                    try {
-                        command.run()
-                    } finally {
-                        busyThreadsCount.decrementAndGet()
-                    }
-                }
-            }
-        }
-        val wrappedExecutor = WrappedService(Executors.newCachedThreadPool())
+        val wrappedExecutor = BusyCountingService(busyThreadsCount, Executors.newCachedThreadPool())
         val localDatabase = Room.inMemoryDatabaseBuilder(
             ApplicationProvider.getApplicationContext(), TestDatabase::class.java
         )
@@ -740,57 +857,67 @@
             }
         }
 
-        wrappedExecutor.awaitTermination(1, TimeUnit.SECONDS)
+        assertThat(busyThreadsCount.get()).isEqualTo(0)
+        wrappedExecutor.shutdown()
+        assertThat(wrappedExecutor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
     }
 
     @Test
     fun withTransaction_busyExecutor() {
+        val executorService = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executorService)
+            .build()
+
+        // Simulate a busy executor, no thread to acquire for transaction.
+        val busyLatch = CountDownLatch(1)
+        executorService.execute {
+            busyLatch.await()
+        }
         runBlocking {
-            val executorService = Executors.newSingleThreadExecutor()
-            val localDatabase = Room.inMemoryDatabaseBuilder(
-                ApplicationProvider.getApplicationContext(), TestDatabase::class.java
-            )
-                .setTransactionExecutor(executorService)
-                .build()
-
-            // Simulate a busy executor, no thread to acquire for transaction.
-            val busyLatch = CountDownLatch(1)
-            executorService.execute {
-                busyLatch.await()
-            }
-
             var asyncExecuted = false
-            val transactionLatch = CountDownLatch(1)
             val job = async(Dispatchers.IO) {
                 asyncExecuted = true
                 localDatabase.withTransaction {
-                    transactionLatch.countDown()
+                    booksDao.insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
                 }
             }
 
-            assertThat(transactionLatch.await(1000, TimeUnit.MILLISECONDS)).isFalse()
+            try {
+                withTimeout(1000) {
+                    job.join()
+                }
+                fail("A timeout should have occurred!")
+            } catch (_: TimeoutCancellationException) { }
             job.cancelAndJoin()
 
             assertThat(asyncExecuted).isTrue()
-
-            // free busy thread
-            busyLatch.countDown()
-            executorService.awaitTermination(1, TimeUnit.SECONDS)
         }
+        // free busy thread
+        busyLatch.countDown()
+        executorService.shutdown()
+        assertThat(executorService.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+
+        assertThat(booksDao.getPublishers()).isEmpty()
     }
 
     @Test
     fun withTransaction_shutdownExecutor() {
+        val executorService = Executors.newCachedThreadPool()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executorService)
+            .build()
+
+        executorService.shutdownNow()
+
         runBlocking {
-            val executorService = Executors.newCachedThreadPool()
-            val localDatabase = Room.inMemoryDatabaseBuilder(
-                ApplicationProvider.getApplicationContext(), TestDatabase::class.java
-            )
-                .setTransactionExecutor(executorService)
-                .build()
-
-            executorService.shutdownNow()
-
             try {
                 localDatabase.withTransaction {
                     fail("This coroutine should never run.")
@@ -801,22 +928,24 @@
                     .contains("Unable to acquire a thread to perform the database transaction")
             }
         }
+
+        executorService.shutdown()
+        assertThat(executorService.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
     }
 
     @Test
     fun withTransaction_databaseOpenError() {
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .addCallback(object : RoomDatabase.Callback() {
+                override fun onOpen(db: SupportSQLiteDatabase) {
+                    // this causes all transaction methods to throw, this can happen IRL
+                    throw RuntimeException("Error opening Database.")
+                }
+            })
+            .build()
         runBlocking {
-            val localDatabase = Room.inMemoryDatabaseBuilder(
-                ApplicationProvider.getApplicationContext(), TestDatabase::class.java
-            )
-                .addCallback(object : RoomDatabase.Callback() {
-                    override fun onOpen(db: SupportSQLiteDatabase) {
-                        // this causes all transaction methods to throw, this can happen IRL
-                        throw RuntimeException("Error opening Database.")
-                    }
-                })
-                .build()
-
             try {
                 localDatabase.withTransaction {
                     fail("This coroutine should never run.")
@@ -830,41 +959,40 @@
 
     @Test
     fun withTransaction_beginTransaction_error() {
-        runBlocking {
-            // delegate and delegate just so that we can throw in beginTransaction()
-            val localDatabase = Room.inMemoryDatabaseBuilder(
-                ApplicationProvider.getApplicationContext(), TestDatabase::class.java
-            )
-                .openHelperFactory(
-                    object : SupportSQLiteOpenHelper.Factory {
-                        val factoryDelegate = FrameworkSQLiteOpenHelperFactory()
-                        override fun create(
-                            configuration: SupportSQLiteOpenHelper.Configuration
-                        ): SupportSQLiteOpenHelper {
-                            val helperDelegate = factoryDelegate.create(configuration)
-                            return object : SupportSQLiteOpenHelper by helperDelegate {
-                                override val writableDatabase: SupportSQLiteDatabase
-                                    get() {
-                                        val databaseDelegate = helperDelegate.writableDatabase
-                                        return object : SupportSQLiteDatabase by databaseDelegate {
-                                            override fun beginTransaction() {
-                                                throw RuntimeException(
-                                                    "Error beginning transaction."
-                                                )
-                                            }
-                                            override fun beginTransactionNonExclusive() {
-                                                throw RuntimeException(
-                                                    "Error beginning transaction."
-                                                )
-                                            }
+        // delegate and delegate just so that we can throw in beginTransaction()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .openHelperFactory(
+                object : SupportSQLiteOpenHelper.Factory {
+                    val factoryDelegate = FrameworkSQLiteOpenHelperFactory()
+                    override fun create(
+                        configuration: SupportSQLiteOpenHelper.Configuration
+                    ): SupportSQLiteOpenHelper {
+                        val helperDelegate = factoryDelegate.create(configuration)
+                        return object : SupportSQLiteOpenHelper by helperDelegate {
+                            override val writableDatabase: SupportSQLiteDatabase
+                                get() {
+                                    val databaseDelegate = helperDelegate.writableDatabase
+                                    return object : SupportSQLiteDatabase by databaseDelegate {
+                                        override fun beginTransaction() {
+                                            throw RuntimeException(
+                                                "Error beginning transaction."
+                                            )
+                                        }
+                                        override fun beginTransactionNonExclusive() {
+                                            throw RuntimeException(
+                                                "Error beginning transaction."
+                                            )
                                         }
                                     }
-                            }
+                                }
                         }
                     }
-                )
-                .build()
-
+                }
+            )
+            .build()
+        runBlocking {
             try {
                 localDatabase.withTransaction {
                     fail("This coroutine should never run.")
@@ -1023,4 +1151,286 @@
         assertThat(booksDao.getBooksSuspend())
             .contains(addedBook)
     }
+
+    @Test
+    fun withTransaction_instantTaskExecutorRule() = runBlocking {
+        // Not the actual InstantTaskExecutorRule since this test class already uses
+        // CountingTaskExecutorRule but same behaviour.
+        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
+            override fun executeOnDiskIO(runnable: Runnable) {
+                runnable.run()
+            }
+
+            override fun postToMainThread(runnable: Runnable) {
+                runnable.run()
+            }
+
+            override fun isMainThread(): Boolean {
+                return false
+            }
+        })
+        database.withTransaction {
+            booksDao.insertPublisherSuspend(
+                TestUtil.PUBLISHER.publisherId,
+                TestUtil.PUBLISHER.name
+            )
+        }
+        assertThat(booksDao.getPublishers().size).isEqualTo(1)
+    }
+
+    @Test
+    fun withTransaction_singleExecutorDispatcher() {
+        val executor = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executor)
+            .build()
+        runBlocking {
+            withContext(executor.asCoroutineDispatcher()) {
+                localDatabase.withTransaction {
+                    localDatabase.booksDao().insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                }
+            }
+        }
+        assertThat(localDatabase.booksDao().getPublishers().size).isEqualTo(1)
+
+        executor.shutdown()
+        assertThat(executor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun withTransaction_reentrant_nested() {
+        val executor = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executor)
+            .build()
+        runBlocking {
+            withContext(executor.asCoroutineDispatcher()) {
+                localDatabase.withTransaction {
+                    localDatabase.booksDao().insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                    localDatabase.withTransaction {
+                        localDatabase.booksDao().insertBookSuspend(TestUtil.BOOK_1)
+                    }
+                }
+            }
+        }
+        assertThat(localDatabase.booksDao().getPublishers().size).isEqualTo(1)
+        assertThat(localDatabase.booksDao().allBooks.size).isEqualTo(1)
+
+        executor.shutdown()
+        assertThat(executor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun withTransaction_reentrant_nested_exception() {
+        val executor = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executor)
+            .build()
+        runBlocking {
+            withContext(executor.asCoroutineDispatcher()) {
+                localDatabase.withTransaction {
+                    localDatabase.booksDao().insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                    try {
+                        localDatabase.withTransaction {
+                            localDatabase.booksDao().insertBookSuspend(TestUtil.BOOK_1)
+                            throw IOException("Boom!")
+                        }
+                        @Suppress("UNREACHABLE_CODE")
+                        fail("An exception should have been thrown.")
+                    } catch (ex: IOException) {
+                        assertThat(ex).hasMessageThat()
+                            .contains("Boom")
+                    }
+                }
+            }
+        }
+        assertThat(localDatabase.booksDao().getPublishers()).isEmpty()
+        assertThat(localDatabase.booksDao().allBooks).isEmpty()
+
+        executor.shutdown()
+        assertThat(executor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun withTransaction_reentrant_nested_contextSwitch() {
+        val executor = Executors.newSingleThreadExecutor()
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executor)
+            .build()
+
+        runBlocking {
+            withContext(executor.asCoroutineDispatcher()) {
+                localDatabase.withTransaction {
+                    localDatabase.booksDao().insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                    withContext(Dispatchers.IO) {
+                        localDatabase.withTransaction {
+                            localDatabase.booksDao().insertBookSuspend(TestUtil.BOOK_1)
+                        }
+                    }
+                }
+            }
+        }
+        assertThat(localDatabase.booksDao().getPublishers().size).isEqualTo(1)
+        assertThat(localDatabase.booksDao().allBooks.size).isEqualTo(1)
+
+        executor.shutdown()
+        assertThat(executor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    fun withTransaction_reentrant_busyExecutor() {
+        val busyThreadsCount = AtomicInteger()
+        val executor =
+            BusyCountingService(busyThreadsCount, Executors.newFixedThreadPool(2))
+        val localDatabase = Room.inMemoryDatabaseBuilder(
+            ApplicationProvider.getApplicationContext(), TestDatabase::class.java
+        )
+            .setTransactionExecutor(executor)
+            .build()
+
+        // Grab one of the thread and simulate busy work
+        val busyLatch = CountDownLatch(1)
+        executor.execute {
+            busyLatch.await()
+        }
+
+        runBlocking {
+            // Using the other thread in the pool this will cause a reentrant situation
+            withContext(executor.asCoroutineDispatcher()) {
+                localDatabase.withTransaction {
+                    val transactionThread = Thread.currentThread()
+                    // Suspend transaction thread while freeing the busy thread from the pool
+                    withContext(Dispatchers.IO) {
+                        busyLatch.countDown()
+                        delay(200)
+                        // Only one thread is busy, the transaction thread
+                        assertThat(busyThreadsCount.get()).isEqualTo(1)
+                    }
+                    // Resume in the transaction thread, the recently free thread in the pool that
+                    // is not in a transaction should not be used.
+                    assertThat(Thread.currentThread()).isEqualTo(transactionThread)
+                    localDatabase.booksDao().insertPublisherSuspend(
+                        TestUtil.PUBLISHER.publisherId,
+                        TestUtil.PUBLISHER.name
+                    )
+                }
+            }
+        }
+
+        assertThat(localDatabase.booksDao().getPublishers().size).isEqualTo(1)
+
+        executor.shutdown()
+        assertThat(executor.awaitTermination(1, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @Test
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun withTransaction_runTest() {
+        runTest {
+            database.withTransaction {
+                booksDao.insertPublisherSuspend(
+                    TestUtil.PUBLISHER.publisherId,
+                    TestUtil.PUBLISHER.name
+                )
+                booksDao.insertBookSuspend(TestUtil.BOOK_1.copy(salesCnt = 0))
+                booksDao.insertBookSuspend(TestUtil.BOOK_2)
+                booksDao.deleteUnsoldBooks()
+            }
+            assertThat(booksDao.getBooksSuspend())
+                .isEqualTo(listOf(TestUtil.BOOK_2))
+        }
+    }
+
+    @Test
+    fun withTransaction_stress_testMutation() {
+        val output = mutableListOf<String>()
+        runBlocking {
+            repeat(5000) { count ->
+                database.withTransaction {
+                    output.add("$count")
+                    suspendHere()
+                    output.add("$count")
+                }
+            }
+        }
+
+        val expectedOutput = buildList {
+            repeat(5000) { count ->
+                add("$count")
+                add("$count")
+            }
+        }
+        assertThat(output).isEqualTo(expectedOutput)
+    }
+
+    @Test
+    fun withTransaction_stress_dbMutation() {
+        val context: Context = ApplicationProvider.getApplicationContext()
+        context.deleteDatabase("test_stress_dbMutation.db")
+        val db = Room.databaseBuilder(
+            context,
+            TestDatabase::class.java,
+            "test.db"
+        ).build()
+        runBlocking {
+            db.counterDao().upsert(Counter(1, 0))
+            repeat(5000) {
+                launch(Dispatchers.IO) {
+                    db.withTransaction {
+                        val current = db.counterDao().getCounter(1)
+                        suspendHere()
+                        db.counterDao().upsert(current.copy(value = current.value + 1))
+                    }
+                }
+            }
+        }
+        runBlocking {
+            val count = db.counterDao().getCounter(1)
+            assertThat(count.value).isEqualTo(5000)
+        }
+        db.close()
+    }
+
+    // Utility function to _really_ suspend.
+    private suspend fun suspendHere(): Unit = suspendCoroutineUninterceptedOrReturn {
+        it.intercepted().resume(Unit)
+        COROUTINE_SUSPENDED
+    }
+
+    // Executor wrapper that counts threads that are busy executing commands.
+    class BusyCountingService(
+        val count: AtomicInteger,
+        val delegate: ExecutorService
+    ) : ExecutorService by delegate {
+        override fun execute(command: Runnable) {
+            delegate.execute {
+                count.incrementAndGet()
+                try {
+                    command.run()
+                } finally {
+                    count.decrementAndGet()
+                }
+            }
+        }
+    }
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Counter.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Counter.kt
new file mode 100644
index 0000000..ce8fd1c
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/vo/Counter.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 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.androidx.room.integration.kotlintestapp.vo
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+
+@Entity
+data class Counter(
+    @PrimaryKey val id: Long,
+    val value: Int
+)
\ No newline at end of file
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
index 8d0b2a8..22fc22d 100644
--- a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
+++ b/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
@@ -53,7 +53,7 @@
             inTransaction: Boolean,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
@@ -74,7 +74,7 @@
             cancellationSignal: CancellationSignal,
             callable: Callable<R>
         ): R {
-            if (db.isOpen && db.inTransaction()) {
+            if (db.isOpenInternal && db.inTransaction()) {
                 return callable.call()
             }
 
diff --git a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt b/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
index 4edabab..9fc8d5d 100644
--- a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
+++ b/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
@@ -18,18 +18,17 @@
 package androidx.room
 
 import androidx.annotation.RestrictTo
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.asContextElement
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import java.util.concurrent.Executor
 import java.util.concurrent.RejectedExecutionException
 import java.util.concurrent.atomic.AtomicInteger
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.resume
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asContextElement
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 
 /**
  * Calls the specified suspending [block] in a database transaction. The transaction will be
@@ -43,13 +42,11 @@
  * one received by the suspending block. It is recommended that all [Dao] function invoked within
  * the [block] be suspending functions.
  *
- * The dispatcher used to execute the given [block] will utilize threads from Room's query executor.
+ * The internal dispatcher used to execute the given [block] will block an utilize a thread from
+ * Room's transaction executor until the [block] is complete.
  */
 public suspend fun <R> RoomDatabase.withTransaction(block: suspend () -> R): R {
-    // Use inherited transaction context if available, this allows nested suspending transactions.
-    val transactionContext =
-        coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
-    return withContext(transactionContext) {
+    val transactionBlock: suspend CoroutineScope.() -> R = transaction@{
         val transactionElement = coroutineContext[TransactionElement]!!
         transactionElement.acquire()
         try {
@@ -59,7 +56,7 @@
                 val result = block.invoke()
                 @Suppress("DEPRECATION")
                 setTransactionSuccessful()
-                return@withContext result
+                return@transaction result
             } finally {
                 @Suppress("DEPRECATION")
                 endTransaction()
@@ -68,6 +65,51 @@
             transactionElement.release()
         }
     }
+    // Use inherited transaction context if available, this allows nested suspending transactions.
+    val transactionDispatcher = coroutineContext[TransactionElement]?.transactionDispatcher
+    return if (transactionDispatcher != null) {
+        withContext(transactionDispatcher, transactionBlock)
+    } else {
+        startTransactionCoroutine(coroutineContext, transactionBlock)
+    }
+}
+
+/**
+ * Suspend caller coroutine and start the transaction coroutine in a thread from the
+ * [RoomDatabase.transactionExecutor], resuming the caller coroutine with the result once done.
+ * The [context] will be a parent of the started coroutine to propagating cancellation and release
+ * the thread when cancelled.
+ */
+private suspend fun <R> RoomDatabase.startTransactionCoroutine(
+    context: CoroutineContext,
+    transactionBlock: suspend CoroutineScope.() -> R
+): R = suspendCancellableCoroutine { continuation ->
+    try {
+        transactionExecutor.execute {
+            try {
+                // Thread acquired, start the transaction coroutine using the parent context.
+                // The started coroutine will have an event loop dispatcher that we'll use for the
+                // transaction context.
+                runBlocking(context.minusKey(ContinuationInterceptor)) {
+                    val dispatcher = coroutineContext[ContinuationInterceptor]!!
+                    val transactionContext = createTransactionContext(dispatcher)
+                    continuation.resume(
+                        withContext(transactionContext, transactionBlock)
+                    )
+                }
+            } catch (ex: Throwable) {
+                // If anything goes wrong, propagate exception to the calling coroutine.
+                continuation.cancel(ex)
+            }
+        }
+    } catch (ex: RejectedExecutionException) {
+        // Couldn't acquire a thread, cancel coroutine.
+        continuation.cancel(
+            IllegalStateException(
+                "Unable to acquire a thread to perform the database transaction.", ex
+            )
+        )
+    }
 }
 
 /**
@@ -76,8 +118,9 @@
  * The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
  *
  * * The dispatcher will dispatch coroutines to a single thread that is taken over from the Room
- * query executor. If the coroutine context is switched, suspending DAO functions will be able to
- * dispatch to the transaction thread.
+ * transaction executor. If the coroutine context is switched, suspending DAO functions will be able
+ * to dispatch to the transaction thread. In reality the dispatcher is the event loop of a
+ * [runBlocking] started on the dedicated thread.
  *
  * * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
  * switch of context, suspending DAO methods will be able to use the indicator to dispatch the
@@ -88,61 +131,22 @@
  * if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
  * this value, for now all we care is if its present or not.
  */
-private suspend fun RoomDatabase.createTransactionContext(): CoroutineContext {
-    val controlJob = Job()
-    // make sure to tie the control job to this context to avoid blocking the transaction if
-    // context get cancelled before we can even start using this job. Otherwise, the acquired
-    // transaction thread will forever wait for the controlJob to be cancelled.
-    // see b/148181325
-    coroutineContext[Job]?.invokeOnCompletion {
-        controlJob.cancel()
-    }
-    val dispatcher = transactionExecutor.acquireTransactionThread(controlJob)
-    val transactionElement = TransactionElement(controlJob, dispatcher)
+private fun RoomDatabase.createTransactionContext(
+    dispatcher: ContinuationInterceptor
+): CoroutineContext {
+    val transactionElement = TransactionElement(dispatcher)
     val threadLocalElement =
-        suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
+        suspendingTransactionId.asContextElement(System.identityHashCode(transactionElement))
     return dispatcher + transactionElement + threadLocalElement
 }
 
 /**
- * Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
- * coroutines to the acquired thread. The [controlJob] is used to control the release of the
- * thread by cancelling the job.
- */
-private suspend fun Executor.acquireTransactionThread(controlJob: Job): ContinuationInterceptor {
-    return suspendCancellableCoroutine { continuation ->
-        continuation.invokeOnCancellation {
-            // We got cancelled while waiting to acquire a thread, we can't stop our attempt to
-            // acquire a thread, but we can cancel the controlling job so once it gets acquired it
-            // is quickly released.
-            controlJob.cancel()
-        }
-        try {
-            execute {
-                runBlocking {
-                    // Thread acquired, resume coroutine.
-                    continuation.resume(coroutineContext[ContinuationInterceptor]!!)
-                    controlJob.join()
-                }
-            }
-        } catch (ex: RejectedExecutionException) {
-            // Couldn't acquire a thread, cancel coroutine.
-            continuation.cancel(
-                IllegalStateException(
-                    "Unable to acquire a thread to perform the database transaction.", ex
-                )
-            )
-        }
-    }
-}
-/**
  * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
  *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 internal class TransactionElement(
-    private val transactionThreadControlJob: Job,
     internal val transactionDispatcher: ContinuationInterceptor
 ) : CoroutineContext.Element {
 
@@ -153,9 +157,7 @@
 
     /**
      * Number of transactions (including nested ones) started with this element.
-     * Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
-     * when [release] is invoked then the transaction job is cancelled and the transaction thread
-     * is released.
+     * Call [acquire] to increase the count and [release] to decrease it.
      */
     private val referenceCount = AtomicInteger(0)
 
@@ -167,9 +169,6 @@
         val count = referenceCount.decrementAndGet()
         if (count < 0) {
             throw IllegalStateException("Transaction was never started or was already released.")
-        } else if (count == 0) {
-            // Cancel the job that controls the transaction thread, causing it to be released.
-            transactionThreadControlJob.cancel()
         }
     }
 }
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index 251b82a..2088242 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -162,11 +162,11 @@
         }
     }
 
-    // TODO: Close CleanupStatement
-    internal fun onAutoCloseCallback() {
+    private fun onAutoCloseCallback() {
         synchronized(trackerLock) {
             initialized = false
             observedTableTracker.resetTriggerState()
+            cleanupStatement?.close()
         }
     }
 
@@ -329,7 +329,7 @@
     }
 
     internal fun ensureInitialization(): Boolean {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return false
         }
         if (!initialized) {
@@ -526,7 +526,7 @@
      * This api should eventually be public.
      */
     internal fun syncTriggers() {
-        if (!database.isOpen) {
+        if (!database.isOpenInternal) {
             return
         }
         syncTriggers(database.openHelper.writableDatabase)
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
index 599d9d3..a455992 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomDatabase.kt
@@ -394,13 +394,22 @@
     /**
      * True if database connection is open and initialized.
      *
+     * When Room is configured with [RoomDatabase.Builder.setAutoCloseTimeout] the database
+     * is considered open even if internally the connection has been closed, unless manually closed.
+     *
      * @return true if the database connection is open, false otherwise.
      */
-    @Suppress("Deprecation")
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
     open val isOpen: Boolean
-        get() {
-            return (autoCloser?.isActive ?: mDatabase?.isOpen) == true
-        }
+        get() = (autoCloser?.isActive ?: mDatabase?.isOpen) == true
+
+    /**
+     * True if the actual database connection is open, regardless of auto-close.
+     */
+    @Suppress("Deprecation") // Due to usage of `mDatabase`
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    val isOpenInternal: Boolean
+        get() = mDatabase?.isOpen == true
 
     /**
      * Closes the database if it is already open.
@@ -1193,7 +1202,7 @@
 
         /**
          * 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 `autoCloseTimeout` has
+         * database will be closed after it's last use after the specified [autoCloseTimeout] has
          * elapsed since its last usage. The database will be automatically
          * re-opened the next time it is accessed.
          *
@@ -1210,7 +1219,7 @@
          *
          * The auto-closing database operation runs on the query executor.
          *
-         * The database will not be reopened if the RoomDatabase or the
+         * The database will not be re-opened if the RoomDatabase or the
          * SupportSqliteOpenHelper is closed manually (by calling
          * [RoomDatabase.close] or [SupportSQLiteOpenHelper.close]. If the
          * database is closed manually, you must create a new database using
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
index 20f62f1..20e2bc7 100644
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
@@ -76,7 +76,7 @@
         doReturn(statement).whenever(mSqliteDb)
             .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
         doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
-        doReturn(true).whenever(mRoomDatabase).isOpen
+        doReturn(true).whenever(mRoomDatabase).isOpenInternal
         doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
         val closeLock = ReentrantLock()
         doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
@@ -245,7 +245,7 @@
 
     @Test
     fun closedDb() {
-        doReturn(false).whenever(mRoomDatabase).isOpen
+        doReturn(false).whenever(mRoomDatabase).isOpenInternal
         doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
         mTracker.addObserver(LatchObserver(1, "a", "b"))
         mTracker.refreshRunnable.run()
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkOpenHelperTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkOpenHelperTest.kt
new file mode 100644
index 0000000..bc5555a
--- /dev/null
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkOpenHelperTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 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.sqlite.db.framework
+
+import android.content.Context
+import android.os.StrictMode
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+@SmallTest
+class FrameworkOpenHelperTest {
+    private val dbName = "test.db"
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val openHelper = FrameworkSQLiteOpenHelper(
+        context = context,
+        name = dbName,
+        callback = OpenHelperRecoveryTest.EmptyCallback(),
+    )
+
+    @Before
+    fun setup() {
+        context.deleteDatabase(dbName)
+    }
+
+    @Test
+    fun testFrameWorkSQLiteOpenHelper_cacheDatabase() {
+        // Open DB, does I/O
+        val firstRef = openHelper.writableDatabase
+        assertThat(firstRef.isOpen).isTrue()
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            StrictMode.setThreadPolicy(
+                StrictMode.ThreadPolicy.Builder()
+                    .detectDiskReads()
+                    .detectDiskWrites()
+                    .penaltyDeath()
+                    .build()
+            )
+
+            // DB is already opened, should not do I/O
+            val secondRef = openHelper.writableDatabase
+            assertThat(secondRef.isOpen).isTrue()
+
+            assertThat(firstRef).isEqualTo(secondRef)
+        }
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
index c807e23..66d82ae 100644
--- a/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
+++ b/sqlite/sqlite-framework/src/androidTest/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabaseTest.kt
@@ -17,6 +17,8 @@
 package androidx.sqlite.db.framework
 
 import android.content.Context
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -79,4 +81,82 @@
         val cursor2 = db.query("select * from user where idk=1")
         assertThat(cursor2.count).isEqualTo(0)
     }
+
+    @Test
+    fun testFrameWorkSQLiteDatabase_attachDbWorks() {
+        val openHelper2 = FrameworkSQLiteOpenHelper(
+            context,
+            "test2.db",
+            OpenHelperRecoveryTest.EmptyCallback(),
+            useNoBackupDirectory = false,
+            allowDataLossOnRecovery = false
+        )
+        val db1 = openHelper.writableDatabase
+        val db2 = openHelper2.writableDatabase
+
+        db1.execSQL(
+            "ATTACH DATABASE '${db2.path}' AS database2"
+        )
+
+        val cursor = db1.query("pragma database_list")
+        val expected = buildList<Pair<String, String>> {
+            while (cursor.moveToNext()) {
+                add(cursor.getString(1) to cursor.getString(2))
+            }
+        }
+        cursor.close()
+        openHelper2.close()
+
+        val actual = db1.attachedDbs?.map { it.first to it.second }
+        assertThat(expected).isEqualTo(actual)
+    }
+
+    // b/271083856 and b/183028015
+    @Test
+    fun testFrameWorkSQLiteDatabase_onUpgrade_maxSqlCache() {
+        // Open and close DB at initial version.
+        openHelper.writableDatabase.use { db ->
+            db.execSQL("CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY, data TEXT)")
+            db.execSQL("INSERT INTO Foo (id, data) VALUES (1, 'bar')")
+        }
+
+        FrameworkSQLiteOpenHelper(
+            context,
+            dbName,
+            object : SupportSQLiteOpenHelper.Callback(10) {
+                override fun onCreate(db: SupportSQLiteDatabase) {}
+
+                override fun onUpgrade(
+                    db: SupportSQLiteDatabase,
+                    oldVersion: Int,
+                    newVersion: Int
+                ) {
+                    // Do a query, this query will get cached, but we expect it to get evicted if
+                    // androidx.sqlite workarounds this issue by reducing the cache size.
+                    db.query("SELECT * FROM Foo").let { c ->
+                        assertThat(c.moveToNext()).isTrue()
+                        assertThat(c.getString(1)).isEqualTo("bar")
+                        c.close()
+                    }
+                    // Alter table, specifically make it so that using a cached query will be
+                    // troublesome.
+                    db.execSQL("ALTER TABLE Foo RENAME TO Foo_old")
+                    db.execSQL("CREATE TABLE Foo (id INTEGER NOT NULL PRIMARY KEY)")
+                    // Do an irrelevant query to evict the last SELECT statement, sadly this is
+                    // required because we can only reduce the cache size to 1, and only SELECT or
+                    // UPDATE statement are cache.
+                    // See frameworks/base/core/java/android/database/sqlite/SQLiteConnection.java;l=1209
+                    db.query("SELECT * FROM Foo_old").close()
+                    // Do earlier query, checking it is not cached
+                    db.query("SELECT * FROM Foo").let { c ->
+                        assertThat(c.columnNames.toList()).containsExactly("id")
+                        assertThat(c.count).isEqualTo(0)
+                        c.close()
+                    }
+                }
+            },
+            useNoBackupDirectory = false,
+            allowDataLossOnRecovery = false
+        ).writableDatabase.close()
+    }
 }
\ No newline at end of file
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
index 00257a1..b9aeb34 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteDatabase.kt
@@ -290,7 +290,8 @@
     override val isWriteAheadLoggingEnabled: Boolean
         get() = SupportSQLiteCompat.Api16Impl.isWriteAheadLoggingEnabled(delegate)
 
-    override val attachedDbs: List<Pair<String, String>>? = delegate.attachedDbs
+    override val attachedDbs: List<Pair<String, String>>?
+        get() = delegate.attachedDbs
 
     override val isDatabaseIntegrityOk: Boolean
         get() = delegate.isDatabaseIntegrityOk
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
index 501180e..65adc7e 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
@@ -162,7 +162,8 @@
 
         private fun innerGetDatabase(writable: Boolean): SQLiteDatabase {
             val name = databaseName
-            if (name != null) {
+            val isOpen = opened
+            if (name != null && !isOpen) {
                 val databaseFile = context.getDatabasePath(name)
                 val parentFile = databaseFile.parentFile
                 if (parentFile != null) {
@@ -256,6 +257,13 @@
         }
 
         override fun onConfigure(db: SQLiteDatabase) {
+            if (!migrated && callback.version != db.version) {
+                // Reduce the prepared statement cache to the minimum allowed (1) to avoid
+                // issues with queries executed during migrations. Note that when a migration is
+                // done the connection is closed and re-opened to avoid stale connections, which
+                // in turns resets the cache max size. See b/271083856
+                db.setMaxSqlCacheSize(1)
+            }
             try {
                 callback.onConfigure(getWrappedDb(db))
             } catch (t: Throwable) {
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
index 3b79342..dc07eb9 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/util/ProcessLock.kt
@@ -52,12 +52,12 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ProcessLock(
     name: String,
-    lockDir: File,
+    lockDir: File?,
     private val processLock: Boolean
 ) {
-    private val lockFile: File = File(lockDir, "$name.lck")
+    private val lockFile: File? = lockDir?.let { File(it, "$name.lck") }
     @SuppressLint("SyntheticAccessor")
-    private val threadLock: Lock = getThreadLock(lockFile.absolutePath)
+    private val threadLock: Lock = getThreadLock(name)
     private var lockChannel: FileChannel? = null
 
     /**
@@ -69,6 +69,9 @@
         threadLock.lock()
         if (processLock) {
             try {
+                if (lockFile == null) {
+                    throw IOException("No lock directory was provided.")
+                }
                 // Verify parent dir
                 val parentDir = lockFile.parentFile
                 parentDir?.mkdirs()
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
index bd253f2..820d26a 100644
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteDatabase.kt
@@ -53,8 +53,8 @@
      *
      * ```
      *  db.beginTransaction()
-     *      try {
-     *          ...
+     *  try {
+     *      ...
      *      db.setTransactionSuccessful()
      *  } finally {
      *      db.endTransaction()
@@ -75,10 +75,10 @@
      *
      * ```
      *  db.beginTransactionNonExclusive()
-     *      try {
-     *          ...
+     *  try {
+     *      ...
      *      db.setTransactionSuccessful()
-     *          } finally {
+     *  } finally {
      *      db.endTransaction()
      *  }
      *  ```
@@ -126,7 +126,7 @@
      *  db.beginTransactionWithListenerNonExclusive(listener)
      *  try {
      *      ...
-     *  db.setTransactionSuccessful()
+     *      db.setTransactionSuccessful()
      *  } finally {
      *      db.endTransaction()
      *  }