Merge room-ktx into room-runtime

And very importantly add Coroutines as a dependency on room-runtime.

Also add an explicit dep to room-ktx to certain projects since atomic group constraints are not enforced via project() dependencies.

Bug: 317120607
Relnote: "RoomDatabase Kotlin extensions, withTransaction(), invalidationTrackerFlow() and others have been moved from `room-ktx` to `room-runtime`. The artifact `room-ktx` is now empty and is not needed for Coroutines support in Room, and specifically not needed when defining suspend functions in DAOs."
Test: ./gradlew checkApi
Change-Id: Ib7619848956632f51608db6d3e0f40f6782c6251
diff --git a/benchmark/integration-tests/macrobenchmark-target/build.gradle b/benchmark/integration-tests/macrobenchmark-target/build.gradle
index d455803..fbe028a 100644
--- a/benchmark/integration-tests/macrobenchmark-target/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark-target/build.gradle
@@ -45,4 +45,5 @@
     implementation(project(":work:work-runtime"))
     implementation(project(":work:work-runtime-ktx"))
     implementation(project(":room:room-runtime"))
+    implementation(project(":room:room-ktx"))
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index a737a73..5942d89 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -111,11 +111,6 @@
         ): MethodProcessorDelegate {
             val asMember = executableElement.asMemberOf(containing)
             return if (asMember.isSuspendFunction()) {
-                val hasCoroutineArtifact = context.processingEnv
-                    .findTypeElement(COROUTINES_ROOM.canonicalName) != null
-                if (!hasCoroutineArtifact) {
-                    context.logger.e(ProcessorErrors.MISSING_ROOM_COROUTINE_ARTIFACT)
-                }
                 SuspendMethodProcessorDelegate(
                     context,
                     containing,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
index 1bea64a..d4cbabc 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ProcessorErrors.kt
@@ -653,9 +653,6 @@
         "add `room-paging-rxjava3` artifact from Room as a dependency. " +
         "androidx.room:room-paging-rxjava3:<version>"
 
-    val MISSING_ROOM_COROUTINE_ARTIFACT = "To use Coroutine features, you must add `ktx`" +
-        " artifact from Room as a dependency. androidx.room:room-ktx:<version>"
-
     fun ambiguousConstructor(
         pojo: String,
         paramName: String,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
index ddd7285..932998d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/binderprovider/CoroutineFlowResultBinderProvider.kt
@@ -18,7 +18,6 @@
 
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.KotlinTypeNames
-import androidx.room.ext.RoomCoroutinesTypeNames.COROUTINES_ROOM
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.processor.ProcessorErrors
@@ -27,17 +26,7 @@
 import androidx.room.solver.query.result.CoroutineFlowResultBinder
 import androidx.room.solver.query.result.QueryResultBinder
 
-@Suppress("FunctionName")
-fun CoroutineFlowResultBinderProvider(context: Context): QueryResultBinderProvider =
-    CoroutineFlowResultBinderProviderImpl(
-        context
-    ).requireArtifact(
-        context = context,
-        requiredType = COROUTINES_ROOM,
-        missingArtifactErrorMsg = ProcessorErrors.MISSING_ROOM_COROUTINE_ARTIFACT
-    )
-
-private class CoroutineFlowResultBinderProviderImpl(
+class CoroutineFlowResultBinderProvider(
     val context: Context
 ) : QueryResultBinderProvider {
     companion object {
diff --git a/room/room-ktx/api/current.ignore b/room/room-ktx/api/current.ignore
new file mode 100644
index 0000000..558a9f0
--- /dev/null
+++ b/room/room-ktx/api/current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.room:
+    Removed package androidx.room
+RemovedPackage: androidx.room.migration:
+    Removed package androidx.room.migration
diff --git a/room/room-ktx/api/current.txt b/room/room-ktx/api/current.txt
index ceaca54..e6f50d0 100644
--- a/room/room-ktx/api/current.txt
+++ b/room/room-ktx/api/current.txt
@@ -1,18 +1 @@
 // Signature format: 4.0
-package androidx.room {
-
-  public final class RoomDatabaseKt {
-    method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
-    method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
-  }
-
-}
-
-package androidx.room.migration {
-
-  public final class MigrationKt {
-    method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
-  }
-
-}
-
diff --git a/room/room-ktx/api/restricted_current.ignore b/room/room-ktx/api/restricted_current.ignore
new file mode 100644
index 0000000..558a9f0
--- /dev/null
+++ b/room/room-ktx/api/restricted_current.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+RemovedPackage: androidx.room:
+    Removed package androidx.room
+RemovedPackage: androidx.room.migration:
+    Removed package androidx.room.migration
diff --git a/room/room-ktx/api/restricted_current.txt b/room/room-ktx/api/restricted_current.txt
index fc6c3c0..e6f50d0 100644
--- a/room/room-ktx/api/restricted_current.txt
+++ b/room/room-ktx/api/restricted_current.txt
@@ -1,31 +1 @@
 // Signature format: 4.0
-package androidx.room {
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoroutinesRoom {
-    method public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
-    method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
-    method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
-    field public static final androidx.room.CoroutinesRoom.Companion Companion;
-  }
-
-  public static final class CoroutinesRoom.Companion {
-    method public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
-    method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
-    method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
-  }
-
-  public final class RoomDatabaseKt {
-    method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
-    method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
-  }
-
-}
-
-package androidx.room.migration {
-
-  public final class MigrationKt {
-    method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
-  }
-
-}
-
diff --git a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt b/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
deleted file mode 100644
index 13b2b53..0000000
--- a/room/room-ktx/src/main/java/androidx/room/RoomDatabaseExt.kt
+++ /dev/null
@@ -1,238 +0,0 @@
-/*
- * Copyright 2019 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.
- */
-@file:JvmName("RoomDatabaseKt")
-
-package androidx.room
-
-import androidx.annotation.RestrictTo
-import java.util.concurrent.RejectedExecutionException
-import java.util.concurrent.atomic.AtomicBoolean
-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.awaitCancellation
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.launch
-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
- * marked as successful unless an exception is thrown in the suspending [block] or the coroutine
- * is cancelled.
- *
- * Room will only perform at most one transaction at a time, additional transactions are queued
- * and executed on a first come, first serve order.
- *
- * Performing blocking database operations is not permitted in a coroutine scope other than the
- * one received by the suspending block. It is recommended that all [Dao] function invoked within
- * the [block] be suspending functions.
- *
- * 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 {
-    val transactionBlock: suspend CoroutineScope.() -> R = transaction@{
-        val transactionElement = coroutineContext[TransactionElement]!!
-        transactionElement.acquire()
-        try {
-            @Suppress("DEPRECATION")
-            beginTransaction()
-            try {
-                val result = block.invoke()
-                @Suppress("DEPRECATION")
-                setTransactionSuccessful()
-                return@transaction result
-            } finally {
-                @Suppress("DEPRECATION")
-                endTransaction()
-            }
-        } finally {
-            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
-            )
-        )
-    }
-}
-
-/**
- * Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
- *
- * 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
- * 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
- * database operation to the transaction thread.
- *
- * * The thread local element serves as a second indicator and marks threads that are used to
- * execute coroutines within the coroutine transaction, more specifically it allows us to identify
- * 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 fun RoomDatabase.createTransactionContext(
-    dispatcher: ContinuationInterceptor
-): CoroutineContext {
-    val transactionElement = TransactionElement(dispatcher)
-    val threadLocalElement =
-        suspendingTransactionId.asContextElement(System.identityHashCode(transactionElement))
-    return dispatcher + transactionElement + threadLocalElement
-}
-
-/**
- * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
- *
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-internal class TransactionElement(
-    internal val transactionDispatcher: ContinuationInterceptor
-) : CoroutineContext.Element {
-
-    companion object Key : CoroutineContext.Key<TransactionElement>
-
-    override val key: CoroutineContext.Key<TransactionElement>
-        get() = TransactionElement
-
-    /**
-     * Number of transactions (including nested ones) started with this element.
-     * Call [acquire] to increase the count and [release] to decrease it.
-     */
-    private val referenceCount = AtomicInteger(0)
-
-    fun acquire() {
-        referenceCount.incrementAndGet()
-    }
-
-    fun release() {
-        val count = referenceCount.decrementAndGet()
-        if (count < 0) {
-            throw IllegalStateException("Transaction was never started or was already released.")
-        }
-    }
-}
-
-/**
- * Creates a [Flow] that listens for changes in the database via the [InvalidationTracker] and emits
- * sets of the tables that were invalidated.
- *
- * The Flow will emit at least one value, a set of all the tables registered for observation to
- * kick-start the stream unless [emitInitialState] is set to `false`.
- *
- * If one of the tables to observe does not exist in the database, this Flow throws an
- * [IllegalArgumentException] during collection.
- *
- * The returned Flow can be used to create a stream that reacts to changes in the database:
- * ```
- * fun getArtistTours(from: Date, to: Date): Flow<Map<Artist, TourState>> {
- *   return db.invalidationTrackerFlow("Artist").map { _ ->
- *     val artists = artistsDao.getAllArtists()
- *     val tours = tourService.fetchStates(artists.map { it.id })
- *     associateTours(artists, tours, from, to)
- *   }
- * }
- * ```
- *
- * @param tables The name of the tables or views to observe.
- * @param emitInitialState Set to `false` if no initial emission is desired. Default value is
- *                         `true`.
- */
-public fun RoomDatabase.invalidationTrackerFlow(
-    vararg tables: String,
-    emitInitialState: Boolean = true
-): Flow<Set<String>> = callbackFlow {
-    // Flag to ignore invalidation until the initial state is sent.
-    val ignoreInvalidation = AtomicBoolean(emitInitialState)
-    val observer = object : InvalidationTracker.Observer(tables) {
-        override fun onInvalidated(tables: Set<String>) {
-            if (ignoreInvalidation.get()) {
-                return
-            }
-            trySend(tables)
-        }
-    }
-    val queryContext =
-        coroutineContext[TransactionElement]?.transactionDispatcher ?: getQueryDispatcher()
-    val job = launch(queryContext) {
-        invalidationTracker.addObserver(observer)
-        try {
-            if (emitInitialState) {
-                // Initial invalidation of all tables, to kick-start the flow
-                trySend(tables.toSet())
-            }
-            ignoreInvalidation.set(false)
-            awaitCancellation()
-        } finally {
-            invalidationTracker.removeObserver(observer)
-        }
-    }
-    awaitClose {
-        job.cancel()
-    }
-}
diff --git a/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt b/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt
deleted file mode 100644
index a697681..0000000
--- a/room/room-ktx/src/main/java/androidx/room/migration/MigrationExt.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-
-/*
- * Copyright 2021 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.
- */
-@file:JvmName("MigrationKt")
-
-package androidx.room.migration
-
-import androidx.sqlite.db.SupportSQLiteDatabase
-
-/**
- * Creates [Migration] from [startVersion] to [endVersion] that runs [migrate] to perform
- * the necessary migrations.
- *
- * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
- * going version 3 to 5 without going to version 4). If Room opens a database at version
- * 3 and latest version is < 5, Room will use the migration object that can migrate from
- * 3 to 5 instead of 3 to 4 and 4 to 5.
- *
- * If there are not enough migrations provided to move from the current version to the latest
- * version, Room will clear the database and recreate so even if you have no changes between 2
- * versions, you should still provide a Migration object to the builder.
- *
- * [migrate] cannot access any generated Dao in this method.
- *
- * [migrate] is already called inside a transaction and that transaction
- * might actually be a composite transaction of all necessary `Migration`s.
- */
-public fun Migration(
-    startVersion: Int,
-    endVersion: Int,
-    migrate: (SupportSQLiteDatabase) -> Unit
-): Migration = MigrationImpl(startVersion, endVersion, migrate)
-
-private class MigrationImpl(
-    startVersion: Int,
-    endVersion: Int,
-    val migrateCallback: (SupportSQLiteDatabase) -> Unit
-) : Migration(startVersion, endVersion) {
-    override fun migrate(db: SupportSQLiteDatabase) = migrateCallback(db)
-}
diff --git a/room/room-runtime/api/current.txt b/room/room-runtime/api/current.txt
index 57ca0f5..50d82d1 100644
--- a/room/room-runtime/api/current.txt
+++ b/room/room-runtime/api/current.txt
@@ -160,6 +160,11 @@
     method public void onQuery(String sqlQuery, java.util.List<?> bindArgs);
   }
 
+  public final class RoomDatabaseKt {
+    method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+    method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+  }
+
   public interface RoomOpenDelegateMarker {
   }
 
@@ -180,5 +185,9 @@
     field public final int startVersion;
   }
 
+  public final class MigrationKt {
+    method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
+  }
+
 }
 
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 5b386e3..ceda800 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -1,6 +1,19 @@
 // Signature format: 4.0
 package androidx.room {
 
+  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class CoroutinesRoom {
+    method public static <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+    method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+    method public static suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+    field public static final androidx.room.CoroutinesRoom.Companion Companion;
+  }
+
+  public static final class CoroutinesRoom.Companion {
+    method public <R> kotlinx.coroutines.flow.Flow<R> createFlow(androidx.room.RoomDatabase db, boolean inTransaction, String[] tableNames, java.util.concurrent.Callable<R> callable);
+    method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, android.os.CancellationSignal? cancellationSignal, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+    method public suspend <R> Object? execute(androidx.room.RoomDatabase db, boolean inTransaction, java.util.concurrent.Callable<R> callable, kotlin.coroutines.Continuation<? super R>);
+  }
+
   public class DatabaseConfiguration {
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, boolean requireMigration, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom);
     ctor @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public DatabaseConfiguration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Factory sqliteOpenHelperFactory, androidx.room.RoomDatabase.MigrationContainer migrationContainer, java.util.List<? extends androidx.room.RoomDatabase.Callback>? callbacks, boolean allowMainThreadQueries, androidx.room.RoomDatabase.JournalMode journalMode, java.util.concurrent.Executor queryExecutor, java.util.concurrent.Executor transactionExecutor, android.content.Intent? multiInstanceInvalidationServiceIntent, boolean requireMigration, boolean allowDestructiveMigrationOnDowngrade, java.util.Set<java.lang.Integer>? migrationNotRequiredFrom, String? copyFromAssetPath, java.io.File? copyFromFile, java.util.concurrent.Callable<java.io.InputStream>? copyFromInputStream, androidx.room.RoomDatabase.PrepackagedDatabaseCallback? prepackagedDatabaseCallback, java.util.List<?> typeConverters, java.util.List<? extends androidx.room.migration.AutoMigrationSpec> autoMigrationSpecs);
@@ -218,6 +231,11 @@
     method public void onQuery(String sqlQuery, java.util.List<?> bindArgs);
   }
 
+  public final class RoomDatabaseKt {
+    method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+    method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+  }
+
   public interface RoomOpenDelegateMarker {
   }
 
@@ -311,6 +329,10 @@
     field public final int startVersion;
   }
 
+  public final class MigrationKt {
+    method public static androidx.room.migration.Migration Migration(int startVersion, int endVersion, kotlin.jvm.functions.Function1<? super androidx.sqlite.db.SupportSQLiteDatabase,kotlin.Unit> migrate);
+  }
+
 }
 
 package androidx.room.paging {
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index fedf63e..9b92064 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -83,11 +83,13 @@
                 api(project(":room:room-common"))
                 api(project(":sqlite:sqlite"))
                 api(projectOrArtifact(":annotation:annotation"))
+                api(libs.kotlinCoroutinesCore)
             }
         }
         commonTest {
             dependencies {
                 implementation(libs.kotlinTest)
+                implementation(libs.kotlinCoroutinesTest)
                 implementation(project(":kruth:kruth"))
             }
         }
@@ -95,6 +97,7 @@
             dependsOn(commonMain)
             dependencies {
                 api(project(":sqlite:sqlite-framework"))
+                api(libs.kotlinCoroutinesAndroid)
                 implementation("androidx.arch.core:core-runtime:2.2.0")
                 compileOnly("androidx.collection:collection:1.2.0")
                 compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
diff --git a/room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt b/room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
similarity index 100%
rename from room/room-ktx/src/androidTest/java/androidx/room/CoroutineRoomCancellationTest.kt
rename to room/room-runtime/src/androidInstrumentedTest/kotlin/androidx/room/CoroutineRoomCancellationTest.kt
diff --git a/room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
similarity index 100%
rename from room/room-ktx/src/main/java/androidx/room/CoroutinesRoom.kt
rename to room/room-runtime/src/androidMain/kotlin/androidx/room/CoroutinesRoom.android.kt
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index d178962..b94af2b 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:JvmName("RoomDatabaseKt")
+
 package androidx.room
 
 import android.annotation.SuppressLint
@@ -48,10 +50,27 @@
 import java.util.TreeMap
 import java.util.concurrent.Callable
 import java.util.concurrent.Executor
+import java.util.concurrent.RejectedExecutionException
 import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
 import java.util.concurrent.locks.Lock
 import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.coroutineContext
+import kotlin.coroutines.resume
 import kotlin.reflect.KClass
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.asContextElement
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 
 /**
  * Base class for all Room databases. All classes that are annotated with [Database] must
@@ -1729,3 +1748,204 @@
         const val MAX_BIND_PARAMETER_CNT = 999
     }
 }
+
+/**
+ * Calls the specified suspending [block] in a database transaction. The transaction will be
+ * marked as successful unless an exception is thrown in the suspending [block] or the coroutine
+ * is cancelled.
+ *
+ * Room will only perform at most one transaction at a time, additional transactions are queued
+ * and executed on a first come, first serve order.
+ *
+ * Performing blocking database operations is not permitted in a coroutine scope other than the
+ * one received by the suspending block. It is recommended that all [Dao] function invoked within
+ * the [block] be suspending functions.
+ *
+ * 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 {
+    val transactionBlock: suspend CoroutineScope.() -> R = transaction@{
+        val transactionElement = coroutineContext[TransactionElement]!!
+        transactionElement.acquire()
+        try {
+            @Suppress("DEPRECATION")
+            beginTransaction()
+            try {
+                val result = block.invoke()
+                @Suppress("DEPRECATION")
+                setTransactionSuccessful()
+                return@transaction result
+            } finally {
+                @Suppress("DEPRECATION")
+                endTransaction()
+            }
+        } finally {
+            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
+            )
+        )
+    }
+}
+
+/**
+ * Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
+ *
+ * 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
+ * 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
+ * database operation to the transaction thread.
+ *
+ * * The thread local element serves as a second indicator and marks threads that are used to
+ * execute coroutines within the coroutine transaction, more specifically it allows us to identify
+ * 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 fun RoomDatabase.createTransactionContext(
+    dispatcher: ContinuationInterceptor
+): CoroutineContext {
+    val transactionElement = TransactionElement(dispatcher)
+    val threadLocalElement =
+        suspendingTransactionId.asContextElement(System.identityHashCode(transactionElement))
+    return dispatcher + transactionElement + threadLocalElement
+}
+
+/**
+ * A [CoroutineContext.Element] that indicates there is an on-going database transaction.
+ *
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+internal class TransactionElement(
+    internal val transactionDispatcher: ContinuationInterceptor
+) : CoroutineContext.Element {
+
+    companion object Key : CoroutineContext.Key<TransactionElement>
+
+    override val key: CoroutineContext.Key<TransactionElement>
+        get() = TransactionElement
+
+    /**
+     * Number of transactions (including nested ones) started with this element.
+     * Call [acquire] to increase the count and [release] to decrease it.
+     */
+    private val referenceCount = AtomicInteger(0)
+
+    fun acquire() {
+        referenceCount.incrementAndGet()
+    }
+
+    fun release() {
+        val count = referenceCount.decrementAndGet()
+        if (count < 0) {
+            throw IllegalStateException("Transaction was never started or was already released.")
+        }
+    }
+}
+
+/**
+ * Creates a [Flow] that listens for changes in the database via the [InvalidationTracker] and emits
+ * sets of the tables that were invalidated.
+ *
+ * The Flow will emit at least one value, a set of all the tables registered for observation to
+ * kick-start the stream unless [emitInitialState] is set to `false`.
+ *
+ * If one of the tables to observe does not exist in the database, this Flow throws an
+ * [IllegalArgumentException] during collection.
+ *
+ * The returned Flow can be used to create a stream that reacts to changes in the database:
+ * ```
+ * fun getArtistTours(from: Date, to: Date): Flow<Map<Artist, TourState>> {
+ *   return db.invalidationTrackerFlow("Artist").map { _ ->
+ *     val artists = artistsDao.getAllArtists()
+ *     val tours = tourService.fetchStates(artists.map { it.id })
+ *     associateTours(artists, tours, from, to)
+ *   }
+ * }
+ * ```
+ *
+ * @param tables The name of the tables or views to observe.
+ * @param emitInitialState Set to `false` if no initial emission is desired. Default value is
+ *                         `true`.
+ */
+public fun RoomDatabase.invalidationTrackerFlow(
+    vararg tables: String,
+    emitInitialState: Boolean = true
+): Flow<Set<String>> = callbackFlow {
+    // Flag to ignore invalidation until the initial state is sent.
+    val ignoreInvalidation = AtomicBoolean(emitInitialState)
+    val observer = object : InvalidationTracker.Observer(tables) {
+        override fun onInvalidated(tables: Set<String>) {
+            if (ignoreInvalidation.get()) {
+                return
+            }
+            trySend(tables)
+        }
+    }
+    val queryContext =
+        coroutineContext[TransactionElement]?.transactionDispatcher ?: getQueryDispatcher()
+    val job = launch(queryContext) {
+        invalidationTracker.addObserver(observer)
+        try {
+            if (emitInitialState) {
+                // Initial invalidation of all tables, to kick-start the flow
+                trySend(tables.toSet())
+            }
+            ignoreInvalidation.set(false)
+            awaitCancellation()
+        } finally {
+            invalidationTracker.removeObserver(observer)
+        }
+    }
+    awaitClose {
+        job.cancel()
+    }
+}
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
index 07838889..dbad445 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/migration/Migration.android.kt
@@ -13,6 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+@file:JvmName("MigrationKt")
+
 package androidx.room.migration
 
 import androidx.room.driver.SupportSQLiteConnection
@@ -70,3 +72,35 @@
         }
     }
 }
+
+/**
+ * Creates [Migration] from [startVersion] to [endVersion] that runs [migrate] to perform
+ * the necessary migrations.
+ *
+ * A migration can handle more than 1 version (e.g. if you have a faster path to choose when
+ * going version 3 to 5 without going to version 4). If Room opens a database at version
+ * 3 and latest version is < 5, Room will use the migration object that can migrate from
+ * 3 to 5 instead of 3 to 4 and 4 to 5.
+ *
+ * If there are not enough migrations provided to move from the current version to the latest
+ * version, Room will clear the database and recreate so even if you have no changes between 2
+ * versions, you should still provide a Migration object to the builder.
+ *
+ * [migrate] cannot access any generated Dao in this method.
+ *
+ * [migrate] is already called inside a transaction and that transaction
+ * might actually be a composite transaction of all necessary `Migration`s.
+ */
+public fun Migration(
+    startVersion: Int,
+    endVersion: Int,
+    migrate: (SupportSQLiteDatabase) -> Unit
+): Migration = MigrationImpl(startVersion, endVersion, migrate)
+
+private class MigrationImpl(
+    startVersion: Int,
+    endVersion: Int,
+    val migrateCallback: (SupportSQLiteDatabase) -> Unit
+) : Migration(startVersion, endVersion) {
+    override fun migrate(db: SupportSQLiteDatabase) = migrateCallback(db)
+}
diff --git a/room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
similarity index 100%
rename from room/room-ktx/src/test/java/androidx/room/CoroutinesRoomTest.kt
rename to room/room-runtime/src/androidUnitTest/kotlin/androidx/room/CoroutinesRoomTest.kt
diff --git a/room/room-ktx/src/test/java/androidx/room/MigrationTest.kt b/room/room-runtime/src/androidUnitTest/kotlin/androidx/room/MigrationTest.kt
similarity index 100%
rename from room/room-ktx/src/test/java/androidx/room/MigrationTest.kt
rename to room/room-runtime/src/androidUnitTest/kotlin/androidx/room/MigrationTest.kt
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 1925b84..7bac479 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -45,6 +45,7 @@
 dependencies {
     annotationProcessor(projectOrArtifact(":room:room-compiler"))
     implementation(projectOrArtifact(":room:room-runtime"))
+    implementation(projectOrArtifact(":room:room-ktx"))
 
     implementation(libs.constraintLayout)
     implementation("androidx.core:core:1.12.0")
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index cd0c8a6..29676c5 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -28,6 +28,7 @@
     androidTestImplementation(project(":work:work-multiprocess"))
     androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
     androidTestImplementation(projectOrArtifact(":room:room-runtime"))
+    androidTestImplementation(projectOrArtifact(":room:room-ktx"))
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)