Merge "Upgrade to androidx.test:runner:1.5.1" into androidx-main
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index a6fafc74..9a04dca 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -73,7 +73,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index a6fafc74..9a04dca 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -73,7 +73,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index cf252d3..45422eb 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -72,7 +72,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
index 67efa4b..7174a2a 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
@@ -37,11 +37,11 @@
  * [OnBackPressedDispatcher] it has been added to. It is strongly recommended
  * to instead disable this callback to handle temporary changes in state.
  *
- * @param isEnabled The default enabled state for this callback.
+ * @param enabled The default enabled state for this callback.
  *
  * @see ComponentActivity.getOnBackPressedDispatcher
  */
-abstract class OnBackPressedCallback(isEnabled: Boolean) {
+abstract class OnBackPressedCallback(enabled: Boolean) {
     /**
      * The enabled state of the callback. Only when this callback
      * is enabled will it receive callbacks to [handleOnBackPressed].
@@ -54,7 +54,7 @@
     @get:MainThread
     @set:MainThread
     @set:OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
-    var isEnabled: Boolean = isEnabled
+    var isEnabled: Boolean = enabled
         set(value) {
             field = value
             if (enabledConsumer != null) {
diff --git a/benchmark/OWNERS b/benchmark/OWNERS
index 171c5cc..eeaf357 100644
--- a/benchmark/OWNERS
+++ b/benchmark/OWNERS
@@ -1,3 +1,4 @@
 ccraik@google.com
 dustinlam@google.com
+jgielzak@google.com
 rahulrav@google.com
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index a016541..936c42f 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -76,6 +76,9 @@
             project.layout.buildDirectory.dir("$name.xcresult")
         }
 
+        // Configure the XCode Build Service so we don't run too many benchmarks at the same time.
+        project.configureXCodeBuildService()
+
         val fetchXCodeGenTask = project.tasks.register(
             FETCH_XCODEGEN_TASK, FetchXCodeGenTask::class.java
         ) {
@@ -90,12 +93,18 @@
             it.yamlFile.set(extension.xcodeGenConfigFile)
             it.projectName.set(extension.xcodeProjectName)
             it.xcProjectPath.set(xcodeProjectPath)
-            it.infoPlistPath.set(project.layout.buildDirectory.file("Info.plist"))
         }
 
         val runDarwinBenchmarks = project.tasks.register(
             RUN_DARWIN_BENCHMARKS_TASK, RunDarwinBenchmarksTask::class.java
         ) {
+            val sharedService =
+                project
+                    .gradle
+                    .sharedServices
+                    .registrations.getByName(XCodeBuildService.XCODE_BUILD_SERVICE_NAME)
+                    .service
+            it.usesService(sharedService)
             it.xcodeProjectPath.set(generateXCodeProjectTask.flatMap { task ->
                 task.xcProjectPath
             })
@@ -152,6 +161,7 @@
         const val DIST_DIR = "DIST_DIR"
         const val LIBRARY_METRICS = "librarymetrics"
         const val DARWIN_BENCHMARKS_DIR = "darwinBenchmarks"
+
         // Gradle Properties
         const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri"
 
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
index 7d4136d..dfca8e9 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -17,7 +17,7 @@
 package androidx.benchmark.darwin.gradle
 
 import androidx.benchmark.darwin.gradle.skia.Metrics
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import androidx.benchmark.darwin.gradle.xcode.XcResultParser
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -67,7 +67,7 @@
         }
         val (record, summaries) = parser.parseResults()
         val metrics = Metrics.buildMetrics(record, summaries)
-        val output = Models.gsonBuilder()
+        val output = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
             .toJson(metrics)
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
index 065e7ff..b07f5235 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
@@ -26,7 +26,6 @@
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.TaskAction
@@ -48,9 +47,6 @@
     @get:Input
     abstract val projectName: Property<String>
 
-    @get:OutputFile
-    abstract val infoPlistPath: RegularFileProperty
-
     @get:OutputDirectory
     abstract val xcProjectPath: DirectoryProperty
 
@@ -78,12 +74,14 @@
     }
 
     private fun copyProjectMetadata() {
-        val sourceFile = File(yamlFile.get().asFile.parent, "Info.plist")
-        require(sourceFile.exists())
-        val targetFile = infoPlistPath.get().asFile
-        val copied = sourceFile.copyRecursively(targetFile, overwrite = true)
-        require(copied) {
-            "Unable to copy $sourceFile to $targetFile"
+        // Copy generated `App.plist` and `Benchmark.plist` files.
+        // Context: b/258545725
+        val metadataFileNames = listOf("App.plist", "Benchmark.plist")
+        metadataFileNames.forEach { name ->
+            val sourceFile = File(yamlFile.get().asFile.parent, name)
+            require(sourceFile.exists())
+            val targetFile = File(xcProjectPath.get().asFile.parent, name)
+            sourceFile.copyRecursively(targetFile, overwrite = true)
         }
     }
 }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
index d33dd6a..b9ebca2 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
@@ -49,21 +49,26 @@
     @TaskAction
     fun runBenchmarks() {
         requireXcodeBuild()
+        // Consider moving this into the shared instance of XCodeBuildService
+        // given that is a much cleaner way of sharing a single instance of a running simulator.
+        val simCtrl = XCodeSimCtrl(execOperations, destination.get())
         val xcodeProject = xcodeProjectPath.get().asFile
         val xcResultFile = xcResultPath.get().asFile
         if (xcResultFile.exists()) {
             xcResultFile.deleteRecursively()
         }
-        val args = listOf(
-            "xcodebuild",
-            "test",
-            "-project", xcodeProject.absolutePath.toString(),
-            "-scheme", scheme.get(),
-            "-destination", destination.get(),
-            "-resultBundlePath", xcResultFile.absolutePath,
-        )
-        logger.info("Command : ${args.joinToString(" ")}")
-        execOperations.executeQuietly(args)
+        simCtrl.start { destinationDesc ->
+            val args = listOf(
+                "xcodebuild",
+                "test",
+                "-project", xcodeProject.absolutePath.toString(),
+                "-scheme", scheme.get(),
+                "-destination", destinationDesc,
+                "-resultBundlePath", xcResultFile.absolutePath,
+            )
+            logger.info("Command : ${args.joinToString(" ")}")
+            execOperations.executeQuietly(args)
+        }
     }
 
     private fun requireXcodeBuild() {
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt
new file mode 100644
index 0000000..6f83f22
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.benchmark.darwin.gradle
+
+import org.gradle.api.Project
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+
+/**
+ * A service that manages simulators / devices. Also manages booting up and tearing down instances
+ */
+interface XCodeBuildService : BuildService<BuildServiceParameters.None> {
+    companion object {
+        const val XCODE_BUILD_SERVICE_NAME = "DarwinXCodeBuildService"
+    }
+}
+
+/**
+ * Register the [XCodeBuildService] as a shared gradle service.
+ */
+fun Project.configureXCodeBuildService() {
+    gradle.sharedServices.registerIfAbsent(
+        XCodeBuildService.XCODE_BUILD_SERVICE_NAME,
+        XCodeBuildService::class.java
+    ) { spec ->
+        // Run one xcodebuild at a time.
+        spec.maxParallelUsages.set(1)
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt
new file mode 100644
index 0000000..bfa0fc6
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.benchmark.darwin.gradle
+
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
+import androidx.benchmark.darwin.gradle.xcode.SimulatorRuntimes
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import org.gradle.process.ExecOperations
+
+/**
+ * Controls XCode Simulator instances.
+ */
+class XCodeSimCtrl(
+    private val execOperations: ExecOperations,
+    private val destination: String
+) {
+
+    private var destinationDesc: String? = null
+    private var deviceId: String? = null
+
+    // A device type looks something like
+    // platform=iOS Simulator,name=iPhone 13,OS=15.2
+
+    fun start(block: (destinationDesc: String) -> Unit) {
+        try {
+            val instance = boot(destination, execOperations)
+            destinationDesc = instance.destinationDesc
+            deviceId = instance.deviceId
+            block(destinationDesc!!)
+        } finally {
+            val id = deviceId
+            if (id != null) {
+                shutDownAndDelete(execOperations, id)
+            }
+        }
+    }
+
+    companion object {
+        private const val PLATFORM_KEY = "platform"
+        private const val NAME_KEY = "name"
+        private const val RUNTIME_KEY = "OS"
+        private const val IOS_SIMULATOR = "iOS Simulator"
+        private const val IPHONE_PRODUCT_FAMILY = "iPhone"
+
+        /** Simulator metadata */
+        internal data class SimulatorInstance(
+            /* The full device descriptor. */
+            val destinationDesc: String,
+            /* The unique device UUID if we end up booting the device. */
+            val deviceId: String? = null
+        )
+
+        internal fun boot(
+            destination: String,
+            execOperations: ExecOperations
+        ): SimulatorInstance {
+            val parsed = parse(destination)
+            return when (platform(parsed)) {
+                // Simulator needs to be booted up.
+                IOS_SIMULATOR -> bootSimulator(destination, parsed, execOperations)
+                // For other destinations, we don't have much to do.
+                else -> SimulatorInstance(destinationDesc = destination)
+            }
+        }
+
+        private fun discoverSimulatorRuntimeVersion(
+            execOperations: ExecOperations
+        ): String? {
+            val json = executeCommand(
+                execOperations, listOf(
+                    "xcrun", "simctl", "list", "runtimes", "--json"
+                )
+            )
+            val simulatorRuntimes = GsonHelpers.gson().fromJson(json, SimulatorRuntimes::class.java)
+            // There is usually one version of the simulator runtime available per xcode version
+            val supported = simulatorRuntimes.runtimes.firstOrNull { runtime ->
+                runtime.isAvailable && runtime.supportedDeviceTypes.any { deviceType ->
+                    deviceType.productFamily == IPHONE_PRODUCT_FAMILY
+                }
+            }
+            return supported?.version
+        }
+
+        private fun bootSimulator(
+            destination: String,
+            parsed: Map<String, String>,
+            execOperations: ExecOperations
+        ): SimulatorInstance {
+            val deviceName = deviceName(parsed)
+            val supported = discoverSimulatorRuntimeVersion(execOperations)
+            // While this is not strictly correct, these versions should be pretty close.
+            val runtimeVersion = supported ?: runtimeVersion(parsed)
+            check(deviceName != null && runtimeVersion != null) {
+                "Invalid destination spec: $destination"
+            }
+            val deviceId = executeCommand(
+                execOperations, listOf(
+                    "xcrun",
+                    "simctl",
+                    "create",
+                    deviceName, // Use the deviceName as the name
+                    deviceName,
+                    "iOS$runtimeVersion"
+                )
+            )
+            check(deviceId.isNotBlank()) {
+                "Invalid device id for simulator: $deviceId (Destination: $destination)"
+            }
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "boot", deviceId)
+            )
+            // Return a simulator instance with the new descriptor + device id
+            return SimulatorInstance(destinationDesc = "id=$deviceId", deviceId = deviceId)
+        }
+
+        internal fun shutDownAndDelete(
+            execOperations: ExecOperations,
+            deviceId: String
+        ) {
+            // Cleans up the instance of the simulator that was booted up.
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "shutdown", deviceId)
+            )
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "delete", deviceId)
+            )
+        }
+
+        private fun executeCommand(execOperations: ExecOperations, args: List<String>): String {
+            val output = ByteArrayOutputStream()
+            output.use {
+                execOperations.exec { spec ->
+                    spec.commandLine = args
+                    spec.standardOutput = output
+                }
+                val input = ByteArrayInputStream(output.toByteArray())
+                return input.use {
+                    // Trimming is important here, otherwise ExecOperations encodes the string
+                    // with shell specific escape sequences which mangle the device
+                    input.reader().readText().trim()
+                }
+            }
+        }
+
+        private fun platform(parsed: Map<String, String>): String? {
+            return parsed[PLATFORM_KEY]
+        }
+
+        private fun deviceName(parsed: Map<String, String>): String? {
+            return parsed[NAME_KEY]
+        }
+
+        private fun runtimeVersion(parsed: Map<String, String>): String? {
+            return parsed[RUNTIME_KEY]
+        }
+
+        private fun parse(destination: String): Map<String, String> {
+            return destination.splitToSequence(",")
+                .map { split ->
+                    check(split.contains("=")) {
+                        "Invalid destination spec: $destination"
+                    }
+                    val (key, value) = split.split("=", limit = 2)
+                    key.trim() to value.trim()
+                }
+                .toMap()
+        }
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt
new file mode 100644
index 0000000..3d9414c
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.benchmark.darwin.gradle.xcode
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+
+object GsonHelpers {
+    internal fun gsonBuilder(): GsonBuilder {
+        val builder = GsonBuilder()
+        builder.registerTypeAdapter(
+            ActionsTestSummaryGroupOrMeta::class.java,
+            ActionTestSummaryDeserializer()
+        )
+        return builder
+    }
+
+    fun gson(): Gson {
+        return gsonBuilder().create()
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
similarity index 93%
rename from benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
rename to benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
index 863da27..237a8e2 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
@@ -16,8 +16,6 @@
 
 package androidx.benchmark.darwin.gradle.xcode
 
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
 import com.google.gson.JsonDeserializationContext
 import com.google.gson.JsonDeserializer
 import com.google.gson.JsonElement
@@ -203,10 +201,10 @@
         context: JsonDeserializationContext
     ): ActionsTestSummaryGroupOrMeta {
         return if (checkType(jsonElement, ACTION_TEST_SUMMARY_GROUP)) {
-            val adapter = Models.gson().getAdapter(ActionTestSummaryGroup::class.java)
+            val adapter = GsonHelpers.gson().getAdapter(ActionTestSummaryGroup::class.java)
             adapter.fromJson(jsonElement.toString())
         } else if (checkType(jsonElement, ACTION_TEST_SUMMARY_META)) {
-            val adapter = Models.gson().getAdapter(ActionTestSummaryMeta::class.java)
+            val adapter = GsonHelpers.gson().getAdapter(ActionTestSummaryMeta::class.java)
             adapter.fromJson(jsonElement.toString())
         } else {
             reportException(jsonElement)
@@ -227,7 +225,7 @@
             val json = jsonElement.asJsonObject
             val jsonType: JsonElement? = json.get(TYPE)
             if (jsonType != null && jsonType.isJsonObject) {
-                val adapter = Models.gson().getAdapter(TypeDefinition::class.java)
+                val adapter = GsonHelpers.gson().getAdapter(TypeDefinition::class.java)
                 val type = adapter.fromJson(jsonType.toString())
                 return type.name == name
             }
@@ -324,18 +322,3 @@
         return activitySummaries.title()
     }
 }
-
-object Models {
-    internal fun gsonBuilder(): GsonBuilder {
-        val builder = GsonBuilder()
-        builder.registerTypeAdapter(
-            ActionsTestSummaryGroupOrMeta::class.java,
-            ActionTestSummaryDeserializer()
-        )
-        return builder
-    }
-
-    fun gson(): Gson {
-        return gsonBuilder().create()
-    }
-}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt
new file mode 100644
index 0000000..5255a96
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.benchmark.darwin.gradle.xcode
+
+/**
+ * A representation of the output of `xcrun simctl runtimes list --json`.
+ *
+ * That produces an object that contains a [List] of [SimulatorRuntime].
+ */
+data class SimulatorRuntimes(
+    val runtimes: List<SimulatorRuntime>
+)
+
+/**
+ * An XCode simulator runtime. The serialized representation looks something like:
+ *
+ * ```json
+ * {
+ *   "bundlePath" : "...\/Profiles\/Runtimes\/watchOS.simruntime",
+ *   "buildversion" : "19S51",
+ *   "runtimeRoot" : "....\/Runtimes\/watchOS.simruntime\/Contents\/Resources\/RuntimeRoot",
+ *   "identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-8-3",
+ *   "version" : "8.3",
+ *    "isAvailable" : true,
+ *    "supportedDeviceTypes" : [
+ *      ...
+ *    ]
+ * }
+ * ```
+ */
+data class SimulatorRuntime(
+    val identifier: String,
+    val version: String,
+    val isAvailable: Boolean,
+    val supportedDeviceTypes: List<SupportedDeviceType>
+)
+
+/**
+ * A serialized supported device type has a representation that looks like:
+ *
+ * ```json
+ * {
+ *  "bundlePath" : "...\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 6s.simdevicetype",
+ *  "name" : "iPhone 6s",
+ *  "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s",
+ *  "productFamily" : "iPhone"
+ *  }
+ * ```
+ */
+data class SupportedDeviceType(
+    val bundlePath: String,
+    val name: String,
+    val identifier: String,
+    val productFamily: String
+)
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
index c0c3c15..953b771 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
@@ -29,7 +29,7 @@
 ) {
     fun parseResults(): Pair<ActionsInvocationRecord, List<ActionTestSummary>> {
         val json = commandExecutor(xcRunCommand())
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
         val summaries = record.actions.testReferences().flatMap { testRef ->
             val summary = commandExecutor(xcRunCommand(testRef))
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
index 663b7ea..20bc13a 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
@@ -17,7 +17,7 @@
 import androidx.benchmark.darwin.gradle.xcode.ActionTestPlanRunSummaries
 import androidx.benchmark.darwin.gradle.xcode.ActionTestSummary
 import androidx.benchmark.darwin.gradle.xcode.ActionsInvocationRecord
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,7 +28,7 @@
     @Test
     fun parseXcResultOutputs() {
         val json = testData(XCRESULT_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
         assertThat(record.actions.testReferences().size).isEqualTo(1)
         assertThat(record.metrics.size()).isEqualTo(1)
@@ -38,7 +38,7 @@
     @Test
     fun parseTestsReferenceOutput() {
         val json = testData(XC_TESTS_REFERENCE_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val testPlanSummaries = gson.fromJson(json, ActionTestPlanRunSummaries::class.java)
         val testSummaryMetas = testPlanSummaries.testSummaries()
         assertThat(testSummaryMetas.size).isEqualTo(1)
@@ -49,7 +49,7 @@
     @Test
     fun parseTestOutput() {
         val json = testData(XC_TEST_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val testSummary = gson.fromJson(json, ActionTestSummary::class.java)
         assertThat(testSummary.title()).isNotEmpty()
         assertThat(testSummary.isSuccessful()).isTrue()
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
index 00468a9..2ce55a7 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
@@ -15,7 +15,7 @@
  */
 
 import androidx.benchmark.darwin.gradle.skia.Metrics
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import androidx.benchmark.darwin.gradle.xcode.XcResultParser
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assume.assumeTrue
@@ -51,7 +51,7 @@
         assertThat(record.metrics.size()).isEqualTo(2)
         assertThat(summaries.isNotEmpty()).isTrue()
         val metrics = Metrics.buildMetrics(record, summaries)
-        val json = Models.gsonBuilder()
+        val json = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
             .toJson(metrics)
diff --git a/benchmark/benchmark-darwin-xcode/.gitignore b/benchmark/benchmark-darwin-xcode/.gitignore
index 78f146f..332a4f0 100644
--- a/benchmark/benchmark-darwin-xcode/.gitignore
+++ b/benchmark/benchmark-darwin-xcode/.gitignore
@@ -1,7 +1,5 @@
 # XCode Projects
 *.xcodeproj
-Info.plist
 
 # XCode results
 *.xcresult
-
diff --git a/benchmark/benchmark-darwin-xcode/projects/App.plist b/benchmark/benchmark-darwin-xcode/projects/App.plist
new file mode 100644
index 0000000..47ef031
--- /dev/null
+++ b/benchmark/benchmark-darwin-xcode/projects/App.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist b/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist
new file mode 100644
index 0000000..6c40a6c
--- /dev/null
+++ b/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml b/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
index a7c06ed..1616cfa 100644
--- a/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
+++ b/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
@@ -7,7 +7,7 @@
     type: application
     platform: iOS
     info:
-      path: Info.plist
+      path: App.plist
     sources:
       - path: '../iosSources/main'
     scheme:
@@ -23,7 +23,7 @@
     type: bundle.unit-test
     platform: iOS
     info:
-      path: Info.plist
+      path: Benchmark.plist
     sources:
       - path: '../iosAppUnitTests/main'
     scheme:
@@ -38,4 +38,4 @@
   CODE_SIGNING_REQUIRED: 'NO'
   CODE_SIGN_ENTITLEMENTS: ''
   CODE_SIGNING_ALLOWED: 'NO'
-  IPHONEOS_DEPLOYMENT_TARGET: 15.2
+  IPHONEOS_DEPLOYMENT_TARGET: 15.0
diff --git a/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml b/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
index c40aa28..910be36 100644
--- a/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
+++ b/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
@@ -7,7 +7,7 @@
     type: application
     platform: iOS
     info:
-      path: Info.plist
+      path: App.plist
     sources:
       - path: '../iosSources/main'
     scheme:
@@ -23,7 +23,7 @@
     type: bundle.unit-test
     platform: iOS
     info:
-      path: Info.plist
+      path: Benchmark.plist
     sources:
       - path: '../iosAppUnitTests/main'
     scheme:
@@ -38,4 +38,4 @@
   CODE_SIGNING_REQUIRED: 'NO'
   CODE_SIGN_ENTITLEMENTS: ''
   CODE_SIGNING_ALLOWED: 'NO'
-  IPHONEOS_DEPLOYMENT_TARGET: 15.2
+  IPHONEOS_DEPLOYMENT_TARGET: 15.0
diff --git a/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
index 63adf8e..9ddb585 100644
--- a/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
@@ -7,6 +7,10 @@
     method public void collectBaselineProfile(String packageName, optional int iterations, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method public void collectBaselineProfile(String packageName, optional int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method public void collectBaselineProfile(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, optional boolean strictStability, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
   }
 
   public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
index 7b09cb7..6ccf203 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
@@ -19,8 +19,10 @@
 import android.Manifest
 import androidx.annotation.RequiresApi
 import androidx.benchmark.Arguments
+import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
 import androidx.benchmark.macro.MacrobenchmarkScope
 import androidx.benchmark.macro.collectBaselineProfile
+import androidx.benchmark.macro.collectStableBaselineProfile
 import androidx.test.rule.GrantPermissionRule
 import org.junit.Assume.assumeTrue
 import org.junit.rules.RuleChain
@@ -102,5 +104,39 @@
         )
     }
 
+    /**
+     * Collects baseline profiles for a critical user journey, while ensuring that the generated
+     * profiles are stable for a minimum of [stableIterations].
+     *
+     * @param packageName Package name of the app for which profiles are to be generated.
+     * @param maxIterations Maximum number of iterations to run for when collecting profiles.
+     * @param stableIterations Minimum number of iterations for while baseline profiles have to be stable.
+     * @param strictStability Enforce if the generated profile was stable
+     * @param packageFilters List of package names to use as a filter for the generated profiles.
+     *  By default no filters are applied. Note that this works only when the code is not obfuscated.
+     *  Package filters are only applied after the profiles are deemed stable.
+     * @param [profileBlock] defines the critical user journey.
+     */
+    @JvmOverloads
+    @ExperimentalStableBaselineProfilesApi
+    public fun collectStableBaselineProfile(
+        packageName: String,
+        maxIterations: Int,
+        stableIterations: Int = 3,
+        strictStability: Boolean = false,
+        packageFilters: List<String> = emptyList(),
+        profileBlock: MacrobenchmarkScope.() -> Unit
+    ) {
+        collectStableBaselineProfile(
+            uniqueName = currentDescription.toUniqueName(),
+            packageName = packageName,
+            stableIterations = stableIterations,
+            maxIterations = maxIterations,
+            strictStability = strictStability,
+            packageFilters = packageFilters,
+            profileBlock = profileBlock
+        )
+    }
+
     private fun Description.toUniqueName() = testClass.simpleName + "_" + methodName
 }
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index 301070a..8f5b8c0 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -58,6 +58,9 @@
   @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
   }
 
+  @kotlin.RequiresOptIn(message="The stable Baseline profile generation API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalStableBaselineProfilesApi {
+  }
+
   public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
     ctor public FrameTimingMetric();
   }
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index aa128ff..eaaf6ef 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -16,6 +16,9 @@
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, optional int iterations, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, optional int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, optional boolean strictStability, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BatteryCharge {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
new file mode 100644
index 0000000..cf9a3e3
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package androidx.benchmark.macro
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class ProfileInstallBroadcastTest {
+    @Test
+    fun installProfile() {
+        assertNull(ProfileInstallBroadcast.installProfile(Packages.TARGET))
+    }
+
+    @Test
+    fun skipFileOperation() {
+        assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "WRITE_SKIP_FILE"))
+        assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "DELETE_SKIP_FILE"))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun saveProfile() {
+        assertNull(ProfileInstallBroadcast.saveProfile(Packages.TARGET))
+    }
+
+    @Test
+    fun dropShaderCache() {
+        assertNull(ProfileInstallBroadcast.dropShaderCache(Packages.TARGET))
+    }
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index 89c624e..8b370af 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -44,26 +44,9 @@
     packageFilters: List<String> = emptyList(),
     profileBlock: MacrobenchmarkScope.() -> Unit,
 ) {
-    require(
-        Build.VERSION.SDK_INT >= 33 ||
-            (Build.VERSION.SDK_INT >= 28 && Shell.isSessionRooted())
-    ) {
-        "Baseline Profile collection requires API 33+, or a rooted" +
-            " device running API 28 or higher and rooted adb session (via `adb root`)."
-    }
-
-    getInstalledPackageInfo(packageName) // throws clearly if not installed
-
+    val scope = buildMacrobenchmarkScope(packageName)
     val startTime = System.nanoTime()
-    val scope = MacrobenchmarkScope(packageName, launchWithClearTask = true)
-
-    val killProcessBlock = {
-        // When generating baseline profiles we want to default to using
-        // killProcess if the session is rooted. This is so we can collect
-        // baseline profiles for System Apps.
-        scope.killProcess(useKillAll = Shell.isSessionRooted())
-        Thread.sleep(Arguments.killProcessDelayMillis)
-    }
+    val killProcessBlock = scope.killProcessBlock()
 
     // always kill the process at beginning of a collection.
     killProcessBlock.invoke()
@@ -90,69 +73,215 @@
         }
 
         check(unfilteredProfile.isNotBlank()) {
+            """
+                Generated Profile is empty, before filtering.
+                Ensure your profileBlock invokes the target app, and
+                runs a non-trivial amount of code.
+            """.trimIndent()
+        }
+        // Filter
+        val profile = filterProfileRulesToTargetP(unfilteredProfile)
+        // Report
+        reportResults(profile, packageFilters, uniqueName, startTime)
+    } finally {
+        killProcessBlock.invoke()
+    }
+}
+
+/**
+ * Collects baseline profiles using a given [profileBlock], while additionally
+ * waiting until they are stable.
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresApi(28)
+@JvmOverloads
+fun collectStableBaselineProfile(
+    uniqueName: String,
+    packageName: String,
+    stableIterations: Int,
+    maxIterations: Int,
+    strictStability: Boolean = false,
+    packageFilters: List<String> = emptyList(),
+    profileBlock: MacrobenchmarkScope.() -> Unit
+) {
+    val scope = buildMacrobenchmarkScope(packageName)
+    val startTime = System.nanoTime()
+    val killProcessBlock = scope.killProcessBlock()
+    // always kill the process at beginning of a collection.
+    killProcessBlock.invoke()
+
+    try {
+        var stableCount = 1
+        var lastProfile: String? = null
+        var iteration = 1
+
+        while (iteration <= maxIterations) {
+            userspaceTrace("generate profile for $packageName ($iteration)") {
+                val mode = CompilationMode.Partial(
+                    baselineProfileMode = BaselineProfileMode.Disable,
+                    warmupIterations = 1
+                )
+                if (iteration == 1) {
+                    Log.d(TAG, "Resetting compiled state for $packageName for stable profiles.")
+                    mode.resetAndCompile(
+                        packageName = packageName,
+                        killProcessBlock = killProcessBlock
+                    ) {
+                        scope.iteration = iteration
+                        profileBlock(scope)
+                    }
+                } else {
+                    // Don't reset for subsequent iterations
+                    Log.d(TAG, "Killing package $packageName")
+                    killProcessBlock()
+                    mode.compileImpl(packageName = packageName,
+                        killProcessBlock = killProcessBlock
+                    ) {
+                        scope.iteration = iteration
+                        Log.d(TAG, "Compile iteration (${scope.iteration}) for $packageName")
+                        profileBlock(scope)
+                    }
+                }
+            }
+            val unfilteredProfile = if (Build.VERSION.SDK_INT >= 33) {
+                extractProfile(packageName)
+            } else {
+                extractProfileRooted(packageName)
+            }
+
+            // Check stability
+            val lastRuleSet = lastProfile?.lines()?.toSet() ?: emptySet()
+            val existingRuleSet = unfilteredProfile.lines().toSet()
+            if (lastRuleSet != existingRuleSet) {
+                if (iteration != 1) {
+                    Log.d(TAG, "Unstable profiles during iteration $iteration")
+                }
+                lastProfile = unfilteredProfile
+                stableCount = 1
+            } else {
+                Log.d(TAG,
+                    "Profiles stable in iteration $iteration (for $stableCount iterations)"
+                )
+                stableCount += 1
+                if (stableCount == stableIterations) {
+                    Log.d(TAG, "Baseline profile for $packageName is stable.")
+                    break
+                }
+            }
+            iteration += 1
+        }
+
+        if (strictStability) {
+            check(stableCount == stableIterations) {
+                "Baseline profiles for $packageName are not stable after $maxIterations."
+            }
+        }
+
+        check(!lastProfile.isNullOrBlank()) {
             "Generated Profile is empty, before filtering. Ensure your profileBlock" +
                 " invokes the target app, and runs a non-trivial amount of code"
         }
 
-        val profile = filterProfileRulesToTargetP(unfilteredProfile)
-
-        // Build a startup profile
-        var startupProfile: String? = null
-        if (Arguments.enableStartupProfiles) {
-            startupProfile =
-                startupProfile(profile, includeStartupOnly = Arguments.strictStartupProfiles)
-        }
-
-        // Filter profile if necessary based on filters
-        val filteredProfile = applyPackageFilters(profile, packageFilters)
-
-        // Write a file with a timestamp to be able to disambiguate between runs with the same
-        // unique name.
-
-        val fileName = "$uniqueName-baseline-prof.txt"
-        val absolutePath = Outputs.writeFile(fileName, "baseline-profile") {
-            it.writeText(filteredProfile)
-        }
-        var startupProfilePath: String? = null
-        if (startupProfile != null) {
-            val startupProfileFileName = "$uniqueName-startup-prof.txt"
-            startupProfilePath = Outputs.writeFile(startupProfileFileName, "startup-profile") {
-                it.writeText(startupProfile)
-            }
-        }
-        val tsFileName = "$uniqueName-baseline-prof-${Outputs.dateToFileName()}.txt"
-        val tsAbsolutePath = Outputs.writeFile(tsFileName, "baseline-profile-ts") {
-            Log.d(TAG, "Pull Baseline Profile with: `adb pull \"${it.absolutePath}\" .`")
-            it.writeText(filteredProfile)
-        }
-        var tsStartupAbsolutePath: String? = null
-        if (startupProfile != null) {
-            val tsStartupFileName = "$uniqueName-startup-prof-${Outputs.dateToFileName()}.txt"
-            tsStartupAbsolutePath = Outputs.writeFile(tsStartupFileName, "startup-profile-ts") {
-                Log.d(TAG, "Pull Startup Profile with: `adb pull \"${it.absolutePath}\" .`")
-                it.writeText(startupProfile)
-            }
-        }
-
-        val totalRunTime = System.nanoTime() - startTime
-        val results = Summary(
-            totalRunTime = totalRunTime,
-            profilePath = absolutePath,
-            profileTsPath = tsAbsolutePath,
-            startupProfilePath = startupProfilePath,
-            startupTsProfilePath = tsStartupAbsolutePath
-        )
-        InstrumentationResults.instrumentationReport {
-            val summary = summaryRecord(results)
-            ideSummaryRecord(summaryV1 = summary, summaryV2 = summary)
-            Log.d(TAG, "Total Run Time Ns: $totalRunTime")
-        }
+        val profile = filterProfileRulesToTargetP(lastProfile)
+        reportResults(profile, packageFilters, uniqueName, startTime)
     } finally {
         killProcessBlock.invoke()
     }
 }
 
 /**
+ * Builds a [MacrobenchmarkScope] instance after checking for the necessary pre-requisites.
+ */
+private fun buildMacrobenchmarkScope(packageName: String): MacrobenchmarkScope {
+    require(
+        Build.VERSION.SDK_INT >= 33 ||
+            (Build.VERSION.SDK_INT >= 28 && Shell.isSessionRooted())
+    ) {
+        "Baseline Profile collection requires API 33+, or a rooted" +
+            " device running API 28 or higher and rooted adb session (via `adb root`)."
+    }
+    getInstalledPackageInfo(packageName) // throws clearly if not installed
+    return MacrobenchmarkScope(packageName, launchWithClearTask = true)
+}
+
+/**
+ * Builds a function that can kill the target process using the provided [MacrobenchmarkScope].
+ */
+private fun MacrobenchmarkScope.killProcessBlock(): () -> Unit {
+    val killProcessBlock = {
+        // When generating baseline profiles we want to default to using
+        // killProcess if the session is rooted. This is so we can collect
+        // baseline profiles for System Apps.
+        this.killProcess(useKillAll = Shell.isSessionRooted())
+        Thread.sleep(Arguments.killProcessDelayMillis)
+    }
+    return killProcessBlock
+}
+
+/**
+ * Reports the results after having collected baseline profiles.
+ */
+private fun reportResults(
+    profile: String,
+    packageFilters: List<String>,
+    uniqueFilePrefix: String,
+    startTime: Long
+) {
+    // Build a startup profile
+    var startupProfile: String? = null
+    if (Arguments.enableStartupProfiles) {
+        startupProfile =
+            startupProfile(profile, includeStartupOnly = Arguments.strictStartupProfiles)
+    }
+
+    // Filter profile if necessary based on filters
+    val filteredProfile = applyPackageFilters(profile, packageFilters)
+
+    // Write a file with a timestamp to be able to disambiguate between runs with the same
+    // unique name.
+
+    val fileName = "$uniqueFilePrefix-baseline-prof.txt"
+    val absolutePath = Outputs.writeFile(fileName, "baseline-profile") {
+        it.writeText(filteredProfile)
+    }
+    var startupProfilePath: String? = null
+    if (startupProfile != null) {
+        val startupProfileFileName = "$uniqueFilePrefix-startup-prof.txt"
+        startupProfilePath = Outputs.writeFile(startupProfileFileName, "startup-profile") {
+            it.writeText(startupProfile)
+        }
+    }
+    val tsFileName = "$uniqueFilePrefix-baseline-prof-${Outputs.dateToFileName()}.txt"
+    val tsAbsolutePath = Outputs.writeFile(tsFileName, "baseline-profile-ts") {
+        Log.d(TAG, "Pull Baseline Profile with: `adb pull \"${it.absolutePath}\" .`")
+        it.writeText(filteredProfile)
+    }
+    var tsStartupAbsolutePath: String? = null
+    if (startupProfile != null) {
+        val tsStartupFileName = "$uniqueFilePrefix-startup-prof-${Outputs.dateToFileName()}.txt"
+        tsStartupAbsolutePath = Outputs.writeFile(tsStartupFileName, "startup-profile-ts") {
+            Log.d(TAG, "Pull Startup Profile with: `adb pull \"${it.absolutePath}\" .`")
+            it.writeText(startupProfile)
+        }
+    }
+
+    val totalRunTime = System.nanoTime() - startTime
+    val results = Summary(
+        totalRunTime = totalRunTime,
+        profilePath = absolutePath,
+        profileTsPath = tsAbsolutePath,
+        startupProfilePath = startupProfilePath,
+        startupTsProfilePath = tsStartupAbsolutePath
+    )
+    InstrumentationResults.instrumentationReport {
+        val summary = summaryRecord(results)
+        ideSummaryRecord(summaryV1 = summary, summaryV2 = summary)
+        Log.d(TAG, "Total Run Time Ns: $totalRunTime")
+    }
+}
+
+/**
  * Use `pm dump-profiles` to get profile from the target app,
  * which puts results in `/data/misc/profman/`
  *
@@ -201,16 +330,29 @@
     // When compiling with CompilationMode.SpeedProfile, ART stores the profile in one of
     // 2 locations. The `ref` profile path, or the `current` path.
     // The `current` path is eventually merged  into the `ref` path after background dexopt.
-    for (currentPath in pathOptions) {
+    val profiles = pathOptions.mapNotNull { currentPath ->
         Log.d(TAG, "Using profile location: $currentPath")
         val profile = Shell.executeScriptCaptureStdout(
             "profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath"
         )
-        if (profile.isNotBlank()) {
-            return profile
+        profile.ifBlank { null }
+    }
+    if (profiles.isEmpty()) {
+        throw IllegalStateException("The profile is empty.")
+    }
+    // Merge rules
+    val rules = mutableSetOf<String>()
+    profiles.forEach { profile ->
+        profile.lines().forEach { rule ->
+            rules.add(rule)
         }
     }
-    throw IllegalStateException("The profile is empty.")
+    val builder = StringBuilder()
+    rules.forEach {
+        builder.append(it)
+        builder.append("\n")
+    }
+    return builder.toString()
 }
 
 @VisibleForTesting
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
similarity index 68%
copy from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
copy to benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
index dfb2685..de9746e 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
@@ -14,13 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.benchmark.macro
 
-import androidx.compose.ui.graphics.Color
-
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+@RequiresOptIn(message = "The stable Baseline profile generation API is experimental.")
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalStableBaselineProfilesApi
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
index 9199b83..4c2451d 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.graphics.Point
+import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
 import androidx.benchmark.macro.junit4.BaselineProfileRule
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -72,6 +73,37 @@
         )
     }
 
+    @Test
+    @OptIn(ExperimentalStableBaselineProfilesApi::class)
+    fun stableBaselineProfiles() {
+        baselineRule.collectStableBaselineProfile(
+            packageName = "androidx.benchmark.integration.macrobenchmark.target",
+            stableIterations = 3,
+            maxIterations = 10,
+            profileBlock = {
+                val intent = Intent()
+                intent.action = ACTION
+                startActivityAndWait(intent)
+                val recycler = device.wait(
+                    Until.findObject(
+                        By.res(
+                            PACKAGE_NAME,
+                            RESOURCE_ID
+                        )
+                    ),
+                    TIMEOUT
+                )
+                // Setting a gesture margin is important otherwise gesture nav is triggered.
+                recycler.setGestureMargin(device.displayWidth / 5)
+                repeat(10) {
+                    // From center we scroll 2/3 of it which is 1/3 of the screen.
+                    recycler.drag(Point(0, recycler.visibleCenter.y / 3))
+                    device.waitForIdle()
+                }
+            }
+        )
+    }
+
     companion object {
         private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
         private const val ACTION =
diff --git a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
index 951e8ee..000db52 100644
--- a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
@@ -23,11 +23,15 @@
  * @hide
  */
 interface ICustomTabsCallback {
-    void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
-    void extraCallback(String callbackName, in Bundle args) = 2;
+    oneway void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
+    oneway void extraCallback(String callbackName, in Bundle args) = 2;
+
+    // Not defined with 'oneway' to preserve the calling order among |onPostMessage()| and related calls.
     void onMessageChannelReady(in Bundle extras) = 3;
     void onPostMessage(String message, in Bundle extras) = 4;
-    void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
+    oneway void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
+
+    // API with return value cannot be 'oneway'.
     Bundle extraCallbackWithResult(String callbackName, in Bundle args) = 6;
-    void onActivityResized(int height, int width, in Bundle extras) = 7;
+    oneway void onActivityResized(int height, int width, in Bundle extras) = 7;
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index d82baf1..c0a5ae5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -17,7 +17,6 @@
 package androidx.build
 
 import androidx.benchmark.gradle.BenchmarkPlugin
-import androidx.build.AndroidXImplPlugin.Companion.CHECK_RELEASE_READY_TASK
 import androidx.build.AndroidXImplPlugin.Companion.TASK_TIMEOUT_MINUTES
 import androidx.build.Release.DEFAULT_PUBLISH_CONFIG
 import androidx.build.SupportConfig.BUILD_TOOLS_VERSION
@@ -836,7 +835,6 @@
             if (extension.type != LibraryType.SAMPLES) {
                 val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
                 if (verifyDependencyVersionsTask != null) {
-                    project.createCheckReleaseReadyTask(listOf(verifyDependencyVersionsTask))
                     taskConfigurator(verifyDependencyVersionsTask)
                 }
             }
@@ -845,7 +843,6 @@
 
     companion object {
         const val BUILD_TEST_APKS_TASK = "buildTestApks"
-        const val CHECK_RELEASE_READY_TASK = "checkReleaseReady"
         const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
         const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
         const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
@@ -906,18 +903,6 @@
 val Project.multiplatformExtension
     get() = extensions.findByType(KotlinMultiplatformExtension::class.java)
 
-/**
- * Creates the [CHECK_RELEASE_READY_TASK], which aggregates tasks that must pass for a
- * project to be considered ready for public release.
- */
-private fun Project.createCheckReleaseReadyTask(taskProviderList: List<TaskProvider<out Task>>) {
-    tasks.register(CHECK_RELEASE_READY_TASK) {
-        for (taskProvider in taskProviderList) {
-            it.dependsOn(taskProvider)
-        }
-    }
-}
-
 @Suppress("UNCHECKED_CAST")
 fun Project.getProjectsMap(): ConcurrentHashMap<String, String> {
     project.rootProject.extra.set(ACCESSED_PROJECTS_MAP_KEY, true)
diff --git a/busytown/androidx-native-mac-host-tests.sh b/busytown/androidx-native-mac-host-tests.sh
index b39a384..9c38c66 100755
--- a/busytown/androidx-native-mac-host-tests.sh
+++ b/busytown/androidx-native-mac-host-tests.sh
@@ -10,7 +10,10 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh allTests \
+# Setup simulators
+impl/androidx-native-mac-simulator-setup.sh
+
+impl/build.sh darwinBenchmarkResults allTests \
     --no-configuration-cache \
     -Pandroidx.ignoreTestFailures \
     -Pandroidx.displayTestOutput=false \
diff --git a/busytown/androidx-native-mac.sh b/busytown/androidx-native-mac.sh
index df2abf1..84d9387 100755
--- a/busytown/androidx-native-mac.sh
+++ b/busytown/androidx-native-mac.sh
@@ -7,6 +7,9 @@
 # disable GCP cache, these machines don't have credentials.
 export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
 
+# Setup simulators
+impl/androidx-native-mac-simulator-setup.sh
+
 impl/build.sh buildOnServer allTests :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
 
 # run a separate createArchive task to prepare a repository
diff --git a/busytown/impl/androidx-native-mac-simulator-setup.sh b/busytown/impl/androidx-native-mac-simulator-setup.sh
new file mode 100755
index 0000000..c10941e
--- /dev/null
+++ b/busytown/impl/androidx-native-mac-simulator-setup.sh
@@ -0,0 +1,8 @@
+XCODE_SIMULATORS=$(xcrun simctl list devices | grep "iPhone" | wc -l)
+if [ $XCODE_SIMULATORS == '0' ]; then
+  SIMULATOR_DEVICE=$(xcrun simctl create 'iPhone 12' 'iPhone 12' 'iOS15.0')
+  echo "Booting device $SIMULATOR_DEVICE"
+  xcrun simctl boot $SIMULATOR_DEVICE
+else
+  echo "Already have $XCODE_SIMULATORS simulators set up."
+fi
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
index 24f4e72..d5b344c 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/SurfaceRequestTest.kt
@@ -32,15 +32,15 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth
+import java.lang.ref.PhantomReference
+import java.lang.ref.ReferenceQueue
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.atomic.AtomicReference
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
 import org.mockito.Mockito
-import java.lang.ref.PhantomReference
-import java.lang.ref.ReferenceQueue
-import java.util.concurrent.TimeoutException
-import java.util.concurrent.atomic.AtomicReference
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -361,7 +361,8 @@
     companion object {
         private val FAKE_SIZE: Size by lazy { Size(0, 0) }
         private val FAKE_INFO: SurfaceRequest.TransformationInfo by lazy {
-            SurfaceRequest.TransformationInfo.of(Rect(), 0, Surface.ROTATION_0)
+            SurfaceRequest.TransformationInfo.of(Rect(), 0, Surface.ROTATION_0,
+                /*hasCameraTransform=*/true)
         }
         private val NO_OP_RESULT_LISTENER = Consumer { _: SurfaceRequest.Result? -> }
         private val MOCK_SURFACE = Mockito.mock(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 3ab6e95..947ac42f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -123,7 +123,7 @@
 
         SaveError saveError = null;
         String errorMessage = null;
-        Exception exception = null;
+        Throwable throwable = null;
         try (ImageProxy imageToClose = mImage;
              FileOutputStream output = new FileOutputStream(tempFile)) {
             byte[] bytes = imageToJpegByteArray(mImage, mJpegQuality);
@@ -151,10 +151,14 @@
             }
 
             exif.save();
+        } catch (OutOfMemoryError e) {
+            saveError = SaveError.UNKNOWN;
+            errorMessage = "Processing failed due to low memory.";
+            throwable = e;
         } catch (IOException | IllegalArgumentException e) {
             saveError = SaveError.FILE_IO_FAILED;
             errorMessage = "Failed to write temp file";
-            exception = e;
+            throwable = e;
         } catch (CodecFailedException e) {
             switch (e.getFailureType()) {
                 case ENCODE_FAILED:
@@ -171,10 +175,10 @@
                     errorMessage = "Failed to transcode mImage";
                     break;
             }
-            exception = e;
+            throwable = e;
         }
         if (saveError != null) {
-            postError(saveError, errorMessage, exception);
+            postError(saveError, errorMessage, throwable);
             tempFile.delete();
             return null;
         }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index 8eff18d..a7830a1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -452,11 +452,16 @@
         SurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
         if (cameraInternal != null && surfaceProvider != null && cropRect != null
                 && surfaceRequest != null) {
-            // TODO: when SurfaceProcessorNode exists, use SettableSurface.setRotationDegrees(int)
-            //  instead. However, this requires PreviewView to rely on relative rotation but not
-            //  target rotation.
-            surfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(cropRect,
-                    getRelativeRotation(cameraInternal), getAppTargetRotation()));
+            if (mNode == null) {
+                surfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(
+                        cropRect,
+                        getRelativeRotation(cameraInternal),
+                        getAppTargetRotation(),
+                        /*hasCameraTransform=*/true));
+            } else {
+                ((SettableSurface) mSessionDeferrableSurface).setRotationDegrees(
+                        getRelativeRotation(cameraInternal));
+            }
         }
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
index 523ad9d..88986d5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceRequest.java
@@ -858,6 +858,29 @@
         public abstract int getTargetRotation();
 
         /**
+         * Whether the {@link Surface} contains the camera transform.
+         *
+         * <p>The {@link Surface} may contain a transformation, which will be used by Android
+         * components such as {@link TextureView} and {@link SurfaceView} to transform the output.
+         * The app may need to handle the transformation differently based on whether this value
+         * exists.
+         *
+         * <ul>
+         * <li>If the producer is the camera, then the {@link Surface} will contain a
+         * transformation that represents the camera orientation. In that case, this method will
+         * return {@code true}.
+         * <li>If the producer is not the camera, for example, if the stream has been edited by
+         * CameraX, then the {@link Surface} will not contain any transformation. In that case,
+         * this method will return {@code false}.
+         * </ul>
+         *
+         * @return true if the producer writes the camera transformation to the {@link Surface}.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public abstract boolean hasCameraTransform();
+
+        /**
          * Creates new {@link TransformationInfo}
          *
          * <p> Internally public to be used in view artifact tests.
@@ -868,9 +891,10 @@
         @NonNull
         public static TransformationInfo of(@NonNull Rect cropRect,
                 @ImageOutputConfig.RotationDegreesValue int rotationDegrees,
-                @ImageOutputConfig.OptionalRotationValue int targetRotation) {
+                @ImageOutputConfig.OptionalRotationValue int targetRotation,
+                boolean hasCameraTransform) {
             return new AutoValue_SurfaceRequest_TransformationInfo(cropRect, rotationDegrees,
-                    targetRotation);
+                    targetRotation, hasCameraTransform);
         }
 
         // Hides public constructor.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index fbe5b37..b4e15fc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -37,6 +37,8 @@
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.LowMemoryQuirk;
 import androidx.camera.core.processing.Edge;
 import androidx.camera.core.processing.InternalImageProcessor;
 import androidx.camera.core.processing.Node;
@@ -57,7 +59,7 @@
 public class ProcessingNode implements Node<ProcessingNode.In, Void> {
 
     @NonNull
-    private final Executor mBlockingExecutor;
+    final Executor mBlockingExecutor;
     @Nullable
     final InternalImageProcessor mImageProcessor;
 
@@ -86,7 +88,12 @@
      */
     ProcessingNode(@NonNull Executor blockingExecutor,
             @Nullable InternalImageProcessor imageProcessor) {
-        mBlockingExecutor = blockingExecutor;
+        boolean isLowMemoryDevice = DeviceQuirks.get(LowMemoryQuirk.class) != null;
+        if (isLowMemoryDevice) {
+            mBlockingExecutor = CameraXExecutors.newSequentialExecutor(blockingExecutor);
+        } else {
+            mBlockingExecutor = blockingExecutor;
+        }
         mImageProcessor = imageProcessor;
     }
 
@@ -142,6 +149,9 @@
             }
         } catch (ImageCaptureException e) {
             sendError(request, e);
+        } catch (OutOfMemoryError e) {
+            sendError(request, new ImageCaptureException(
+                    ERROR_UNKNOWN, "Processing failed due to low memory.", e));
         } catch (RuntimeException e) {
             // For unexpected exceptions, throw an ERROR_UNKNOWN ImageCaptureException.
             sendError(request, new ImageCaptureException(ERROR_UNKNOWN, "Processing failed.", e));
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
index afcaf8e..801f381 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/DeviceQuirksLoader.java
@@ -49,6 +49,9 @@
         if (CaptureFailedRetryQuirk.load()) {
             quirks.add(new CaptureFailedRetryQuirk());
         }
+        if (LowMemoryQuirk.load()) {
+            quirks.add(new LowMemoryQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.java
new file mode 100644
index 0000000..c38eefd
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.java
@@ -0,0 +1,49 @@
+/*
+ * 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.camera.core.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: 235321365
+ *     Description: For the devices in low spec which may not have enough memory to process the
+ *     image cropping and apply effects in parallel.
+ *     Device(s): Samsung Galaxy A5, Motorola Moto G (3rd gen)
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class LowMemoryQuirk implements Quirk {
+
+    // TODO(b/258618028): Making a public API and giving developers the option to set the devices
+    //  having the Quirk.
+    private static final Set<String> DEVICE_MODELS = new HashSet<>(Arrays.asList(
+            "SM-A520W", // Samsung Galaxy A5
+            "MOTOG3" // Motorola Moto G (3rd gen)
+    ));
+
+    static boolean load() {
+        return DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
index 6fae56c..64bd82e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SettableSurface.java
@@ -411,7 +411,8 @@
     private void notifyTransformationInfoUpdate() {
         if (mProviderSurfaceRequest != null) {
             mProviderSurfaceRequest.updateTransformationInfo(
-                    TransformationInfo.of(mCropRect, mRotationDegrees, ROTATION_NOT_SPECIFIED));
+                    TransformationInfo.of(mCropRect, mRotationDegrees, ROTATION_NOT_SPECIFIED,
+                            /*hasCameraTransform=*/hasEmbeddedTransform()));
         }
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 5f64956..4559de9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -35,6 +35,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.processing.SettableSurface
 import androidx.camera.core.processing.SurfaceProcessorInternal
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -215,6 +216,66 @@
     }
 
     @Test
+    fun createSurfaceRequestWithProcessor_noCameraTransform() {
+        // Arrange: attach Preview without a SurfaceProvider.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: create pipeline in Preview and provide Surface.
+        val preview = createPreview(processor)
+        preview.mCurrentSurfaceRequest!!.setTransformationInfoListener(mainThreadExecutor()) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Get pending SurfaceRequest created by pipeline.
+        assertThat(transformationInfo!!.hasCameraTransform()).isFalse()
+    }
+
+    @Test
+    fun createSurfaceRequestWithoutProcessor_hasCameraTransform() {
+        // Arrange: attach Preview without a SurfaceProvider.
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: create pipeline in Preview and provide Surface.
+        val preview = createPreview()
+        preview.mCurrentSurfaceRequest!!.setTransformationInfoListener(mainThreadExecutor()) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Get pending SurfaceRequest created by pipeline.
+        assertThat(transformationInfo!!.hasCameraTransform()).isTrue()
+    }
+
+    @Test
+    fun setTargetRotationWithProcessor_rotationChangesOnSettableSurface() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+
+        // Act: create pipeline
+        val preview = createPreview(processor)
+        // Act: update target rotation
+        preview.targetRotation = Surface.ROTATION_0
+        shadowOf(getMainLooper()).idle()
+        // Assert that the rotation of the SettableFuture is updated based on ROTATION_0.
+        assertThat(preview.getSurfaceRotationDegrees()).isEqualTo(0)
+
+        // Act: update target rotation again.
+        preview.targetRotation = Surface.ROTATION_180
+        shadowOf(getMainLooper()).idle()
+        // Assert: the rotation of the SettableFuture is updated based on ROTATION_90.
+        assertThat(preview.getSurfaceRotationDegrees()).isEqualTo(180)
+
+        // Clean up
+        preview.onDetached()
+    }
+
+    private fun Preview.getSurfaceRotationDegrees(): Int {
+        return (this.sessionConfig.surfaces.single() as SettableSurface).rotationDegrees
+    }
+
+    @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
         val processor = FakeSurfaceProcessorInternal(
@@ -223,7 +284,7 @@
         )
 
         // Act: create pipeline in Preview and provide Surface.
-        val preview = createPreviewPipelineAndAttachProcessor(processor)
+        val preview = createPreview(processor)
         val surfaceRequest = preview.mCurrentSurfaceRequest!!
         var appSurfaceReadyToRelease = false
         surfaceRequest.provideSurface(appSurface, mainThreadExecutor()) {
@@ -263,7 +324,7 @@
         val processor = FakeSurfaceProcessorInternal(
             mainThreadExecutor()
         )
-        val preview = createPreviewPipelineAndAttachProcessor(processor)
+        val preview = createPreview(processor)
         val originalSessionConfig = preview.sessionConfig
 
         // Act: invoke the error listener.
@@ -438,9 +499,7 @@
         return Pair(surfaceRequest!!, transformationInfo!!)
     }
 
-    private fun createPreviewPipelineAndAttachProcessor(
-        surfaceProcessor: SurfaceProcessorInternal?
-    ): Preview {
+    private fun createPreview(surfaceProcessor: SurfaceProcessorInternal? = null): Preview {
         val preview = Preview.Builder()
             .setTargetRotation(Surface.ROTATION_0)
             .build()
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
index 484950f..542d22b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
 import androidx.camera.core.imagecapture.Utils.WIDTH
 import androidx.camera.core.imagecapture.Utils.createProcessingRequest
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.isSequentialExecutor
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.TestImageUtil.createJpegBytes
 import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
@@ -40,6 +41,7 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers.setStaticField
 
 /**
  * Unit tests for [ProcessingNode].
@@ -100,4 +102,14 @@
         assertThat(takePictureCallback.processFailure)
             .isInstanceOf(ImageCaptureException::class.java)
     }
+
+    @Test
+    fun singleExecutorForLowMemoryQuirkEnabled() {
+        listOf("sm-a520w", "motog3").forEach { model ->
+            setStaticField(Build::class.java, "MODEL", model)
+            assertThat(
+                isSequentialExecutor(ProcessingNode(mainThreadExecutor()).mBlockingExecutor)
+            ).isTrue()
+        }
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
index 96048cd..d2dca35 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SettableSurfaceTest.kt
@@ -188,6 +188,35 @@
     }
 
     @Test
+    fun createSurfaceRequest_hasCameraTransformSetCorrectly() {
+        assertThat(getSurfaceRequestHasTransform(true)).isTrue()
+        assertThat(getSurfaceRequestHasTransform(false)).isFalse()
+    }
+
+    /**
+     * Creates a [SettableSurface] with the given hasEmbeddedTransform value, and returns the
+     * [TransformationInfo.hasCameraTransform] from the [SurfaceRequest].
+     */
+    private fun getSurfaceRequestHasTransform(hasEmbeddedTransform: Boolean): Boolean {
+        // Arrange.
+        val surface = SettableSurface(
+            CameraEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
+            Matrix(), hasEmbeddedTransform, Rect(), 0, false
+        ) {}
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: get the hasCameraTransform bit from the SurfaceRequest.
+        surface.createSurfaceRequest(FakeCamera()).setTransformationInfoListener(
+            mainThreadExecutor()
+        ) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+        surface.close()
+        return transformationInfo!!.hasCameraTransform()
+    }
+
+    @Test
     fun setSourceSurfaceFutureAndProvide_surfaceIsPropagated() {
         // Arrange: set a ListenableFuture<Surface> as the source.
         var completer: CallbackToFutureAdapter.Completer<Surface>? = null
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 2df259f..8c33f0a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -455,7 +455,7 @@
             } else {
                 surfaceRequest.updateTransformationInfo(
                         SurfaceRequest.TransformationInfo.of(cropRect, relativeRotation,
-                                targetRotation));
+                                targetRotation, /*hasCameraTransform=*/true));
             }
         }
     }
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
index c553bc3..5df9f75 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewDeviceTest.kt
@@ -1099,7 +1099,8 @@
     private fun updateCropRectAndWaitForIdle(cropRect: Rect) {
         for (surfaceRequest in surfaceRequestList) {
             surfaceRequest.updateTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(cropRect, 0, Surface.ROTATION_0)
+                SurfaceRequest.TransformationInfo.of(cropRect, 0, Surface.ROTATION_0,
+                    /*hasCameraTransform=*/true)
             )
         }
         instrumentation.waitForIdleSync()
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
index a66b68d..6e0c7bf 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
@@ -205,8 +205,9 @@
         val previewTransformation = PreviewTransformation()
         previewTransformation.scaleType = scaleType
         previewTransformation.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of
-            (cropRect, rotationDegrees, FAKE_TARGET_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                cropRect, rotationDegrees, FAKE_TARGET_ROTATION, /*hasCameraTransform=*/true
+            ),
             surfaceSize, isFrontCamera
         )
         val meteringPointFactory = PreviewViewMeteringPointFactory(previewTransformation)
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
index ddf2b9e..52a18af 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
@@ -111,7 +111,10 @@
     private fun isCropRectAspectRatioMatchPreviewView(cropRect: Rect): Boolean {
         mPreviewTransform.setTransformationInfo(
             // Height and width is swapped because rotation is 90°.
-            SurfaceRequest.TransformationInfo.of(cropRect, 90, ARBITRARY_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                cropRect, 90, ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
+            ),
             SURFACE_SIZE,
             BACK_CAMERA
         )
@@ -195,7 +198,12 @@
     ): IntArray {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of(CROP_RECT, 90, rotation),
+            SurfaceRequest.TransformationInfo.of(
+                CROP_RECT,
+                90,
+                rotation, /*hasCameraTransform=*/
+                true
+            ),
             SURFACE_SIZE,
             isFrontCamera
         )
@@ -223,7 +231,8 @@
             SurfaceRequest.TransformationInfo.of(
                 CROP_RECT,
                 90,
-                ARBITRARY_ROTATION
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
             ),
             SURFACE_SIZE, BACK_CAMERA
         )
@@ -351,7 +360,12 @@
     ) {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of(MISMATCHED_CROP_RECT, 90, ARBITRARY_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                MISMATCHED_CROP_RECT,
+                90,
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
+            ),
             FIT_SURFACE_SIZE,
             isFrontCamera
         )
@@ -436,7 +450,8 @@
             SurfaceRequest.TransformationInfo.of(
                 cropRect,
                 rotationDegrees,
-                ARBITRARY_ROTATION
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
             ),
             SURFACE_SIZE,
             isFrontCamera
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
index 3269265..e763fdb 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
@@ -66,8 +66,13 @@
         // Arrange.
         PreviewTransformation previewTransformation = new PreviewTransformation();
         previewTransformation.setTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(new Rect(0, 0, WIDTH, HEIGHT), 0,
-                        Surface.ROTATION_0), new Size(WIDTH, HEIGHT), false);
+                SurfaceRequest.TransformationInfo.of(
+                        new Rect(0, 0, WIDTH, HEIGHT),
+                        0,
+                        Surface.ROTATION_0,
+                        /*hasCameraTransform=*/true),
+                new Size(WIDTH, HEIGHT),
+                /*isFrontCamera=*/false);
         PreviewViewMeteringPointFactory previewViewMeteringPointFactory =
                 new PreviewViewMeteringPointFactory(previewTransformation);
 
@@ -85,8 +90,13 @@
         // Arrange.
         PreviewTransformation previewTransformation = new PreviewTransformation();
         previewTransformation.setTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(new Rect(0, 0, WIDTH, HEIGHT), 0,
-                        Surface.ROTATION_0), new Size(WIDTH, HEIGHT), false);
+                SurfaceRequest.TransformationInfo.of(
+                        new Rect(0, 0, WIDTH, HEIGHT),
+                        /*rotationDegrees=*/0,
+                        Surface.ROTATION_0,
+                        /*hasCameraTransform=*/true),
+                new Size(WIDTH, HEIGHT),
+                /*isFrontCamera=*/false);
         PreviewViewMeteringPointFactory previewViewMeteringPointFactory =
                 new PreviewViewMeteringPointFactory(previewTransformation);
 
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
index ff9cca4..2a55cbd 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
@@ -22,14 +22,16 @@
 import android.os.HandlerThread
 import android.util.Log
 import android.util.Size
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageProxy
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
@@ -38,6 +40,7 @@
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.GLUtil
 import androidx.camera.testing.LabTestRule
@@ -81,11 +84,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class BindUnbindUseCasesStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -135,6 +145,8 @@
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -164,9 +176,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @LabTestRule.LabTestOnly
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 45f998f..1781ca6 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -64,6 +64,7 @@
  */
 internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
     val idlingResource = withActivity {
+        cleanTakePictureErrorMessage()
         imageSavedIdlingResource
     }
     try {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
index 52c6853..0fa1f88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
@@ -17,17 +17,22 @@
 package androidx.camera.integration.core
 
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import android.hardware.camera2.CameraDevice
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraState
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.LabTestRule
@@ -36,7 +41,6 @@
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.camera.video.Recorder
 import androidx.camera.video.VideoCapture
-import androidx.lifecycle.Observer
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -60,11 +64,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class OpenCloseCameraStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
+        PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -81,9 +92,14 @@
     private lateinit var preview: Preview
     private lateinit var imageCapture: ImageCapture
     private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private val cameraDeviceStateMonitor = CameraDeviceStateMonitor()
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Skips CameraPipe part now and will open this when camera-pipe-integration can support
+        assumeTrue(implName != CameraPipeConfig::class.simpleName)
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -94,7 +110,7 @@
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraIdCameraSelector)
         }
 
-        preview = Preview.Builder().build()
+        preview = createPreviewWithDeviceStateMonitor(implName, cameraDeviceStateMonitor)
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
         }
@@ -116,16 +132,19 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @LabTestRule.LabTestOnly
     @Test
     @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
     fun openCloseCameraStressTest_withPreviewImageCapture(): Unit = runBlocking {
-        bindUseCase_unbindAll_toCheckCameraState_repeatedly(preview, imageCapture)
+        bindUseCase_unbindAll_toCheckCameraState_repeatedly(
+            preview,
+            imageCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
+        )
     }
 
     @LabTestRule.LabTestOnly
@@ -137,7 +156,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             imageCapture,
-            imageAnalysis = imageAnalysis
+            imageAnalysis = imageAnalysis,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -146,7 +166,11 @@
     @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
     fun openCloseCameraStressTest_withPreviewVideoCapture(): Unit = runBlocking {
         val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
-        bindUseCase_unbindAll_toCheckCameraState_repeatedly(preview, videoCapture = videoCapture)
+        bindUseCase_unbindAll_toCheckCameraState_repeatedly(
+            preview,
+            videoCapture = videoCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
+        )
     }
 
     @LabTestRule.LabTestOnly
@@ -158,7 +182,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
-            imageCapture = imageCapture
+            imageCapture = imageCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -172,7 +197,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
-            imageAnalysis = imageAnalysis
+            imageAnalysis = imageAnalysis,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -188,23 +214,13 @@
         imageCapture: ImageCapture? = null,
         videoCapture: VideoCapture<Recorder>? = null,
         imageAnalysis: ImageAnalysis? = null,
+        cameraDeviceStateMonitor: CameraDeviceStateMonitor,
         repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
     ): Unit = runBlocking {
         for (i in 1..repeatCount) {
-            val openCameraLatch = CountDownLatch(1)
-            val closeCameraLatch = CountDownLatch(1)
-            val observer = Observer<CameraState> { state ->
-                if (state.type == CameraState.Type.OPEN) {
-                    openCameraLatch.countDown()
-                } else if (state.type == CameraState.Type.CLOSED) {
-                    closeCameraLatch.countDown()
-                }
-            }
+            cameraDeviceStateMonitor.reset()
 
             withContext(Dispatchers.Main) {
-                // Arrange: sets up CameraState observer
-                camera.cameraInfo.cameraState.observe(lifecycleOwner, observer)
-
                 // VideoCapture needs to be recreated everytime until b/212654991 is fixed
                 var newVideoCapture: VideoCapture<Recorder>? = null
                 videoCapture?.let {
@@ -224,21 +240,68 @@
                 )
             }
 
-            // Assert: checks the CameraState.Type.OPEN can be received
-            assertThat(openCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+            // Assert: checks the CameraDevice opened event can be received
+            cameraDeviceStateMonitor.awaitCameraOpenedAndAssert()
 
             // Act: unbinds all use cases
             withContext(Dispatchers.Main) {
                 cameraProvider.unbindAll()
             }
 
-            // Assert: checks the CameraState.Type.CLOSED can be received
-            assertThat(closeCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+            // Assert: checks the CameraDevice closed event can be received
+            cameraDeviceStateMonitor.awaitCameraClosedAndAssert()
+        }
+    }
 
-            // Clean it up.
-            withContext(Dispatchers.Main) {
-                camera.cameraInfo.cameraState.removeObserver(observer)
+    @OptIn(ExperimentalCamera2Interop::class)
+    private fun createPreviewWithDeviceStateMonitor(
+        implementationName: String,
+        cameraDeviceStateMonitor: CameraDeviceStateMonitor
+    ): Preview {
+        val builder = Preview.Builder()
+
+        when (implementationName) {
+            CameraPipeConfig::class.simpleName -> {
+                androidx.camera.camera2.pipe.integration.interop.Camera2Interop.Extender(builder)
+                    .setDeviceStateCallback(cameraDeviceStateMonitor)
             }
+            else -> Camera2Interop.Extender(builder)
+                .setDeviceStateCallback(cameraDeviceStateMonitor)
+        }
+
+        return builder.build()
+    }
+
+    private class CameraDeviceStateMonitor : CameraDevice.StateCallback() {
+        private var openCameraLatch = CountDownLatch(1)
+        private var closeCameraLatch = CountDownLatch(1)
+        override fun onOpened(p0: CameraDevice) {
+            openCameraLatch.countDown()
+        }
+
+        override fun onClosed(camera: CameraDevice) {
+            closeCameraLatch.countDown()
+        }
+
+        override fun onDisconnected(p0: CameraDevice) {
+            // No op.
+        }
+
+        override fun onError(p0: CameraDevice, p1: Int) {
+            // No op.
+        }
+
+        fun reset() {
+            openCameraLatch = CountDownLatch(1)
+            closeCameraLatch = CountDownLatch(1)
+        }
+
+        fun awaitCameraOpenedAndAssert() {
+            assertThat(openCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+        }
+
+        fun awaitCameraClosedAndAssert() {
+            assertThat(closeCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
         }
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
index a6dfbca..4a33164 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
@@ -17,20 +17,23 @@
 package androidx.camera.integration.core
 
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.impl.Camera2ImplConfig
-import androidx.camera.camera2.impl.CameraEventCallback
-import androidx.camera.camera2.impl.CameraEventCallbacks
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCaptureSession.StateCallback
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
-import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.StressTestRule
@@ -61,11 +64,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class OpenCloseCaptureSessionStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -82,10 +92,14 @@
     private lateinit var preview: Preview
     private lateinit var imageCapture: ImageCapture
     private lateinit var lifecycleOwner: FakeLifecycleOwner
-    private val cameraEventMonitor = CameraEventMonitor()
+    private val sessionStateMonitor = CameraCaptureSessionStateMonitor()
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Skips CameraPipe part now and will open this when camera-pipe-integration can support
+        assumeTrue(implName != CameraPipeConfig::class.simpleName)
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -96,9 +110,10 @@
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraIdCameraSelector)
         }
 
-        // Creates the Preview with the CameraEventMonitor to monitor whether the event callbacks
-        // are called.
-        preview = createPreviewWithCameraEventMonitor(cameraEventMonitor)
+        // Creates the Preview with the CameraCaptureSessionStateMonitor to monitor whether the
+        // event callbacks are called.
+        preview = createPreviewWithSessionStateMonitor(implName, sessionStateMonitor)
+
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
         }
@@ -193,7 +208,7 @@
     ): Unit = runBlocking {
         for (i in 1..repeatCount) {
             // Arrange: resets the camera event monitor
-            cameraEventMonitor.reset()
+            sessionStateMonitor.reset()
 
             withContext(Dispatchers.Main) {
                 // VideoCapture needs to be recreated everytime until b/212654991 is fixed
@@ -215,16 +230,13 @@
                 )
             }
 
-            // Assert: checks the CameraEvent#onEnableSession callback function is called
-            cameraEventMonitor.awaitSessionEnabledAndAssert()
+            // Assert: checks the capture session opened callback function is called
+            sessionStateMonitor.awaitSessionConfiguredAndAssert()
 
             // Act: unbinds all use cases
             withContext(Dispatchers.Main) {
                 cameraProvider.unbindAll()
             }
-
-            // Assert: checks the CameraEvent#onSessionDisabled callback function is called
-            cameraEventMonitor.awaitSessionDisabledAndAssert()
         }
     }
 
@@ -233,51 +245,49 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
-    private fun createPreviewWithCameraEventMonitor(
-        cameraEventMonitor: CameraEventMonitor
+    @OptIn(ExperimentalCamera2Interop::class)
+    private fun createPreviewWithSessionStateMonitor(
+        implementationName: String,
+        sessionStateMonitor: CameraCaptureSessionStateMonitor
     ): Preview {
         val builder = Preview.Builder()
 
-        Camera2ImplConfig.Extender(builder)
-            .setCameraEventCallback(CameraEventCallbacks(cameraEventMonitor))
+        when (implementationName) {
+            CameraPipeConfig::class.simpleName -> {
+                androidx.camera.camera2.pipe.integration.interop.Camera2Interop.Extender(
+                    builder
+                ).setSessionStateCallback(sessionStateMonitor)
+            }
+            else -> Camera2Interop.Extender(builder).setSessionStateCallback(sessionStateMonitor)
+        }
 
         return builder.build()
     }
 
     /**
-     * An implementation of CameraEventCallback to monitor whether the camera event callbacks are
-     * called properly or not.
+     * An implementation of CameraCaptureSession.StateCallback to monitor whether the event
+     * callbacks are called properly or not.
      */
-    private class CameraEventMonitor : CameraEventCallback() {
-        private var sessionEnabledLatch = CountDownLatch(1)
-        private var sessionDisabledLatch = CountDownLatch(1)
-
-        override fun onEnableSession(): CaptureConfig? {
-            sessionEnabledLatch.countDown()
-            return null
+    private class CameraCaptureSessionStateMonitor : StateCallback() {
+        private var sessionConfiguredLatch = CountDownLatch(1)
+        override fun onConfigured(session: CameraCaptureSession) {
+            sessionConfiguredLatch.countDown()
         }
 
-        override fun onDisableSession(): CaptureConfig? {
-            sessionDisabledLatch.countDown()
-            return null
+        override fun onConfigureFailed(session: CameraCaptureSession) {
+            throw RuntimeException("Capture session configures failed!")
         }
 
         fun reset() {
-            sessionEnabledLatch = CountDownLatch(1)
-            sessionDisabledLatch = CountDownLatch(1)
+            sessionConfiguredLatch = CountDownLatch(1)
         }
 
-        fun awaitSessionEnabledAndAssert() {
-            assertThat(sessionEnabledLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
-        }
-
-        fun awaitSessionDisabledAndAssert() {
-            assertThat(sessionDisabledLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
+        fun awaitSessionConfiguredAndAssert() {
+            assertThat(sessionConfiguredLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
         }
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
index 70e5598..1ceec71 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
@@ -25,6 +25,7 @@
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.waitForIdle
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.Espresso.onView
@@ -33,6 +34,8 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.TimeUnit
 import org.junit.After
 import org.junit.Assume.assumeTrue
@@ -137,4 +140,35 @@
             }
         }
     }
+
+    @Test
+    fun testTakePictureQuickly() {
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) {
+            use { // Ensure ActivityScenario is cleaned up properly.
+
+                // Arrange, wait for camera starts processing output.
+                waitForViewfinderIdle()
+
+                // Act. continuously take 5 photos.
+                withActivity {
+                    cleanTakePictureErrorMessage()
+                    imageSavedIdlingResource
+                }.apply {
+                    for (i in 5 downTo 1) {
+                        onView(withId(R.id.Picture)).perform(click())
+                    }
+                    waitForIdle()
+                }
+
+                // Assert, there's no error message.
+                withActivity {
+                    deleteSessionImages()
+                    lastTakePictureErrorMessage ?: ""
+                }.let { errorMessage ->
+                    assertWithMessage("Fail to take picture: $errorMessage").that(errorMessage)
+                        .isEmpty()
+                }
+            }
+        }
+    }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
index e37c5f1..c03d388 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageAnalysisLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class ImageAnalysisLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
index f0996e9..6d2ab58 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageAnalysisSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class ImageAnalysisSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
index 8e35e34..2682fa5 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class ImageCaptureLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index befb664..a447c7f 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -18,7 +18,8 @@
 
 import android.Manifest
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
@@ -28,6 +29,7 @@
 import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -58,12 +60,21 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureStressTest(val cameraId: String) {
+class ImageCaptureStressTest(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
+) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -78,15 +89,17 @@
     @get:Rule
     val repeatRule = RepeatRule()
 
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var cameraProvider: ProcessCameraProvider
+
     companion object {
         @ClassRule
         @JvmField
         val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -94,6 +107,17 @@
         Assume.assumeTrue(CameraUtil.deviceHasCamera())
         CoreAppTestUtil.assumeCompatibleDevice()
         CoreAppTestUtil.assumeNotUntestableFrontCamera(cameraId)
+
+        // For running the ImageCaptureStressTest, we need to get the target test camera to check
+        // whether the testing use case combination can be supported to skip unsupported cases. For
+        // the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
+
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
         // window before start the test.
         CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
@@ -106,17 +130,17 @@
 
     @After
     fun tearDown(): Unit = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
+            }
+        }
+
         // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
         // to other tests :)
         device.unfreezeRotation()
         device.pressHome()
         device.waitForIdle(StressTestUtil.HOME_TIMEOUT_MS)
-
-        withContext(Dispatchers.Main) {
-            val context = ApplicationProvider.getApplicationContext<Context>()
-            val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
-            cameraProvider.shutdown()[10, TimeUnit.SECONDS]
-        }
     }
 
     @LabTestRule.LabTestOnly
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
index c446e77..0837894 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,9 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
-
+class ImageCaptureSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
     @LabTestRule.LabTestOnly
     @Test
     @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
index cfc9c26..3c59a34 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
@@ -18,9 +18,10 @@
 
 import android.Manifest
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.recordVideoAndWaitForVideoSavedIdle
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
 import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
@@ -30,10 +31,12 @@
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_PREVIEW
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
+import androidx.camera.integration.core.util.StressTestUtil.getAllCameraXConfigCameraIdCombinations
 import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
 import androidx.camera.integration.core.waitForImageAnalysisIdle
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -57,13 +60,20 @@
 import org.junit.runners.Parameterized
 
 abstract class LifecycleStatusChangeStressTestBase(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
     val cameraId: String
 ) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -90,9 +100,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -109,6 +118,15 @@
         // explicitly initiated from within the test.
         device.setOrientationNatural()
 
+        // For running the LifecycleStatusChangeStressTest, we need to get the target test camera
+        // to check whether the testing use case combination can be supported to skip unsupported
+        // cases. For the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
index 04e4fe4..464e84d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class PreviewLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
index ee61902..30fc762 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -33,8 +34,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class PreviewSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
index bbb9455..f710cab8 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
@@ -19,13 +19,15 @@
 import android.Manifest
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.integration.core.recordVideoAndWaitForVideoSavedIdle
 import androidx.camera.integration.core.switchCameraAndWaitForViewfinderIdle
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
@@ -38,6 +40,7 @@
 import androidx.camera.integration.core.waitForImageAnalysisIdle
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -60,13 +63,20 @@
 import org.junit.runners.Parameterized
 
 abstract class SwitchCameraStressTestBase(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
     val cameraId: String
 ) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -93,9 +103,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -112,6 +121,15 @@
         // explicitly initiated from within the test.
         device.setOrientationNatural()
 
+        // For running the LifecycleStatusChangeStressTest, we need to get the target test camera
+        // to check whether the testing use case combination can be supported to skip unsupported
+        // cases. For the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -233,9 +251,7 @@
         // Checks whether the input camera can support the use case combination
         assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
 
-        val camera2CameraInfo = Camera2CameraInfo.from(camera.cameraInfo)
-        val lensFacing =
-            camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.LENS_FACING)
+        val lensFacing = (camera.cameraInfo as CameraInfoInternal).lensFacing
 
         val otherLensFacingCameraSelector =
             if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
index cd215e3..13defec 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class VideoCaptureLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class VideoCaptureLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
index 39e7011..7d8630d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -33,8 +34,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class VideoCaptureSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class VideoCaptureSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
index da6f867..857db58 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
@@ -18,9 +18,8 @@
 
 import android.content.Context
 import android.content.Intent
-import androidx.annotation.OptIn
-import androidx.camera.camera2.interop.Camera2CameraInfo
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraFilter
 import androidx.camera.core.CameraInfo
@@ -28,6 +27,7 @@
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.integration.core.CameraXActivity
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
@@ -36,6 +36,7 @@
 import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_CAMERA_ID
 import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_USE_CASE_COMBINATION
 import androidx.camera.integration.core.waitForViewfinderIdle
+import androidx.camera.testing.CameraUtil
 import androidx.camera.video.Recorder
 import androidx.camera.video.VideoCapture
 import androidx.test.core.app.ActivityScenario
@@ -80,9 +81,7 @@
 
         activityScenario.onActivity {
             // Checks that the camera id is correct
-            val camera2CameraInfo = Camera2CameraInfo.from(it.camera!!.cameraInfo)
-
-            if (camera2CameraInfo.cameraId != cameraId) {
+            if ((it.camera!!.cameraInfo as CameraInfoInternal).cameraId != cameraId) {
                 it.finish()
                 throw IllegalArgumentException("The activity is not launched with the correct" +
                     " camera of expected id.")
@@ -131,11 +130,10 @@
     }
 
     @JvmStatic
-    @OptIn(ExperimentalCamera2Interop::class)
     fun createCameraSelectorById(cameraId: String) =
         CameraSelector.Builder().addCameraFilter(CameraFilter { cameraInfos ->
             cameraInfos.forEach {
-                if (Camera2CameraInfo.from(it).cameraId.equals(cameraId)) {
+                if ((it as CameraInfoInternal).cameraId == cameraId) {
                     return@CameraFilter listOf<CameraInfo>(it)
                 }
             }
@@ -143,6 +141,30 @@
             throw IllegalArgumentException("No camera can be find for id: $cameraId")
         }).build()
 
+    @JvmStatic
+    fun getAllCameraXConfigCameraIdCombinations() = mutableListOf<Array<Any?>>().apply {
+        val cameraxConfigs =
+            listOf(Camera2Config::class.simpleName, CameraPipeConfig::class.simpleName)
+
+        cameraxConfigs.forEach { configImplName ->
+            CameraUtil.getBackwardCompatibleCameraIdListOrThrow().forEach { cameraId ->
+                add(
+                    arrayOf(
+                        configImplName,
+                        when (configImplName) {
+                            CameraPipeConfig::class.simpleName ->
+                                CameraPipeConfig.defaultConfig()
+                            Camera2Config::class.simpleName ->
+                                Camera2Config.defaultConfig()
+                            else -> Camera2Config.defaultConfig()
+                        },
+                        cameraId
+                    )
+                )
+            }
+        }
+    }
+
     /**
      * Large stress test repeat count to run the test
      */
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 24257c5..d78d107 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -737,8 +737,6 @@
                     @Override
                     public void onClick(View view) {
                         mImageSavedIdlingResource.increment();
-                        mLastTakePictureErrorMessage = null;
-
                         mStartCaptureTime = SystemClock.elapsedRealtime();
                         createDefaultPictureFolderIfNotExist();
                         ContentValues contentValues = new ContentValues();
@@ -1276,7 +1274,6 @@
      *
      * @param calledBySelf flag indicates if this is a recursive call.
      */
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     void tryBindUseCases(boolean calledBySelf) {
         boolean isViewFinderReady = mViewFinder.getWidth() != 0 && mViewFinder.getHeight() != 0;
         boolean isCameraReady = mCameraProvider != null;
@@ -1312,10 +1309,7 @@
             // camera id.
             if (mCurrentCameraSelector == mLaunchingCameraIdSelector
                     && mLaunchingCameraLensFacing == UNKNOWN_LENS_FACING) {
-                Camera2CameraInfo camera2CameraInfo =
-                        Camera2CameraInfo.from(mCamera.getCameraInfo());
-                mLaunchingCameraLensFacing = camera2CameraInfo.getCameraCharacteristic(
-                        CameraCharacteristics.LENS_FACING);
+                mLaunchingCameraLensFacing = getLensFacing(mCamera.getCameraInfo());
             }
             List<UseCase> useCases = buildUseCases();
             mCamera = bindToLifecycleSafely(useCases);
@@ -1743,7 +1737,11 @@
             synchronized (mSessionMediaUris) {
                 Iterator<Uri> it = mSessionMediaUris.iterator();
                 while (it.hasNext()) {
-                    getContentResolver().delete(it.next(), null, null);
+                    try {
+                        getContentResolver().delete(it.next(), null, null);
+                    } catch (SecurityException e) {
+                        Log.w(TAG, "Cannot delete the content.", e);
+                    }
                     it.remove();
                 }
             }
@@ -1876,6 +1874,11 @@
         return mLastTakePictureErrorMessage;
     }
 
+    @VisibleForTesting
+    void cleanTakePictureErrorMessage() {
+        mLastTakePictureErrorMessage = null;
+    }
+
     @SuppressWarnings("unchecked")
     VideoCapture<Recorder> getVideoCapture() {
         return findUseCase(VideoCapture.class);
@@ -1989,10 +1992,9 @@
         return new CameraSelector.Builder().addCameraFilter(new CameraFilter() {
             @NonNull
             @Override
-            @OptIn(markerClass = ExperimentalCamera2Interop.class)
             public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
                 for (CameraInfo cameraInfo : cameraInfos) {
-                    if (cameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
+                    if (Objects.equals(cameraId, getCameraId(cameraInfo))) {
                         return Collections.singletonList(cameraInfo);
                     }
                 }
@@ -2001,4 +2003,53 @@
             }
         }).build();
     }
+
+    private static int getLensFacing(@NonNull CameraInfo cameraInfo) {
+        try {
+            return getCamera2LensFacing(cameraInfo);
+        } catch (IllegalArgumentException e) {
+            return getCamera2PipeLensFacing(cameraInfo);
+        }
+    }
+
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
+    private static int getCamera2LensFacing(@NonNull CameraInfo cameraInfo) {
+        Integer lensFacing = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
+                    CameraCharacteristics.LENS_FACING);
+
+        return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
+    }
+
+    @OptIn(markerClass =
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
+    private static int getCamera2PipeLensFacing(@NonNull CameraInfo cameraInfo) {
+        Integer lensFacing =
+                androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
+                        cameraInfo).getCameraCharacteristic(CameraCharacteristics.LENS_FACING);
+
+        return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
+    }
+
+    @NonNull
+    private static String getCameraId(@NonNull CameraInfo cameraInfo) {
+        try {
+            return getCamera2CameraId(cameraInfo);
+        } catch (IllegalArgumentException e) {
+            return getCameraPipeCameraId(cameraInfo);
+        }
+    }
+
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
+    @NonNull
+    private static String getCamera2CameraId(@NonNull CameraInfo cameraInfo) {
+        return Camera2CameraInfo.from(cameraInfo).getCameraId();
+    }
+
+    @OptIn(markerClass =
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
+    @NonNull
+    private static String getCameraPipeCameraId(@NonNull CameraInfo cameraInfo) {
+        return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
+                cameraInfo).getCameraId();
+    }
 }
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index 1fd90c0..7198311 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -339,7 +339,7 @@
     property public static final androidx.compose.animation.core.Easing LinearOutSlowInEasing;
   }
 
-  @kotlin.RequiresOptIn(message="This is an experimental animation API for Transition. It may change in the future.") public @interface ExperimentalTransitionApi {
+  @kotlin.RequiresOptIn(message="This is an experimental animation API for Transition. It may change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTransitionApi {
   }
 
   public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
@@ -424,7 +424,7 @@
     method @androidx.compose.runtime.Composable public static androidx.compose.animation.core.InfiniteTransition rememberInfiniteTransition();
   }
 
-  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface InternalAnimationApi {
+  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface InternalAnimationApi {
   }
 
   @androidx.compose.runtime.Immutable public final class KeyframesSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
index 9972083..8fd3df7 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
@@ -19,4 +19,5 @@
 @RequiresOptIn(
     message = "This is an experimental animation API for Transition. It may change in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTransitionApi
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
index 952630f..d87eb4c 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
@@ -21,4 +21,5 @@
     AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
     AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalAnimationApi
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index f465c25..0e0b663 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -201,7 +201,7 @@
 
     companion object {
         fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
-            val KOTLIN_VERSION_EXPECTATION = "1.7.20"
+            val KOTLIN_VERSION_EXPECTATION = "1.7.21"
             KotlinCompilerVersion.getVersion()?.let { version ->
                 val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
                 val suppressKotlinVersionCheck = configuration.get(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index b29e3df..d199b5b 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -110,7 +110,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.4.0-alpha01"
+        const val compilerVersion: String = "1.4.0-alpha02"
         private val minimumRuntimeVersion: String
             get() = runtimeVersionToMavenVersionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 1d0a744..78d781e 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,7 +36,7 @@
          */
         api("androidx.annotation:annotation:1.1.0")
         api("androidx.compose.animation:animation:1.2.1")
-        api("androidx.compose.runtime:runtime:1.3.0-rc01")
+        api("androidx.compose.runtime:runtime:1.3.1")
         api(project(":compose:ui:ui"))
 
         implementation(libs.kotlinStdlibCommon)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
index 1a5794f..5633465 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
@@ -254,6 +254,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 32) // b/257069369
     @Test
     fun border_non_simple_rounded_rect() {
         val topleft = 0f
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 8b59f0e..c1678a8 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -47,8 +47,8 @@
 
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate:1.1.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
@@ -106,8 +106,8 @@
 
                 // TODO: remove next 3 dependencies when b/202810604 is fixed
                 implementation("androidx.savedstate:savedstate:1.1.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             desktopMain.dependencies {
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index c479be1..7a79167 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -36,21 +36,21 @@
          * corresponding block below
          */
         implementation(libs.kotlinStdlibCommon)
-        implementation("androidx.compose.animation:animation-core:1.3.0-rc01")
-        implementation("androidx.compose.foundation:foundation-layout:1.3.0-rc01")
-        implementation("androidx.compose.ui:ui-util:1.3.0-rc01")
+        implementation("androidx.compose.animation:animation-core:1.3.1")
+        implementation("androidx.compose.foundation:foundation-layout:1.3.1")
+        implementation("androidx.compose.ui:ui-util:1.3.1")
         api(project(":compose:foundation:foundation"))
-        api("androidx.compose.material:material-icons-core:1.3.0-rc01")
-        api("androidx.compose.material:material-ripple:1.3.0-rc01")
-        api("androidx.compose.runtime:runtime:1.3.0-rc01")
-        api("androidx.compose.ui:ui-graphics:1.3.0-rc01")
-        api("androidx.compose.ui:ui:1.3.0-rc01")
-        api("androidx.compose.ui:ui-text:1.3.0-rc01")
+        api("androidx.compose.material:material-icons-core:1.3.1")
+        api("androidx.compose.material:material-ripple:1.3.1")
+        api("androidx.compose.runtime:runtime:1.3.1")
+        api("androidx.compose.ui:ui-graphics:1.3.1")
+        api("androidx.compose.ui:ui:1.3.1")
+        api("androidx.compose.ui:ui-text:1.3.1")
 
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
@@ -107,8 +107,8 @@
 
                 // TODO: remove next 3 dependencies when b/202810604 is fixed
                 implementation("androidx.savedstate:savedstate:1.1.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             desktopMain.dependencies {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 03e25a0..65ee2ec 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -3314,7 +3314,7 @@
                         invokeComposable(this, content)
                         endGroup()
                     } else if (
-                        forciblyRecompose &&
+                        (forciblyRecompose || providersInvalid) &&
                         savedContent != null &&
                         savedContent != Composer.Empty
                     ) {
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index c7099eb..d7e2dfb 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -46,8 +46,8 @@
         implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
         implementation("androidx.activity:activity-compose:1.3.0")
         implementation("androidx.annotation:annotation:1.1.0")
-        implementation("androidx.lifecycle:lifecycle-common:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
         implementation("androidx.test:core:1.5.0")
         implementation("androidx.test:monitor:1.6.0")
         implementation("androidx.test.espresso:espresso-core:3.5.0")
@@ -104,8 +104,8 @@
                 implementation("androidx.annotation:annotation:1.1.0")
 
                 implementation(project(":compose:runtime:runtime-saveable"))
-                implementation("androidx.lifecycle:lifecycle-common:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
                 implementation("androidx.test:core:1.4.0")
                 implementation(libs.testMonitor)
                 implementation("androidx.test.espresso:espresso-core:3.3.0")
diff --git a/compose/ui/ui-unit/samples/build.gradle b/compose/ui/ui-unit/samples/build.gradle
index 4055ea0..33161d2 100644
--- a/compose/ui/ui-unit/samples/build.gradle
+++ b/compose/ui/ui-unit/samples/build.gradle
@@ -32,8 +32,8 @@
     implementation("androidx.compose.runtime:runtime:1.2.1")
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-unit"))
-    implementation("androidx.compose.foundation:foundation:1.3.0-rc01")
-    implementation("androidx.compose.foundation:foundation-layout:1.3.0-rc01")
+    implementation("androidx.compose.foundation:foundation:1.3.1")
+    implementation("androidx.compose.foundation:foundation-layout:1.3.1")
 }
 
 androidx {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index a8403cb..bd274e6 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -76,9 +76,9 @@
         implementation('androidx.collection:collection:1.0.0')
         implementation("androidx.customview:customview-poolingcontainer:1.0.0")
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-        implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
         implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         testImplementation(libs.testRules)
@@ -171,9 +171,9 @@
                 implementation('androidx.collection:collection:1.0.0')
                 implementation("androidx.customview:customview-poolingcontainer:1.0.0")
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-                implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             jvmMain.dependencies {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index ad704ea..7d0d0ff 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -228,6 +228,13 @@
     )
 )
 
+private val AccessibilityDemos = DemoCategory(
+    "Accessibility",
+    listOf(
+        ComposableDemo("Overlaid Nodes") { OverlaidNodeLayoutDemo() }
+    )
+)
+
 val CoreDemos = DemoCategory(
     "Framework",
     listOf(
@@ -243,6 +250,7 @@
         GestureDemos,
         ViewInteropDemos,
         ComposableDemo("Software Keyboard Controller") { SoftwareKeyboardControllerDemo() },
-        RecyclerViewDemos
+        RecyclerViewDemos,
+        AccessibilityDemos
     )
 )
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
new file mode 100644
index 0000000..6e36aee
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.compose.ui.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun LastElementOverLaidColumn(
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit,
+) {
+    var yPosition = 0
+
+    Layout(modifier = modifier, content = content) { measurables, constraints ->
+        val placeables = measurables.map { measurable ->
+            measurable.measure(constraints)
+        }
+
+        layout(constraints.maxWidth, constraints.maxHeight) {
+            placeables.forEach { placeable ->
+                if (placeable != placeables[placeables.lastIndex]) {
+                    placeable.placeRelative(x = 0, y = yPosition)
+                    yPosition += placeable.height
+                } else {
+                    // if the element is our last element (our overlaid node)
+                    // then we'll put it over the middle of our previous elements
+                    placeable.placeRelative(x = 0, y = yPosition / 2)
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+fun OverlaidNodeLayoutDemo() {
+    LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+        Row {
+            Column {
+                Row { Text("text1\n") }
+                Row { Text("text2\n") }
+                Row { Text("text3\n") }
+            }
+        }
+        Row {
+            Text("overlaid node")
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index a4889e2..1b57cbe 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -65,6 +65,8 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
@@ -80,6 +82,7 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.ClassName
@@ -560,6 +563,116 @@
         @Suppress("DEPRECATION") accessibilityNodeInfo.recycle()
     }
 
+    @Composable
+    fun LastElementOverLaidColumn(
+        modifier: Modifier = Modifier,
+        content: @Composable () -> Unit,
+    ) {
+        var yPosition = 0
+
+        Layout(modifier = modifier, content = content) { measurables, constraints ->
+            val placeables = measurables.map { measurable ->
+                measurable.measure(constraints)
+            }
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                placeables.forEach { placeable ->
+                    if (placeable != placeables[placeables.lastIndex]) {
+                        placeable.placeRelative(x = 0, y = yPosition)
+                        yPosition += placeable.height
+                    } else {
+                        // if the element is our last element (our overlaid node)
+                        // then we'll put it over the middle of our previous elements
+                        placeable.placeRelative(x = 0, y = yPosition / 2)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layout() {
+        val overlaidText = "Overlaid node text"
+        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        container.setContent {
+            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+                Row {
+                    Column {
+                        Row { Text(text1) }
+                        Row { Text(text2) }
+                        Row { Text(text3) }
+                    }
+                }
+                Row {
+                    Text(overlaidText)
+                }
+            }
+        }
+
+        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
+        val overlaidNode = rule.onNodeWithText(overlaidText).fetchSemanticsNode()
+
+        val ani3 = provider.createAccessibilityNodeInfo(node3.id)
+        val ani3TraversalBeforeVal = ani3?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
+        // comparison (like SemanticsSort), the third text node should come before the overlaid node
+        // — OverlaidNode should be read last
+        assertNotEquals(ani3TraversalBeforeVal, 0)
+        if (ani3TraversalBeforeVal != null) {
+            assertEquals(ani3TraversalBeforeVal, overlaidNode.id)
+        }
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layoutTestTags() {
+        val overlaidText = "Overlaid node text"
+        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        container.setContent {
+            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+                Row(modifier = Modifier
+                    .semantics(true) { contentDescription = "Row1" }
+                    .testTag("Row1")
+                ) {
+                    Column(modifier = Modifier.testTag("Column1")) {
+                        Row(modifier = Modifier.testTag("Text1")) { Text(text1) }
+                        Row(modifier = Modifier.testTag("Text2")) { Text(text2) }
+                        Row(modifier = Modifier.testTag("Text3")) { Text(text3) }
+                    }
+                }
+                Row(modifier = Modifier
+                    .semantics(true) { contentDescription = "Row2" }
+                    .testTag("Row2")
+                ) {
+                    Text(overlaidText)
+                }
+            }
+        }
+
+        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
+        val row2 = rule.onNodeWithTag("Row2").fetchSemanticsNode()
+
+        val ani3 = provider.createAccessibilityNodeInfo(node3.id)
+        val ani3TraversalBeforeVal = ani3?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
+        // comparison (like SemanticsSort), the third text node should come before the overlaid node
+        // — OverlaidNode and its row should be read last
+        assertNotEquals(ani3TraversalBeforeVal, 0)
+        if (ani3TraversalBeforeVal != null) {
+            assertTrue(ani3TraversalBeforeVal < row2.id)
+        }
+    }
+
+    companion object {
+        private const val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
+            "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
+    }
+
     @Test
     fun testPerformAction_showOnScreen() {
         rule.mainClock.autoAdvance = false
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 792181a..588bd86 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -111,7 +111,6 @@
 import androidx.compose.ui.unit.offset
 import androidx.compose.ui.unit.toOffset
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import org.junit.Assert.assertEquals
@@ -120,6 +119,7 @@
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -3099,7 +3099,7 @@
         validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
     }
 
-    @FlakyTest
+    @Ignore // b/173806298
     @Test
     fun makingItemLarger() {
         var height by mutableStateOf(30)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index a71891f..f3a63ae 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -64,6 +64,7 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
@@ -1198,6 +1199,57 @@
     }
 
     @Test
+    fun staticCompositionLocalChangeInMainComposition_withNonStaticLocal_invalidatesComposition() {
+        var isDark by mutableStateOf(false)
+
+        val staticLocal = staticCompositionLocalOf<Boolean> { error("Not defined") }
+        val local = compositionLocalOf<Boolean> { error("Not defined") }
+        val innerLocal = staticCompositionLocalOf<Unit> { error("\not defined") }
+
+        val content = @Composable {
+            CompositionLocalProvider(innerLocal provides Unit) {
+                val value1 = staticLocal.current
+                val value2 = local.current
+                Box(
+                    Modifier
+                        .testTag(if (value1) "dark" else "light")
+                        .requiredSize(if (value2) 50.dp else 100.dp)
+                )
+            }
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(
+                staticLocal provides isDark,
+            ) {
+                CompositionLocalProvider(
+                    local provides staticLocal.current
+                ) {
+                    SubcomposeLayout { constraints ->
+                        val measurables = subcompose(Unit, content)
+                        val placeables = measurables.map {
+                            it.measure(constraints)
+                        }
+                        layout(100, 100) {
+                            placeables.forEach { it.place(IntOffset.Zero) }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("light")
+            .assertWidthIsEqualTo(100.dp)
+
+        isDark = true
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("dark")
+            .assertWidthIsEqualTo(50.dp)
+    }
+
+    @Test
     fun derivedStateChangeInMainCompositionRecomposesSubcomposition() {
         var flag by mutableStateOf(true)
         var subcomposionValue: Boolean? = null
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index a5cd31e..aed28d7 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.LayoutNode
@@ -272,13 +273,17 @@
      */
     private var currentSemanticsNodes: Map<Int, SemanticsNodeWithAdjustedBounds> = mapOf()
         get() {
-            if (currentSemanticsNodesInvalidated) {
-                field = view.semanticsOwner.getAllUncoveredSemanticsNodesToMap()
+            if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes
                 currentSemanticsNodesInvalidated = false
+                field = view.semanticsOwner.getAllUncoveredSemanticsNodesToMap()
+                setTraversalValues()
             }
             return field
         }
     private var paneDisplayed = ArraySet<Int>()
+    private var idToBeforeMap = HashMap<Int, Int>()
+    private val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
+        "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
 
     /**
      * A snapshot of the semantics node. The children here is fixed and are taken from the time
@@ -435,6 +440,44 @@
         return info.unwrap()
     }
 
+    private fun setTraversalValues() {
+        idToBeforeMap.clear()
+        var idToCoordinatesList = mutableListOf<Pair<Int, Rect>>()
+
+        fun depthFirstSearch(currNode: SemanticsNode) {
+            if (currNode.parent?.layoutNode?.innerCoordinator?.isAttached == true &&
+                currNode.layoutNode.innerCoordinator.isAttached
+            ) {
+                idToCoordinatesList.add(
+                    Pair(
+                        currNode.id,
+                        currNode.layoutNode.coordinates.boundsInWindow()
+                    )
+                )
+            }
+            // This retrieves the children in the order that we want (respecting child/parent
+            // hierarchies)
+            currNode.replacedChildrenSortedByBounds.fastForEach { child ->
+                depthFirstSearch(child)
+            }
+        }
+
+        currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]?.semanticsNode
+            ?.replacedChildrenSortedByBounds?.fastForEach { node ->
+                depthFirstSearch(node)
+            }
+
+        // Iterate through our ordered list, and creating a mapping of current node to next node ID
+        // We'll later read through this and set traversal order with IdToBeforeMap
+        for (i in 1..idToCoordinatesList.lastIndex) {
+            val prevId = idToCoordinatesList[i - 1].first
+            val currId = idToCoordinatesList[i].first
+            idToBeforeMap[prevId] = currId
+        }
+
+        return
+    }
+
     @VisibleForTesting
     @OptIn(ExperimentalComposeUiApi::class)
     fun populateAccessibilityNodeInfoProperties(
@@ -494,7 +537,7 @@
         // "important".
         info.isImportantForAccessibility = true
 
-        semanticsNode.replacedChildrenSortedByBounds.fastForEach { child ->
+        semanticsNode.replacedChildren.fastForEach { child ->
             if (currentSemanticsNodes.contains(child.id)) {
                 val holder = view.androidViewsHandler.layoutNodeToHolder[child.layoutNode]
                 if (holder != null) {
@@ -979,6 +1022,12 @@
         info.isScreenReaderFocusable =
             semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants ||
             isUnmergedLeafNode && isSpeakingNode
+
+        if (idToBeforeMap[virtualViewId] != null) {
+            idToBeforeMap[virtualViewId]?.let { info.setTraversalBefore(view, it) }
+            addExtraDataToAccessibilityNodeInfoHelper(
+                virtualViewId, info.unwrap(), EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL, null)
+        }
     }
 
     /** Set the error text for this node */
@@ -1459,7 +1508,14 @@
     ) {
         val node = currentSemanticsNodes[virtualViewId]?.semanticsNode ?: return
         val text = getIterableTextForAccessibility(node)
-        if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
+
+        // This extra is just for testing: needed a way to retrieve `traversalBefore` from
+        // non-sealed instance of ANI
+        if (extraDataKey == EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL) {
+            idToBeforeMap[virtualViewId]?.let {
+                info.extras.putInt(extraDataKey, it)
+            }
+        } else if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
             arguments != null && extraDataKey == EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
         ) {
             val positionInfoStartIndex = arguments.getInt(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index dc8372e..e4f5e82 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -37,9 +37,9 @@
  *
  * The input data is provided by calling [addPosition]. Adding data is cheap.
  *
- * To obtain a velocity, call [calculateVelocity] or [getVelocityEstimate]. This will
- * compute the velocity based on the data added so far. Only call these when
- * you need to use the velocity, as they are comparatively expensive.
+ * To obtain a velocity, call [calculateVelocity]. This will compute the velocity
+ * based on the data added so far. Only call this when  you need to use the velocity,
+ * as it is comparatively expensive.
  *
  * The quality of the velocity estimation will be better if more data points
  * have been received.
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index a20071d..27587e6 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -18,6 +18,7 @@
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_SPREAD;
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_WRAP;
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
@@ -2376,7 +2377,7 @@
         public static final int START = 6;
 
         /**
-         * The right side of a view in right to left languages.
+         * The right side of a view in left to right languages.
          * In right to left languages it corresponds to the left side of the view
          */
         public static final int END = 7;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
index 55c8c2d..d2c94b9 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
@@ -62,7 +62,7 @@
     public static final int START = ConstraintLayout.LayoutParams.START;
 
     /**
-     * The right side of a view in right to left languages.
+     * The right side of a view in left to right languages.
      * In right to left languages it corresponds to the left side of the view
      */
     public static final int END = ConstraintLayout.LayoutParams.END;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
index 59a0a66..63d4f6c 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
@@ -252,7 +252,7 @@
     public static final int START = ConstraintLayout.LayoutParams.START;
 
     /**
-     * The right side of a view in right to left languages.
+     * The right side of a view in left to right languages.
      * In right to left languages it corresponds to the left side of the view
      */
     public static final int END = ConstraintLayout.LayoutParams.END;
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
index 0ec2645..5b88fd9 100644
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
@@ -20,7 +20,6 @@
 import android.os.Bundle
 import androidx.datastore.core.handlers.NoOpCorruptionHandler
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.FlakyTest
 import androidx.testing.TestMessageProto.FooProto
 import com.google.common.truth.Truth.assertThat
 import com.google.protobuf.ExtensionRegistryLite
@@ -43,6 +42,7 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -265,7 +265,7 @@
         }
     }
 
-    @FlakyTest(bugId = 242765757)
+    @Ignore // b/242765757
     @Test
     fun testInterleavedUpdateDataWithLocalRead() = runTest(UnconfinedTestDispatcher()) {
         val testData: Bundle = createDataStoreBundle(testFile.absolutePath)
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
index f9e16bc..5f967ca 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -57,7 +57,7 @@
         BundledEmojiListLoader.load(context)
 
         val cacheFileName = fileCache.emojiPickerCacheDir.listFiles()!![0].listFiles()!![0].name
-        val emptyDefaultValue = listOf<BundledEmojiListLoader.EmojiData>()
+        val emptyDefaultValue = listOf<EmojiViewItem>()
         // Read from cache instead of using default value
         var output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
         assertTrue(output.isNotEmpty())
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 4f4d9c1..3a4e7ed 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -85,14 +85,14 @@
     private fun loadSingleCategory(
         context: Context,
         resId: Int,
-    ): List<EmojiData> =
+    ): List<EmojiViewItem> =
         context.resources
             .openRawResource(resId)
             .bufferedReader()
             .useLines { it.toList() }
             .map { filterRenderableEmojis(it.split(",")) }
             .filter { it.isNotEmpty() }
-            .map { EmojiData(it.first(), it.drop(1)) }
+            .map { EmojiViewItem(it.first(), it.drop(1)) }
 
     private fun getCacheFileName(categoryIndex: Int) =
         StringBuilder().append("emoji.v1.")
@@ -112,10 +112,8 @@
             UnicodeRenderableManager.isEmojiRenderable(it)
         }.toList()
 
-    internal data class EmojiData(val primary: String, val variants: List<String>)
-
     internal data class EmojiDataCategory(
         val categoryName: String,
-        val emojiDataList: List<EmojiData>
+        val emojiDataList: List<EmojiViewItem>
     )
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt
new file mode 100644
index 0000000..2db67d7
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.emoji2.emojipicker
+
+/**
+ * Separator for each category.
+ *
+ *
+ * CategorySeparatorViewData: A 0-width `Space` at the beginning of each category. The
+ * `Space` works an anchor (prevention from unexpected scrolling) when the contents of the
+ * RecyclerView is updated.
+ */
+internal class CategorySeparatorViewData
+/**
+ * Instantiates a CategorySeparatorViewData.
+ *
+ * @param categoryIndex Used to compute the id.
+ * @param idInCategory Used to compute the id.
+ * @param categoryName The category name showing in the text view, e.g. "CUSTOM EMOJIS". If empty,
+ * will look up the corresponding category name based on `categoryIndex`
+ * in [EmojiPickerBodyAdapter.onBindViewHolder]
+ */(
+    categoryIndex: Int,
+    idInCategory: Int,
+    /** The name of this category.  */
+    val categoryName: String
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, /* idInCategory= */idInCategory)) {
+
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE = CategorySeparatorViewData::class.java.name.hashCode()
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt
new file mode 100644
index 0000000..1218fef
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.emoji2.emojipicker
+
+/**
+ * Placeholder entry, filled at the end of each category if there are room to be filled.
+ *
+ * This behaves like NaN. Nothing is equal to Placeholder entry.
+ */
+internal class DummyViewData(id: Long) : ItemViewData(id) {
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE: Int = DummyViewData::class.java.name.hashCode()
+        val INSTANCE = DummyViewData(TYPE.toLong())
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 43c70e4..4a9d32c 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -17,13 +17,16 @@
 package androidx.emoji2.emojipicker
 
 import android.content.Context
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
+import androidx.annotation.IntRange
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
 import androidx.appcompat.widget.AppCompatTextView
-import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import androidx.tracing.Trace
 
@@ -31,37 +34,150 @@
 internal class EmojiPickerBodyAdapter(
     context: Context,
     private val emojiGridColumns: Int,
-    private val emojiGridRows: Float
-) : RecyclerView.Adapter<ViewHolder>() {
+    private val emojiGridRows: Float,
+    private val categoryNames: Array<String>
+) : Adapter<ViewHolder>() {
     private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
     private val context = context
 
+    private var flattenSource: ItemViewDataFlatList
+
+    init {
+        val categorizedEmojis: MutableList<MutableList<ItemViewData>> = mutableListOf()
+        for (i in categoryNames.indices) {
+            categorizedEmojis.add(mutableListOf())
+        }
+        flattenSource = ItemViewDataFlatList(
+            categorizedEmojis,
+            emojiGridColumns
+        )
+    }
+
     @UiThread
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
-        try {
-            // TODO: Load real emoji data in the next change
-            val view: View = layoutInflater.inflate(
-                R.layout.emoji_picker_empty_category_text_view, parent,
-                /*attachToRoot= */ false
-            )
-            view.layoutParams = LayoutParams(
-                parent.width / emojiGridColumns, (parent.measuredHeight / emojiGridRows).toInt()
-            )
-            view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
-            return object : ViewHolder(view) {}
+        return try {
+            val view: View
+            if (viewType == CategorySeparatorViewData.TYPE) {
+                view = layoutInflater.inflate(
+                    R.layout.category_text_view,
+                    parent,
+                    /* attachToRoot= */ false
+                )
+                view.layoutParams =
+                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+            } else if (viewType == EmptyCategoryViewData.TYPE) {
+                view = layoutInflater.inflate(
+                    R.layout.emoji_picker_empty_category_text_view,
+                    parent,
+                    /* attachToRoot= */ false
+                )
+                view.layoutParams =
+                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+                view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
+            } else if (viewType == EmojiViewData.TYPE) {
+                return EmojiViewHolder(
+                    parent,
+                    layoutInflater,
+                    getParentWidth(parent) / emojiGridColumns,
+                    (parent.measuredHeight / emojiGridRows).toInt(),
+                )
+            } else if (viewType == DummyViewData.TYPE) {
+                view = View(context)
+                view.layoutParams = LayoutParams(
+                    getParentWidth(parent) / emojiGridColumns,
+                    (parent.measuredHeight / emojiGridRows).toInt()
+                )
+            } else {
+                Log.e(
+                    "EmojiPickerBodyAdapter",
+                    "EmojiPickerBodyAdapter gets unsupported view type."
+                )
+                view = View(context)
+                view.layoutParams =
+                    LayoutParams(
+                        getParentWidth(parent) / emojiGridColumns,
+                        (parent.measuredHeight / emojiGridRows).toInt()
+                    )
+            }
+            object : ViewHolder(view) {}
         } finally {
             Trace.endSection()
         }
     }
 
     override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
-        val emptyCategoryView: AppCompatTextView =
-            viewHolder.itemView.findViewById(R.id.emoji_picker_empty_category_view)
-        emptyCategoryView.setText(R.string.emoji_empty_non_recent_category)
+        val viewType = viewHolder.itemViewType
+        val view = viewHolder.itemView
+        if (viewType == CategorySeparatorViewData.TYPE) {
+            val categoryIndex = flattenSource.getCategoryIndex(position)
+            val item = flattenSource[position] as CategorySeparatorViewData
+            var categoryName = item.categoryName
+            if (categoryName.isEmpty()) {
+                categoryName = categoryNames[categoryIndex]
+            }
+            // Show category label.
+            val categoryLabel = view.findViewById<AppCompatTextView>(R.id.category_name)
+            if (categoryName.isEmpty()) {
+                categoryLabel.visibility = View.GONE
+            } else {
+                categoryLabel.text = categoryName
+                categoryLabel.visibility = View.VISIBLE
+            }
+        } else if (viewType == EmptyCategoryViewData.TYPE) {
+            // Show empty category description.
+            val emptyCategoryView =
+                view.findViewById<AppCompatTextView>(R.id.emoji_picker_empty_category_view)
+            val item = flattenSource[position] as EmptyCategoryViewData
+            var content = item.description
+            if (content.isEmpty()) {
+                val categoryIndex: Int = getCategoryIndex(position)
+                content = context.getString(
+                    if (categoryIndex == EmojiPickerConstants.RECENT_CATEGORY_INDEX)
+                        R.string.emoji_empty_recent_category
+                    else R.string.emoji_empty_non_recent_category
+                )
+            }
+            emptyCategoryView.text = content
+        } else if (viewType == EmojiViewData.TYPE) {
+            val item = flattenSource[position] as EmojiViewData
+            val emojiViewHolder = viewHolder as EmojiViewHolder
+            emojiViewHolder.bindEmoji(
+                EmojiViewItem(
+                    item.primary,
+                    item.secondaries.toList()
+                )
+            )
+        }
     }
 
     override fun getItemCount(): Int {
         return (emojiGridColumns * emojiGridRows).toInt()
     }
+
+    override fun getItemViewType(position: Int): Int {
+        return flattenSource[position].type
+    }
+
+    @IntRange(from = 0)
+    fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
+        return getFlattenSource().getCategoryIndex(position)
+    }
+
+    @VisibleForTesting
+    fun getFlattenSource(): ItemViewDataFlatList {
+        return flattenSource
+    }
+
+    fun getParentWidth(parent: ViewGroup): Int {
+        return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
+    }
+
+    internal fun updateEmojis(emojis: List<List<ItemViewData>>) {
+        flattenSource = ItemViewDataFlatList(
+            emojis,
+            emojiGridColumns
+        )
+        notifyDataSetChanged()
+    }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
new file mode 100644
index 0000000..ffe1390
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+
+/** Body view contains all emojis.  */
+internal class EmojiPickerBodyView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : RecyclerView(context, attrs) {
+
+    init {
+        val layoutManager = GridLayoutManager(
+            getContext(),
+            EmojiPickerConstants.DEFAULT_BODY_COLUMNS,
+            LinearLayoutManager.VERTICAL,
+            /* reverseLayout = */ false
+        )
+        layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+            override fun getSpanSize(position: Int): Int {
+                val adapter = adapter ?: return 1
+                val viewType = adapter.getItemViewType(position)
+                // The following viewTypes occupy entire row.
+                return if (
+                    viewType == CategorySeparatorViewData.TYPE ||
+                    viewType == EmptyCategoryViewData.TYPE
+                ) EmojiPickerConstants.DEFAULT_BODY_COLUMNS else 1
+            }
+        }
+        setLayoutManager(layoutManager)
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 7108b68..e579631 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -24,4 +24,10 @@
 
     // The default number of body rows.
     const val DEFAULT_BODY_ROWS = 7.5f
+
+    // The default minimal number of each body row.
+    const val MIN_ROWS_PER_CATEGORY = 1
+
+    // The default recent category index number.
+    const val RECENT_CATEGORY_INDEX = 0
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index baaac54..178c0cb 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -18,15 +18,15 @@
 
 import android.content.Context
 import android.content.res.TypedArray
+import android.os.Trace
 import android.util.AttributeSet
 import android.widget.FrameLayout
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
 
 /**
  * The emoji picker view that provides up-to-date emojis in a vertical scrollable view with a
@@ -84,24 +84,75 @@
         }
     }
 
-    private suspend fun showEmojiPickerView(context: Context) {
-        BundledEmojiListLoader.getCategorizedEmojiData()
+    private fun getEmojiPickerBodyAdapter(
+        context: Context,
+        emojiGridColumns: Int,
+        emojiGridRows: Float,
+        categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>
+    ): EmojiPickerBodyAdapter {
+        val categoryNames = mutableListOf<String>()
+        val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
+        for (i in categorizedEmojiData.indices) {
+            categoryNames.add(categorizedEmojiData[i].categoryName)
+            categorizedEmojis.add(
+                categorizedEmojiData[i].emojiDataList.toMutableList()
+            )
+        }
+        val adapter = EmojiPickerBodyAdapter(
+            context,
+            emojiGridColumns,
+            emojiGridRows,
+            categoryNames.toTypedArray()
+        )
+        adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
 
+        return adapter
+    }
+
+    private fun createEmojiViewData(categorizedEmojis: MutableList<MutableList<EmojiViewItem>>):
+        List<List<ItemViewData>> {
+        Trace.beginSection("createEmojiViewData")
+        return try {
+            val listBuilder = mutableListOf<List<ItemViewData>>()
+            for ((categoryIndex, sameType) in categorizedEmojis.withIndex()) {
+                val builder = mutableListOf<ItemViewData>()
+                for ((idInCategory, eachEmoji) in sameType.withIndex()) {
+                    builder.add(
+                        EmojiViewData(
+                            categoryIndex,
+                            idInCategory,
+                            eachEmoji.primary,
+                            eachEmoji.variants.toTypedArray()
+                        )
+                    )
+                }
+                listBuilder.add(builder.toList())
+            }
+            listBuilder.toList()
+        } finally {
+            Trace.endSection()
+        }
+    }
+
+    private suspend fun showEmojiPickerView(context: Context) {
         // get emoji picker
         val emojiPicker = inflate(context, R.layout.emoji_picker, this)
 
         // set headerView
         headerView = emojiPicker.findViewById(R.id.emoji_picker_header)
         headerView.layoutManager =
-            LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, /* reverseLayout= */false)
+            LinearLayoutManager(
+                context,
+                LinearLayoutManager.HORIZONTAL,
+                /* reverseLayout= */ false
+            )
         headerView.adapter = EmojiPickerHeaderAdapter(context)
 
         // set bodyView
         bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
-        bodyView.layoutManager = GridLayoutManager(
-            context, emojiGridColumns, LinearLayoutManager.VERTICAL, /* reverseLayout= */
-            false
+        val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
+        bodyView.adapter = getEmojiPickerBodyAdapter(
+            context, emojiGridColumns, emojiGridRows, categorizedEmojiData
         )
-        bodyView.adapter = EmojiPickerBodyAdapter(context, emojiGridColumns, emojiGridRows)
     }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
new file mode 100644
index 0000000..aace300
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.emoji2.emojipicker
+
+/** Concrete entry which contains emoji view data.  */
+internal class EmojiViewData(
+    categoryIndex: Int,
+    idInCategory: Int,
+    primary: String,
+    secondaries: Array<String>
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
+    /** The index of category where the emoji view located in.  */
+    private val categoryIndex: Int
+
+    /** The id of this emoji view in the category, usually is the position of the emoji.  */
+    private val idInCategory: Int
+
+    /** Primary key which is used for labeling and for PRESS action.  */
+    val primary: String
+
+    /** Secondary keys which are used for LONG_PRESS action.  */
+    val secondaries: Array<String>
+
+    /**
+     * Instantiates a EmojiViewData.
+     *
+     * @param categoryIndex Used to compute the id.
+     * @param idInCategory Used to compute the id.
+     * @param primary The default base variant of the given emoji (no skin tone or gender modifier)
+     * @param secondaries Array of variants associated to primary
+     */
+    init {
+        this.categoryIndex = categoryIndex
+        this.idInCategory = idInCategory
+        this.primary = primary
+        this.secondaries = secondaries
+    }
+
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE: Int = EmojiViewData::class.java.name.hashCode()
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
new file mode 100644
index 0000000..4dc5cd7
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+/** A [ViewHolder] containing an emoji view and emoji data.  */
+internal class EmojiViewHolder(
+    parent: ViewGroup,
+    layoutInflater: LayoutInflater,
+    width: Int,
+    height: Int
+) : ViewHolder(
+    layoutInflater
+        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+) {
+    private val emojiView: EmojiView
+
+    init {
+        itemView.layoutParams = LayoutParams(width, height)
+        emojiView = itemView.findViewById(R.id.emoji_view)
+        emojiView.isClickable = true
+    }
+
+    fun bindEmoji(
+        emojiViewItem: EmojiViewItem
+    ) {
+        emojiView.emoji = emojiViewItem.primary
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
similarity index 74%
rename from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
rename to emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
index dfb2685..2f79955 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.emoji2.emojipicker
 
-import androidx.compose.ui.graphics.Color
-
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+internal class EmojiViewItem(val primary: String, val variants: List<String>)
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
new file mode 100644
index 0000000..9b96673
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.emoji2.emojipicker
+
+/**
+ * Indicator to show "You haven't used any emojis yet"-like label for empty category.
+ *
+ * EmptyCategoryViewData: A full-width `Space` at the beginning of each empty category, to
+ * show description and indicate that there is no items in the category.
+ */
+internal class EmptyCategoryViewData
+/**
+ * Instantiates an EmptyCategoryViewData.
+ *
+ * @param categoryIndex Used to compute the id.
+ * @param idInCategory Used to compute the id.
+ * @param description The description showing in the text view, e.g. "You haven't used any emojis
+ * yet". If empty, will look up the corresponding description based on `categoryIndex`
+ * in [EmojiPickerBodyAdapter.onBindViewHolder].
+ */(
+    categoryIndex: Int,
+    idInCategory: Int,
+    /** The description to indicate the category is empty.  */
+    val description: String
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE = EmptyCategoryViewData::class.java.name.hashCode()
+
+        /**
+         * Use -1 as categoryIndex and idInCategory and empty string as description for the default
+         * instance. The categoryIndex and idInCategory are just used to compute the id for the instance.
+         * Make the description empty to look up the corresponding description based on category index in
+         * [EmojiPickerBodyAdapter.onBindViewHolder].
+         */
+        val INSTANCE = EmptyCategoryViewData( /* categoryIndex= */
+            -1, /* idInCategory= */-1, /* description= */""
+        )
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
new file mode 100644
index 0000000..6df7bae
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.emoji2.emojipicker
+
+import androidx.annotation.IntRange
+
+/** Value (immutable) classes for Emoji Picker.*/
+internal abstract class ItemViewData(val id: Long) {
+    abstract val type: Int
+
+    override fun hashCode(): Int {
+        return (id xor (id ushr 32)).toInt()
+    }
+
+    companion object {
+        fun calculateId(
+            type: Int,
+            @IntRange(from = 0, to = 256) categoryIndex: Int,
+            @IntRange(from = 0) idInCategory: Int
+        ): Long {
+            return type.toLong() shl 60 or (categoryIndex.toLong() shl 32) or idInCategory.toLong()
+        }
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
new file mode 100644
index 0000000..45edc3c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.emoji2.emojipicker
+
+import android.util.Log
+import androidx.annotation.IntRange
+
+/**
+ * Flattened list of categorized `ItemViewData` (`List<List<ItemViewData>>`) with placeholder
+ * entries and category separators.
+ *
+ * Keyword "position" is defined in `RecyclerView`.
+ */
+internal class ItemViewDataFlatList(
+    categorizedSources: List<List<ItemViewData>>,
+    @IntRange(from = 1) columns: Int
+) : AbstractList<ItemViewData>() {
+
+    companion object {
+        const val LOG_TAG = "ItemViewDataFlatList"
+    }
+
+    override val size: Int
+        get() = totalSize
+
+    /** Returns number of categories  */
+    /** # of categories.  */
+    @get:IntRange(from = 0)
+    val numberOfCategories: Int
+    private val categorizedSources: MutableList<List<ItemViewData>>
+    private val categorySizes: IntArray
+    private val categoryStartPositions: IntArray
+    private val columns: Int
+
+    /** == `size()`, including all types of `ItemViewData`s.  */
+    private var totalSize = 0
+
+    init {
+        this.categorizedSources = ArrayList(categorizedSources)
+        this.columns = columns
+        numberOfCategories = this.categorizedSources.size
+        categorySizes = IntArray(numberOfCategories)
+        categoryStartPositions = IntArray(numberOfCategories)
+        updateIndex()
+        if (categorizedSources.isEmpty()) {
+            Log.wtf(LOG_TAG, "Initialized with empty categorized sources")
+        }
+    }
+
+    private fun updateIndex() {
+        var categoryStartPosition = 0
+        for (currentCategoryIndex in 0 until numberOfCategories) {
+            val sources: List<ItemViewData> = categorizedSources[currentCategoryIndex]
+            val sourcesSize: Int = sources.size
+            categoryStartPositions[currentCategoryIndex] = categoryStartPosition
+            var sourcesSizeIncludingEmpty: Int
+            var rowsInCategory = Math.ceil(sourcesSize / columns.toDouble()).toInt()
+            // Guarantee showing at least `minRowsPerCategory` rows for each category.
+            rowsInCategory = Math.max(rowsInCategory, EmojiPickerConstants.MIN_ROWS_PER_CATEGORY)
+            sourcesSizeIncludingEmpty =
+                if (sourcesSize <= 0 || sourcesSize == 1 && sources[0] is EmptyCategoryViewData) {
+                    // category separator(occupy entire row) + empty category indicator(occupy entire row)
+                    // + placeholder view items
+                    1 + 1 + if (rowsInCategory >= 1) (rowsInCategory - 1) * columns else 0
+                } else {
+                    rowsInCategory * columns + 1 // +1 for category separator
+                }
+            categorySizes[currentCategoryIndex] = sourcesSizeIncludingEmpty
+            categoryStartPosition += sourcesSizeIncludingEmpty
+        }
+        totalSize = categoryStartPosition
+    }
+
+    override fun get(@IntRange(from = 0) index: Int): ItemViewData {
+        val currentCategoryIndex = getCategoryIndex(index)
+        val indexInCategory = index - categoryStartPositions[currentCategoryIndex]
+        return if (indexInCategory < 0) {
+            Log.wtf(
+                LOG_TAG,
+                String.format(
+                    "position (%d) for category (%d) is invalid",
+                    index,
+                    currentCategoryIndex
+                )
+
+            )
+            DummyViewData.INSTANCE
+        } else if (indexInCategory == 0) {
+            // Category separator occupies first place.
+            CategorySeparatorViewData(
+                currentCategoryIndex, indexInCategory, /* categoryName= */""
+            )
+        } else if (indexInCategory < categorizedSources[currentCategoryIndex].size + 1) {
+            // Concrete ItemViewData.
+            categorizedSources[currentCategoryIndex][indexInCategory - 1]
+        } else if (indexInCategory == 1 && categorizedSources[currentCategoryIndex].isEmpty()) {
+            // Empty category indicator.
+            EmptyCategoryViewData.INSTANCE
+        } else {
+            // Placeholder entries located at the end of category.
+            DummyViewData.INSTANCE
+        }
+    }
+
+    /** Returns category index for given `position`  */
+    @IntRange(from = 0)
+    fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
+        var currentCategoryIndex = 0
+        while (currentCategoryIndex + 1 < numberOfCategories &&
+            position >= categoryStartPositions[currentCategoryIndex + 1]
+        ) {
+            currentCategoryIndex++
+        }
+        return currentCategoryIndex
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
index 6c1d57d..7b1bdaf 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.content.ContextCompat
 import androidx.emoji2.emojipicker.BundledEmojiListLoader
+import androidx.emoji2.emojipicker.EmojiViewItem
 import java.io.File
 
 /**
@@ -52,8 +53,8 @@
     /** Get cache for a given file name, or write to a new file using the [defaultValue] factory. */
     internal fun getOrPut(
         key: String,
-        defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
-    ): List<BundledEmojiListLoader.EmojiData> {
+        defaultValue: () -> List<EmojiViewItem>
+    ): List<EmojiViewItem> {
         val targetDir = File(emojiPickerCacheDir, currentProperty)
         // No matching cache folder for current property, clear stale cache directory if any
         if (!targetDir.exists()) {
@@ -65,19 +66,19 @@
         return readFrom(targetFile) ?: writeTo(targetFile, defaultValue)
     }
 
-    private fun readFrom(targetFile: File): List<BundledEmojiListLoader.EmojiData>? {
+    private fun readFrom(targetFile: File): List<EmojiViewItem>? {
         if (!targetFile.isFile)
             return null
         return targetFile.bufferedReader()
             .useLines { it.toList() }
             .map { it.split(",") }
-            .map { BundledEmojiListLoader.EmojiData(it.first(), it.drop(1)) }
+            .map { EmojiViewItem(it.first(), it.drop(1)) }
     }
 
     private fun writeTo(
         targetFile: File,
-        defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
-    ): List<BundledEmojiListLoader.EmojiData> {
+        defaultValue: () -> List<EmojiViewItem>
+    ): List<EmojiViewItem> {
         val data = defaultValue.invoke()
         targetFile.bufferedWriter()
             .use { out ->
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.xml
new file mode 100644
index 0000000..3bae1dd
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  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.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="12dp" />
+            <solid android:color="@android:color/white" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
new file mode 100644
index 0000000..b5453f1
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:importantForAccessibility="no">
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/category_name"
+        android:layout_width="wrap_content"
+        android:layout_height="24dp"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"
+        android:paddingTop="4dp"
+        android:paddingLeft="7dp"
+        android:paddingRight="7dp"
+        android:gravity="center_vertical|start"
+        android:letterSpacing="0.1"
+        android:importantForAccessibility="yes"
+        style="?attr/EmojiPickerStyleCategoryLabelText"
+        android:textSize="12dp" />
+</RelativeLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
index 0753bdd..956d3f2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -24,7 +24,7 @@
         android:layout_width="wrap_content"
         android:layout_height="@dimen/emoji_picker_header_height"/>
 
-    <androidx.recyclerview.widget.RecyclerView
+    <androidx.emoji2.emojipicker.EmojiPickerBodyView
         android:id="@+id/emoji_picker_body"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
new file mode 100644
index 0000000..0fa8c9c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="0dp"
+    android:layout_height="0dp">
+
+    <androidx.emoji2.emojipicker.EmojiView
+        android:id="@+id/emoji_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/ripple_emoji_view"
+        android:importantForAccessibility="yes"
+        android:textSize="30dp"
+        tools:ignore="SpUsage" />
+</FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
index 3948f9f..0a2d01f2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
@@ -15,9 +15,11 @@
   -->
 
 <resources>
-    <attr name="EmojiPickerColorCategoryEmptyHintText" format="color"/>
-    <attr name="EmojiPickerColorHeaderIcon" format="color"/>
-    <attr name="EmojiPickerColorHeaderIconSelected" format="color"/>
-    <attr name="EmojiPickerStyleHeaderIcon" format="reference"/>
-    <attr name="EmojiPickerStyleCategoryEmptyHintText" format="reference"/>
+    <attr name="EmojiPickerColorCategoryEmptyHintText" format="color" />
+    <attr name="EmojiPickerColorCategoryLabelText" format="color" />
+    <attr name="EmojiPickerColorHeaderIcon" format="color" />
+    <attr name="EmojiPickerColorHeaderIconSelected" format="color" />
+    <attr name="EmojiPickerStyleCategoryEmptyHintText" format="reference" />
+    <attr name="EmojiPickerStyleCategoryLabelText" format="reference" />
+    <attr name="EmojiPickerStyleHeaderIcon" format="reference" />
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index e2ffab3..472702f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?><!--
   Copyright 2022 The Android Open Source Project
 
   Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +36,6 @@
 
     <!-- Shown in emoji keyboard (non-Recent category) when the category is empty. -->
     <string name="emoji_empty_non_recent_category">No emojis available</string>
+    <!-- Shown in emoji keyboard when Recent emoji is empty. -->
+    <string name="emoji_empty_recent_category">You haven\'t used any emojis yet</string>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index fa44e69..0e6d24e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -27,4 +27,8 @@
     <style name="EmojiPickerCategoryContainer">
         <item name="android:layout_marginBottom">8dp</item>
     </style>
+
+    <style name="EmojiPickerStyleCategoryLabelText">
+        <item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
+    </style>
 </resources>
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
index 8debe9d..6611e65 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -19,6 +19,7 @@
 import android.app.AlertDialog
 import android.app.Dialog
 import android.content.DialogInterface
+import android.os.Build
 import android.os.Bundle
 import android.os.Looper
 import androidx.fragment.app.test.EmptyFragmentTestActivity
@@ -111,6 +112,11 @@
 
     @Test
     fun testDialogFragmentDismiss() {
+        // Due to b/157955883, we need to early return if API == 30.
+        // Otherwise, this test flakes.
+        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+            return
+        }
         val fragment = TestDialogFragment()
         activityTestRule.runOnUiThread {
             fragment.showNow(activityTestRule.activity.supportFragmentManager, null)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
index 5ce2fab..c68e779 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.app.test.TestViewModel
+import androidx.fragment.app.test.ViewModelActivity
 import androidx.fragment.test.R
 import androidx.lifecycle.HasDefaultViewModelProviderFactory
 import androidx.lifecycle.ViewModel
@@ -129,6 +130,48 @@
         }
     }
 
+    @Test
+    fun testCreateViewModelViaExtrasSavedState() {
+        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                setContentView(R.layout.simple_container)
+                supportFragmentManager
+            }
+            val fragment = StrictViewFragment()
+
+            fm.beginTransaction()
+                .add(R.id.fragmentContainer, fragment, "fragment")
+                .commit()
+            executePendingTransactions()
+
+            val viewLifecycleOwner = (fragment.viewLifecycleOwner as FragmentViewLifecycleOwner)
+
+            val creationViewModel = ViewModelProvider(
+                viewLifecycleOwner.viewModelStore,
+                viewLifecycleOwner.defaultViewModelProviderFactory,
+                viewLifecycleOwner.defaultViewModelCreationExtras
+            )["test", ViewModelActivity.TestSavedStateViewModel::class.java]
+
+            creationViewModel.savedStateHandle["key"] = "value"
+
+            recreate()
+
+            val recreatedViewLifecycleOwner = withActivity {
+                supportFragmentManager.findFragmentByTag("fragment")?.viewLifecycleOwner
+                    as FragmentViewLifecycleOwner
+            }
+
+            val recreateViewModel = ViewModelProvider(recreatedViewLifecycleOwner)[
+                "test", ViewModelActivity.TestSavedStateViewModel::class.java
+            ]
+
+            assertThat(recreateViewModel).isSameInstanceAs(creationViewModel)
+
+            val value: String? = recreateViewModel.savedStateHandle["key"]
+            assertThat(value).isEqualTo("value")
+        }
+    }
+
     class FakeViewModelProviderFactory : ViewModelProvider.Factory {
         private var createCalled: Boolean = false
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
index 6f416d8..7c5330a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
@@ -16,8 +16,6 @@
 
 package androidx.fragment.app;
 
-import static androidx.lifecycle.SavedStateHandleSupport.enableSavedStateHandles;
-
 import android.app.Application;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -76,7 +74,6 @@
             mLifecycleRegistry = new LifecycleRegistry(this);
             mSavedStateRegistryController = SavedStateRegistryController.create(this);
             mSavedStateRegistryController.performAttach();
-            enableSavedStateHandles(this);
             mRestoreViewSavedStateRunnable.run();
         }
     }
@@ -134,7 +131,7 @@
 
             mDefaultFactory = new SavedStateViewModelFactory(
                     application,
-                    this,
+                    mFragment,
                     mFragment.getArguments());
         }
 
@@ -158,7 +155,7 @@
         if (application != null) {
             extras.set(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY, application);
         }
-        extras.set(SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY, this);
+        extras.set(SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY, mFragment);
         extras.set(SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY, this);
         if (mFragment.getArguments() != null) {
             extras.set(SavedStateHandleSupport.DEFAULT_ARGS_KEY, mFragment.getArguments());
diff --git a/gradle/README.md b/gradle/README.md
index 232393b..e945e4e 100644
--- a/gradle/README.md
+++ b/gradle/README.md
@@ -4,7 +4,7 @@
 
 ## libs.versions.toml
 
-Keeps track of library and plugin dependencies used by androidx. Adding or updating a library there requires running `./development/importMaven/import_maven_artifacts.py -n myartifact:here:1.0.0`
+Keeps track of library and plugin dependencies used by androidx. Adding or updating a library there requires running `./development/importMaven/importMaven.sh myartifact:here:1.0.0`
 
 ## verification-keyring.keys
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 3c05d3a..4831e6ca 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -34,13 +34,13 @@
 hilt = "2.44"
 incap = "0.2"
 jcodec = "0.2.5"
-kotlin = "1.7.20"
+kotlin = "1.7.21"
 kotlinBenchmark = "0.4.5"
-kotlinNative = "1.7.20"
+kotlinNative = "1.7.21"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.6.4"
 kotlinSerialization = "1.3.3"
-ksp = "1.7.20-1.0.6"
+ksp = "1.7.21-1.0.8"
 ktlint = "0.46.0-20220520.192227-74"
 leakcanary = "2.8.1"
 metalava = "1.0.0-alpha06"
@@ -91,7 +91,7 @@
 checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
 checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
 constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.0.4" }
+dackka = { module = "com.google.devsite:dackka", version = "1.0.5" }
 dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
 daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
 dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
@@ -208,6 +208,7 @@
 protobufCompiler = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
 protobufGradlePluginz = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.9.0" }
 protobufLite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
+protobufKotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
 reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
 retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
 retrofitConverterWire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f9a0caf..764b8b2e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -910,21 +910,21 @@
             <sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.20" androidx:reason="Unsigned, b/227204920">
-         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz">
-            <sha256 value="2b82114ad226276f88a321bd2c47ed162214c5c579ac899dd1f9ccdac4c739b9" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.21" androidx:reason="Unsigned, b/227204920">
+         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz">
+            <sha256 value="b6a4aef343c029ac1b7d3e70101c45356a02a30b10fdd0813fb085b29cc714f4" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.7.20">
-         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.7.20.tar.gz">
-            <sha256 value="efdc6e5c1e5b15aa63989b725a2acfd5ffa6682824d7963e3b4b0987e5aecd3b" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.7.21">
+         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz">
+            <sha256 value="6953ddaa5ed2466ac606bd81338475af8064dce6a932aa51baaa0d3bff64bcc2" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.7.20">
-         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz">
-            <sha256 value="59ff4942f783def4c2293bccfbe2fd5fd7b3a8a6b878cfe95bb83921c75fe0cb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.7.21">
+         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz">
+            <sha256 value="ee01ebb7f44f8acc0aad87b61219f292dd766a06acb6ecba9fb21c5834c747eb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
    </components>
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index 31442ee..39e1c14 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -28,9 +28,9 @@
  * app; a motion predictor is a utility that provides predicted {@link android.view.MotionEvent}
  * based on the previously received ones. Obtain a new predictor instance using
  * {@link #newInstance(android.view.View)}; put the motion events you receive into it with
- * {@link #recordMovement(android.view.MotionEvent)}, and call {@link #predict()} to retrieve the
+ * {@link #record(android.view.MotionEvent)}, and call {@link #predict()} to retrieve the
  * predicted  {@link android.view.MotionEvent} that would occur at the moment the next frame is
- * rendered on the display. Once no more predictions are needed, call {@link #dispose()} to stop it
+ * rendered on the display. Once no more predictions are needed, call {@link #close()} to stop it
  * and clean up resources.
  */
 public interface MotionEventPredictor extends AutoCloseable {
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index df88594..106c01e4 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -30,11 +30,9 @@
  */
 @RestrictTo(LIBRARY)
 public class KalmanMotionEventPredictor implements MotionEventPredictor {
-    private MultiPointerPredictor mMultiPointerPredictor;
-    private boolean mClosed = false;
+    private MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
 
     public KalmanMotionEventPredictor() {
-        mMultiPointerPredictor = new MultiPointerPredictor();
         // 1 may seem arbitrary, but this basically tells the predictor to
         // just predict the next MotionEvent.
         // This will need to change as we want to build a prediction depending
@@ -44,13 +42,16 @@
 
     @Override
     public void record(@NonNull MotionEvent event) {
+        if (mMultiPointerPredictor == null) {
+            return;
+        }
         mMultiPointerPredictor.onTouchEvent(event);
     }
 
     @Nullable
     @Override
     public MotionEvent predict() {
-        if (mClosed) {
+        if (mMultiPointerPredictor == null) {
             return null;
         }
         return mMultiPointerPredictor.predict();
@@ -58,6 +59,6 @@
 
     @Override
     public void close() {
-        mClosed = true;
+        mMultiPointerPredictor = null;
     }
 }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index 1b0b3cd..f5929e0 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -36,7 +36,7 @@
     private static final String TAG = "MultiPointerPredictor";
     private static final boolean DEBUG_PREDICTION = Log.isLoggable(TAG, Log.DEBUG);
 
-    private SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
+    private final SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
     private int mPredictionTargetMs = 0;
     private int mReportRateMs = 0;
 
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
index 6a80269..65d828d 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
@@ -30,14 +30,14 @@
  */
 @RestrictTo(LIBRARY)
 public class PointerKalmanFilter {
-    private KalmanFilter mXKalman;
-    private KalmanFilter mYKalman;
-    private KalmanFilter mPKalman;
+    private final KalmanFilter mXKalman;
+    private final KalmanFilter mYKalman;
+    private final KalmanFilter mPKalman;
 
-    private DVector2 mPosition = new DVector2();
-    private DVector2 mVelocity = new DVector2();
-    private DVector2 mAcceleration = new DVector2();
-    private DVector2 mJank = new DVector2();
+    private final DVector2 mPosition = new DVector2();
+    private final DVector2 mVelocity = new DVector2();
+    private final DVector2 mAcceleration = new DVector2();
+    private final DVector2 mJank = new DVector2();
     private double mPressure = 0;
     private double mPressureChange = 0;
 
@@ -46,9 +46,9 @@
 
     private int mNumIterations = 0;
 
-    private Matrix mNewX = new Matrix(1, 1);
-    private Matrix mNewY = new Matrix(1, 1);
-    private Matrix mNewP = new Matrix(1, 1);
+    private final Matrix mNewX = new Matrix(1, 1);
+    private final Matrix mNewY = new Matrix(1, 1);
+    private final Matrix mNewP = new Matrix(1, 1);
 
     /**
      * @param sigmaProcess lower value = more filtering
diff --git a/libraryversions.toml b/libraryversions.toml
index 20d5b80..b5af2eb 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -17,11 +17,11 @@
 CAMERA = "1.3.0-alpha01"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
-CAR_APP = "1.3.0-rc01"
+CAR_APP = "1.4.0-alpha01"
 COLLECTION = "1.3.0-alpha03"
 COLLECTION_KMP = "1.3.0-dev01"
 COMPOSE = "1.4.0-alpha02"
-COMPOSE_COMPILER = "1.4.0-alpha01"
+COMPOSE_COMPILER = "1.4.0-alpha02"
 COMPOSE_MATERIAL3 = "1.1.0-alpha02"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
 CONSTRAINTLAYOUT = "2.2.0-alpha05"
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
index a0955d6..6c9d4b8 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
@@ -27,6 +27,7 @@
 import androidx.core.os.bundleOf
 import androidx.savedstate.SavedStateRegistry
 import java.io.Serializable
+import java.lang.ClassCastException
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -220,8 +221,15 @@
      */
     @MainThread
     operator fun <T> get(key: String): T? {
-        @Suppress("UNCHECKED_CAST")
-        return regular[key] as T?
+        return try {
+            @Suppress("UNCHECKED_CAST")
+            regular[key] as T?
+        } catch (e: ClassCastException) {
+            // Instead of failing on ClassCastException, we remove the value from the
+            // SavedStateHandle and return null.
+            remove<T>(key)
+            null
+        }
     }
 
     /**
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 6f87eac..57222fb 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -31,10 +31,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    api(project(":lifecycle:lifecycle-runtime-ktx"))
+    api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.savedstate:savedstate-ktx:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
 
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 322181e..8495d53 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -33,13 +33,13 @@
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-compose"))
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 03a4616..fa30316 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,8 +26,8 @@
 dependencies {
     api(project(":navigation:navigation-common"))
     api("androidx.activity:activity-ktx:1.6.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    api(project(":lifecycle:lifecycle-runtime-ktx"))
+    api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
index 422199b..a417d4a 100644
--- a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
+++ b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
@@ -40,6 +40,9 @@
             <intent-filter>
                 <action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
+            </intent-filter>
         </receiver>
     </application>
 </manifest>
\ No newline at end of file
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
index b56858e..a1b18f0 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -850,6 +850,102 @@
     }
 
     @Test
+    public void rowCountForAccessibility_verticalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(34, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(3, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(1, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(2, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(3, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_horizontalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(34, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(2, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(1, count);
+    }
+
+    @Test
     public void accessibilityClassName() throws Throwable {
         final RecyclerView recyclerView = setupBasic(new Config(3, 100));
         waitForFirstLayout(recyclerView);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 1649118..f00957f 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -22,6 +22,8 @@
 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -1473,4 +1475,22 @@
     private void assertFirstItemIsAtTop() {
         assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (1)");
     }
+
+    @Test
+    public void onInitializeAccessibilityNodeInfo_noAdapter() throws Throwable {
+        mRecyclerView = inflateWrappedRV();
+        mLayoutManager = new WrappedLinearLayoutManager(
+                getActivity(), LinearLayoutManager.VERTICAL, false);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+        mActivityRule.runOnUiThread(() -> {
+            mLayoutManager.onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler,
+                    mRecyclerView.mState, nodeInfo);
+        });
+
+        assertThat(nodeInfo.getActionList()).doesNotContain(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
+
+    }
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
index 81fc754..aa53614 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
@@ -48,6 +48,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 
@@ -1424,4 +1425,45 @@
                 Math.max(start, end), event.getToIndex());
 
     }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final int itemCount = 2;
+        Config config = new Config(HORIZONTAL, false, 3, GAP_HANDLING_NONE).itemCount(itemCount);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getRowCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(itemCount, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final int itemCount = 2;
+        Config config = new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(itemCount);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getColumnCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(itemCount, count);
+    }
+
+    @Test
+    public void onInitializeAccessibilityNodeInfo() throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
+        waitFirstLayout();
+        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+
+        mActivityRule.runOnUiThread(
+                () -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(
+                        mRecyclerView.mRecycler, mRecyclerView.mState, info));
+        assertEquals(info.getClassName(),
+                "androidx.recyclerview.widget.StaggeredGridLayoutManager");
+    }
 }
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
index d18f0b9..f043bf6 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -122,7 +122,7 @@
     public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
             RecyclerView.State state) {
         if (mOrientation == HORIZONTAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, getItemCount());
         }
         if (state.getItemCount() < 1) {
             return 0;
@@ -136,7 +136,7 @@
     public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
             RecyclerView.State state) {
         if (mOrientation == VERTICAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, getItemCount());
         }
         if (state.getItemCount() < 1) {
             return 0;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 19a9b5f..ac79210 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -294,7 +294,7 @@
             @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
         super.onInitializeAccessibilityNodeInfo(recycler, state, info);
         // TODO(b/251823537)
-        if (mRecyclerView.mAdapter.getItemCount() > 0) {
+        if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 0) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 info.addAction(AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
             }
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
index 73f1d50..df03e317 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
@@ -1291,6 +1291,16 @@
     }
 
     @Override
+    public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
+            @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+        // Setting the classname allows accessibility services to set a role for staggered grids
+        // and ensures that they are treated distinctly from canonical grids with clear row/column
+        // semantics.
+        info.setClassName("androidx.recyclerview.widget.StaggeredGridLayoutManager");
+    }
+
+    @Override
     public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state, @NonNull View host,
             @NonNull AccessibilityNodeInfoCompat info) {
@@ -1346,7 +1356,7 @@
     public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state) {
         if (mOrientation == HORIZONTAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, state.getItemCount());
         }
         return super.getRowCountForAccessibility(recycler, state);
     }
@@ -1355,7 +1365,7 @@
     public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state) {
         if (mOrientation == VERTICAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, state.getItemCount());
         }
         return super.getColumnCountForAccessibility(recycler, state);
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
index 69b9a8f..e384141 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
@@ -59,6 +59,9 @@
         fun nextControlFlow(controlFlow: String, vararg args: Any?): Builder
         fun endControlFlow(): Builder
 
+        fun indent(): Builder
+        fun unindent(): Builder
+
         fun build(): XCodeBlock
 
         companion object {
@@ -71,7 +74,7 @@
                 name: String,
                 typeName: XTypeName,
                 assignExprFormat: String,
-                vararg assignExprArgs: Any
+                vararg assignExprArgs: Any?
             ) = apply {
                 addLocalVariable(
                     name = name,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
index 342c41c..56cc91c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
@@ -34,6 +34,8 @@
 
     interface Builder : TargetLanguage {
 
+        val name: String
+
         fun addAnnotation(annotation: XAnnotationSpec)
 
         // TODO(b/247247442): Maybe make a XParameterSpec ?
@@ -135,7 +137,11 @@
                     KotlinFunSpec.Builder(
                         name,
                         FunSpec.constructorBuilder().apply {
-                            addModifiers(visibility.toKotlinVisibilityModifier())
+                            // Workaround for the unreleased fix in
+                            // https://github.com/square/kotlinpoet/pull/1342
+                            if (visibility != VisibilityModifier.PUBLIC) {
+                                addModifiers(visibility.toKotlinVisibilityModifier())
+                            }
                         }
                     )
                 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
index 36bf690..a66f12e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
@@ -73,6 +73,22 @@
                 )
             }
         }
+
+        fun XPropertySpec.Builder.apply(
+            javaFieldBuilder: com.squareup.javapoet.FieldSpec.Builder.() -> Unit,
+            kotlinPropertyBuilder: com.squareup.kotlinpoet.PropertySpec.Builder.() -> Unit,
+        ): XPropertySpec.Builder = apply {
+            when (language) {
+                CodeLanguage.JAVA -> {
+                    check(this is JavaPropertySpec.Builder)
+                    this.actual.javaFieldBuilder()
+                }
+                CodeLanguage.KOTLIN -> {
+                    check(this is KotlinPropertySpec.Builder)
+                    this.actual.kotlinPropertyBuilder()
+                }
+            }
+        }
     }
 }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 222db27..d1416b0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -40,6 +40,7 @@
 import com.squareup.kotlinpoet.javapoet.JTypeName
 import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
 import com.squareup.kotlinpoet.javapoet.KClassName
+import com.squareup.kotlinpoet.javapoet.KParameterizedTypeName
 import com.squareup.kotlinpoet.javapoet.KTypeName
 import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
 import kotlin.reflect.KClass
@@ -56,11 +57,27 @@
 open class XTypeName protected constructor(
     internal open val java: JTypeName,
     internal open val kotlin: KTypeName,
-    internal val nullability: XNullability
+    val nullability: XNullability
 ) {
     val isPrimitive: Boolean
         get() = java.isPrimitive
 
+    /**
+     * Returns the raw [XTypeName] if this is a parametrized type name, or itself if not.
+     *
+     * @see [XClassName.parametrizedBy]
+     */
+    val rawTypeName: XTypeName
+        get() {
+            val javaRawType = java.let {
+                if (it is JParameterizedTypeName) it.rawType else it
+            }
+            val kotlinRawType = kotlin.let {
+                if (it is KParameterizedTypeName) it.rawType else it
+            }
+            return XTypeName(javaRawType, kotlinRawType, nullability)
+        }
+
     open fun copy(nullable: Boolean): XTypeName {
         // TODO(b/248633751): Handle primitive to boxed when becoming nullable?
         return XTypeName(
@@ -214,6 +231,11 @@
     val simpleNames: List<String> = java.simpleNames()
     val canonicalName: String = java.canonicalName()
 
+    /**
+     * Returns a parameterized type, applying the `typeArguments` to `this`.
+     *
+     * @see [XTypeName.rawTypeName]
+     */
     fun parametrizedBy(
         vararg typeArguments: XTypeName,
     ): XTypeName {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
index 5261713..c4608e9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
@@ -37,12 +37,13 @@
         fun addProperty(propertySpec: XPropertySpec): Builder
         fun addFunction(functionSpec: XFunSpec): Builder
         fun addType(typeSpec: XTypeSpec): Builder
+        fun setPrimaryConstructor(functionSpec: XFunSpec): Builder
         fun setVisibility(visibility: VisibilityModifier)
         fun build(): XTypeSpec
 
         companion object {
 
-            fun XTypeSpec.Builder.addOriginatingElement(element: XElement) = apply {
+            fun Builder.addOriginatingElement(element: XElement) = apply {
                 when (language) {
                     CodeLanguage.JAVA -> {
                         check(this is JavaTypeSpec.Builder)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
index b8d2752..3620c0e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
@@ -88,6 +88,14 @@
             actual.endControlFlow()
         }
 
+        override fun indent() = apply {
+            actual.indent()
+        }
+
+        override fun unindent() = apply {
+            actual.unindent()
+        }
+
         override fun build(): XCodeBlock {
             return JavaCodeBlock(actual.build())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 470f3b7..fc14709 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -22,7 +22,6 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.processing.KnownTypeNames.KOTLIN_UNIT
 import androidx.room.compiler.processing.XNullability
 import com.squareup.javapoet.CodeBlock
 import com.squareup.javapoet.MethodSpec
@@ -36,7 +35,7 @@
 ) : JavaLang(), XFunSpec {
 
     internal class Builder(
-        private val name: String,
+        override val name: String,
         internal val actual: MethodSpec.Builder
     ) : JavaLang(), XFunSpec.Builder {
 
@@ -82,7 +81,7 @@
         }
 
         override fun returns(typeName: XTypeName) = apply {
-            if (typeName.java == JTypeName.VOID || typeName.java == KOTLIN_UNIT) {
+            if (typeName.java == JTypeName.VOID) {
                 return@apply
             }
             // TODO(b/247242374) Add nullability annotations for non-private methods
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
index f281154..8a50dae 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
@@ -69,6 +69,8 @@
             actual.addType(typeSpec.actual)
         }
 
+        override fun setPrimaryConstructor(functionSpec: XFunSpec) = addFunction(functionSpec)
+
         override fun setVisibility(visibility: VisibilityModifier) {
             actual.addModifiers(visibility.toJavaVisibilityModifier())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
index 7958c03..825a499 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
@@ -94,6 +94,14 @@
             actual.endControlFlow()
         }
 
+        override fun indent() = apply {
+            actual.indent()
+        }
+
+        override fun unindent() = apply {
+            actual.unindent()
+        }
+
         override fun build(): XCodeBlock {
             return KotlinCodeBlock(actual.build())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
index 4c90427..4f95e62 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
@@ -24,7 +24,6 @@
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
-import com.squareup.kotlinpoet.UNIT
 
 internal class KotlinFunSpec(
     override val name: String,
@@ -32,7 +31,7 @@
 ) : KotlinLang(), XFunSpec {
 
     internal class Builder(
-        private val name: String,
+        override val name: String,
         internal val actual: FunSpec.Builder
     ) : KotlinLang(), XFunSpec.Builder {
 
@@ -68,9 +67,6 @@
         }
 
         override fun returns(typeName: XTypeName) = apply {
-            if (typeName.kotlin == UNIT) {
-                return@apply
-            }
             actual.returns(typeName.kotlin)
         }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
index 425a758..3151ff0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
@@ -69,6 +69,14 @@
             actual.addType(typeSpec.actual)
         }
 
+        override fun setPrimaryConstructor(functionSpec: XFunSpec) = apply {
+            require(functionSpec is KotlinFunSpec)
+            actual.primaryConstructor(functionSpec.actual)
+            functionSpec.actual.delegateConstructorArguments.forEach {
+                actual.addSuperclassConstructorParameter(it)
+            }
+        }
+
         override fun setVisibility(visibility: VisibilityModifier) {
             actual.addModifiers(visibility.toKotlinVisibilityModifier())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
index 1f71ce4..13fd9f5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
@@ -24,6 +24,7 @@
 import com.google.devtools.ksp.processing.Resolver
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
+import com.squareup.kotlinpoet.javapoet.KClassName
 import javax.annotation.processing.ProcessingEnvironment
 import kotlin.reflect.KClass
 
@@ -119,7 +120,17 @@
         }
         return when (backend) {
             Backend.JAVAC -> requireType(typeName.java)
-            Backend.KSP -> requireType(typeName.kotlin.toString())
+            Backend.KSP -> {
+                val kClassName = typeName.kotlin as? KClassName
+                    ?: error("cannot find required type ${typeName.kotlin}")
+                requireType(kClassName.canonicalName)
+            }
+        }.let {
+            when (typeName.nullability) {
+                XNullability.NULLABLE -> it.makeNullable()
+                XNullability.NONNULL -> it.makeNonNullable()
+                XNullability.UNKNOWN -> it
+            }
         }
     }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
index 4a42d64..f325855 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
@@ -124,4 +124,11 @@
             ).hashCode()
         ).isEqualTo(expectedClass.hashCode())
     }
+
+    @Test
+    fun rawType() {
+        val expectedRawClass = XClassName.get("foo", "Bar")
+        assertThat(expectedRawClass.parametrizedBy(String::class.asClassName()).rawTypeName)
+            .isEqualTo(expectedRawClass)
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index b49b301..3802cb2 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -587,12 +587,7 @@
                     )
                 subject.getField("x").let { field ->
                     assertThat(field.isFinal()).isFalse()
-                    // b/250567151: Remove exception for KSP + classes
-                    if (invocation.isKsp && pkg == "lib") {
-                        assertThat(field.isPrivate()).isTrue()
-                    } else {
-                        assertThat(field.isPrivate()).isFalse()
-                    }
+                    assertThat(field.isPrivate()).isFalse()
                 }
             }
         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
index 766bfe6..b753598 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
@@ -30,7 +30,6 @@
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import com.google.devtools.ksp.symbol.Origin
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeVariableName
@@ -233,22 +232,11 @@
             val element = invocation.processingEnv.requireTypeElement(input.qName)
             input.expected.forEach { (name, modifiers) ->
                 val field = element.getField(name)
-                // b/250567151: Remove exception for KSP + classes
-                if (invocation.isKsp &&
-                        (element as KspTypeElement).declaration.origin == Origin.KOTLIN_LIB &&
-                        name.lowercase().contains("lateinit")) {
-                    assertWithMessage("${input.qName}:$name")
-                        .that(field.modifiers)
-                        .containsExactlyElementsIn(
-                            listOf(PRIVATE)
-                        )
-                } else {
-                    assertWithMessage("${input.qName}:$name")
-                        .that(field.modifiers)
-                        .containsExactlyElementsIn(
-                            modifiers
-                        )
-                }
+                assertWithMessage("${input.qName}:$name")
+                    .that(field.modifiers)
+                    .containsExactlyElementsIn(
+                        modifiers
+                    )
                 assertThat(field.enclosingElement).isEqualTo(element)
             }
         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index e3cbe2f..4075f36 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -204,6 +204,24 @@
             fileDependencies[fileName] = dependencies
             return OutputStream.nullOutputStream()
         }
+
+        override fun associateByPath(
+            sources: List<KSFile>,
+            path: String,
+            extensionName: String
+        ) {
+            // no-op for the sake of dependency tracking.
+        }
+
+        override fun createNewFileByPath(
+            dependencies: Dependencies,
+            path: String,
+            extensionName: String
+        ): OutputStream {
+            val fileName = path.split(File.separator).last()
+            fileDependencies[fileName] = dependencies
+            return OutputStream.nullOutputStream()
+        }
     }
 
     companion object {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index acad3d6..fa49f87 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -16,7 +16,6 @@
 
 package androidx.room
 
-import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XProcessingEnvConfig
@@ -100,7 +99,7 @@
         }
 
         databases?.forEach { db ->
-            DatabaseWriter(db, CodeLanguage.JAVA).write(context.processingEnv)
+            DatabaseWriter(db, context.codeLanguage).write(context.processingEnv)
             if (db.exportSchema) {
                 val schemaOutFolderPath = context.schemaOutFolderPath
                 if (schemaOutFolderPath == null) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
similarity index 79%
rename from room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
rename to room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 9ef5f62..03390ee 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -28,6 +28,8 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.asMutableClassName
+import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.ext.CommonTypeNames.STRING
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.CodeBlock
@@ -51,15 +53,14 @@
     get() = ArrayTypeName.of(typeName)
 
 object SupportDbTypeNames {
-    val DB: ClassName = ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
+    val DB = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
     val SQLITE_STMT: XClassName =
         XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteStatement")
-    val SQLITE_OPEN_HELPER: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper")
-    val SQLITE_OPEN_HELPER_CALLBACK: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper.Callback")
-    val SQLITE_OPEN_HELPER_CONFIG: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper.Configuration")
+    val SQLITE_OPEN_HELPER = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper")
+    val SQLITE_OPEN_HELPER_CALLBACK =
+        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Callback")
+    val SQLITE_OPEN_HELPER_CONFIG =
+        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Configuration")
     val QUERY = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteQuery")
 }
 
@@ -67,7 +68,8 @@
     val STRING_UTIL: XClassName = XClassName.get("$ROOM_PACKAGE.util", "StringUtil")
     val ROOM_DB: XClassName = XClassName.get(ROOM_PACKAGE, "RoomDatabase")
     val ROOM_DB_KT = XClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
-    val ROOM_DB_CONFIG: ClassName = ClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
+    val ROOM_DB_CALLBACK = XClassName.get(ROOM_PACKAGE, "RoomDatabase", "Callback")
+    val ROOM_DB_CONFIG = XClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
     val INSERTION_ADAPTER: XClassName =
         XClassName.get(ROOM_PACKAGE, "EntityInsertionAdapter")
     val UPSERTION_ADAPTER: XClassName =
@@ -76,45 +78,35 @@
         XClassName.get(ROOM_PACKAGE, "EntityDeletionOrUpdateAdapter")
     val SHARED_SQLITE_STMT: XClassName =
         XClassName.get(ROOM_PACKAGE, "SharedSQLiteStatement")
-    val INVALIDATION_TRACKER: ClassName =
-        ClassName.get(ROOM_PACKAGE, "InvalidationTracker")
+    val INVALIDATION_TRACKER = XClassName.get(ROOM_PACKAGE, "InvalidationTracker")
     val INVALIDATION_OBSERVER: ClassName =
         ClassName.get("$ROOM_PACKAGE.InvalidationTracker", "Observer")
     val ROOM_SQL_QUERY: XClassName =
         XClassName.get(ROOM_PACKAGE, "RoomSQLiteQuery")
-    val OPEN_HELPER: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper")
-    val OPEN_HELPER_DELEGATE: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper.Delegate")
-    val OPEN_HELPER_VALIDATION_RESULT: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper.ValidationResult")
-    val TABLE_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo")
-    val TABLE_INFO_COLUMN: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.Column")
-    val TABLE_INFO_FOREIGN_KEY: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.ForeignKey")
-    val TABLE_INFO_INDEX: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.Index")
-    val FTS_TABLE_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "FtsTableInfo")
-    val VIEW_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "ViewInfo")
+    val OPEN_HELPER = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper")
+    val OPEN_HELPER_DELEGATE = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "Delegate")
+    val OPEN_HELPER_VALIDATION_RESULT =
+        XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "ValidationResult")
+    val TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "TableInfo")
+    val TABLE_INFO_COLUMN = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Column")
+    val TABLE_INFO_FOREIGN_KEY = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "ForeignKey")
+    val TABLE_INFO_INDEX =
+        XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Index")
+    val FTS_TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "FtsTableInfo")
+    val VIEW_INFO = XClassName.get("$ROOM_PACKAGE.util", "ViewInfo")
     val LIMIT_OFFSET_DATA_SOURCE: ClassName =
         ClassName.get("$ROOM_PACKAGE.paging", "LimitOffsetDataSource")
     val DB_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "DBUtil")
     val CURSOR_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "CursorUtil")
-    val MIGRATION: ClassName = ClassName.get("$ROOM_PACKAGE.migration", "Migration")
-    val AUTO_MIGRATION_SPEC: ClassName = ClassName.get(
-        "$ROOM_PACKAGE.migration",
-        "AutoMigrationSpec"
-    )
+    val MIGRATION = XClassName.get("$ROOM_PACKAGE.migration", "Migration")
+    val AUTO_MIGRATION_SPEC = XClassName.get("$ROOM_PACKAGE.migration", "AutoMigrationSpec")
     val UUID_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "UUIDUtil")
     val AMBIGUOUS_COLUMN_RESOLVER: ClassName =
         ClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
+    val RELATION_UTIL = XClassName.get("androidx.room.util", "RelationUtil")
 }
 
 object PagingTypeNames {
@@ -144,13 +136,13 @@
 
 object AndroidTypeNames {
     val CURSOR: XClassName = XClassName.get("android.database", "Cursor")
-    val BUILD: ClassName = ClassName.get("android.os", "Build")
+    val BUILD = XClassName.get("android.os", "Build")
     val CANCELLATION_SIGNAL: XClassName = XClassName.get("android.os", "CancellationSignal")
 }
 
 object CollectionTypeNames {
-    val ARRAY_MAP: ClassName = ClassName.get(COLLECTION_PACKAGE, "ArrayMap")
-    val LONG_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
+    val ARRAY_MAP = XClassName.get(COLLECTION_PACKAGE, "ArrayMap")
+    val LONG_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
     val INT_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
 }
 
@@ -159,16 +151,18 @@
 }
 
 object CommonTypeNames {
-    val ARRAYS = ClassName.get("java.util", "Arrays")
-    val LIST = ClassName.get("java.util", "List")
+    val LIST = List::class.asClassName()
     val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
-    val MAP = ClassName.get("java.util", "Map")
-    val SET = ClassName.get("java.util", "Set")
-    val STRING = ClassName.get("java.lang", "String")
+    val MAP = Map::class.asClassName()
+    val HASH_MAP = XClassName.get("java.util", "HashMap")
+    val SET = Set::class.asClassName()
+    val HASH_SET = XClassName.get("java.util", "HashSet")
+    val STRING = String::class.asClassName()
     val INTEGER = ClassName.get("java.lang", "Integer")
     val OPTIONAL = ClassName.get("java.util", "Optional")
     val UUID = ClassName.get("java.util", "UUID")
-    val BYTE_BUFFER = ClassName.get("java.nio", "ByteBuffer")
+    val BYTE_BUFFER = XClassName.get("java.nio", "ByteBuffer")
+    val JAVA_CLASS = XClassName.get("java.lang", "Class")
 }
 
 object GuavaBaseTypeNames {
@@ -259,15 +253,24 @@
     val RECEIVE_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "ReceiveChannel")
     val SEND_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "SendChannel")
     val FLOW = ClassName.get("kotlinx.coroutines.flow", "Flow")
+    val LAZY = XClassName.get("kotlin", "Lazy")
 }
 
 object RoomMemberNames {
+    val DB_UTIL_QUERY = RoomTypeNames.DB_UTIL.packageMember("query")
+    val DB_UTIL_DROP_FTS_SYNC_TRIGGERS = RoomTypeNames.DB_UTIL.packageMember("dropFtsSyncTriggers")
     val CURSOR_UTIL_GET_COLUMN_INDEX =
         RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
     val ROOM_SQL_QUERY_ACQUIRE =
         RoomTypeNames.ROOM_SQL_QUERY.companionMember("acquire", isJvmStatic = true)
     val ROOM_DATABASE_WITH_TRANSACTION =
         RoomTypeNames.ROOM_DB_KT.packageMember("withTransaction")
+    val TABLE_INFO_READ =
+        RoomTypeNames.TABLE_INFO.companionMember("read", isJvmStatic = true)
+    val FTS_TABLE_INFO_READ =
+        RoomTypeNames.FTS_TABLE_INFO.companionMember("read", isJvmStatic = true)
+    val VIEW_INFO_READ =
+        RoomTypeNames.VIEW_INFO.companionMember("read", isJvmStatic = true)
 }
 
 val DEFERRED_TYPES = listOf(
@@ -392,7 +395,7 @@
                 CodeBlock.join(
                     List(columnSizeProducer(i)) { j ->
                         CodeBlock.of(
-                            if (type == CommonTypeNames.STRING) S else L,
+                            if (type == STRING.toJavaPoet()) S else L,
                             valueProducer(i, j)
                         )
                     },
@@ -426,4 +429,16 @@
         CodeLanguage.KOTLIN -> "%L.size" // kotlin.Array.size and primitives (e.g. IntArray)
     },
     varName
+)
+
+/**
+ * Code of expression for [Map.keys] in Kotlin, and [java.util.Map.keySet] for Java.
+ */
+fun MapKeySetExprCode(language: CodeLanguage, varName: String) = XCodeBlock.of(
+    language,
+    when (language) {
+        CodeLanguage.JAVA -> "%L.keySet()" // java.util.Map.keySet()
+        CodeLanguage.KOTLIN -> "%L.keys" // kotlin.collections.Map.keys
+    },
+    varName
 )
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
index 7272936..1bb79c5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.ext
 
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isByte
@@ -60,7 +61,7 @@
 /**
  * Returns `true` if this is a `ByteBuffer` type.
  */
-fun XType.isByteBuffer(): Boolean = typeName == BYTE_BUFFER
+fun XType.isByteBuffer(): Boolean = asTypeName() == BYTE_BUFFER
 
 /**
  * Returns `true` if this represents a `UUID` type.
@@ -111,7 +112,7 @@
 fun XType.isSupportedMapTypeArg(): Boolean {
     if (this.typeName.isPrimitive) return true
     if (this.typeName.isBoxedPrimitive) return true
-    if (this.typeName == CommonTypeNames.STRING) return true
+    if (this.typeName == CommonTypeNames.STRING.toJavaPoet()) return true
     if (this.isTypeOf(ByteArray::class)) return true
     if (this.isArray() && this.isByte()) return true
     val typeElement = this.typeElement ?: return false
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index b76b817..f8266b0 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -17,17 +17,17 @@
 package androidx.room.parser
 
 import androidx.room.ColumnInfo
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CommonTypeNames
 import androidx.room.parser.expansion.isCoreSelect
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
+import java.util.Locale
 import org.antlr.v4.runtime.tree.ParseTree
 import org.antlr.v4.runtime.tree.TerminalNode
-import java.util.Locale
 
-@Suppress("FunctionName")
 class QueryVisitor(
     private val original: String,
     private val syntaxErrors: List<String>,
@@ -269,7 +269,7 @@
 
     fun getTypeMirrors(env: XProcessingEnv): List<XType>? {
         return when (this) {
-            TEXT -> withBoxedAndNullableTypes(env, CommonTypeNames.STRING)
+            TEXT -> withBoxedAndNullableTypes(env, CommonTypeNames.STRING.toJavaPoet())
             INTEGER -> withBoxedAndNullableTypes(
                 env, TypeName.INT, TypeName.BYTE, TypeName.CHAR,
                 TypeName.LONG, TypeName.SHORT
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
index 14ec3a8..a3bc71c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.RewriteQueriesToDropUnusedColumns
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
@@ -133,14 +134,16 @@
             processingEnv.requireType("java.lang.Void")
         }
         val STRING: XType by lazy {
-            processingEnv.requireType("java.lang.String")
+            processingEnv.requireType(String::class.asClassName())
         }
         val READONLY_COLLECTION: XType by lazy {
-            if (processingEnv.backend == XProcessingEnv.Backend.KSP) {
-                processingEnv.requireType("kotlin.collections.Collection")
-            } else {
-                processingEnv.requireType("java.util.Collection")
-            }
+            processingEnv.requireType(Collection::class.asClassName())
+        }
+        val LIST: XType by lazy {
+            processingEnv.requireType(List::class.asClassName())
+        }
+        val SET: XType by lazy {
+            processingEnv.requireType(Set::class.asClassName())
         }
     }
 
@@ -168,9 +171,33 @@
         return Pair(result, collector)
     }
 
-    fun fork(element: XElement, forceSuppressedWarnings: Set<Warning> = emptySet()): Context {
+    /**
+     * Forks the processor context adding suppressed warnings a type converters found in the
+     * given [element].
+     *
+     * @param element the element from which to create the fork.
+     * @param forceSuppressedWarnings the warning that will be silenced regardless if they are
+     * present or not in the [element].
+     * @param forceBuiltInConverters the built-in converter states that will be set regardless of
+     * the states found in the [element].
+     */
+    fun fork(
+        element: XElement,
+        forceSuppressedWarnings: Set<Warning> = emptySet(),
+        forceBuiltInConverters: BuiltInConverterFlags? = null
+    ): Context {
         val suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element)
-        val processConvertersResult = CustomConverterProcessor.findConverters(this, element)
+        val processConvertersResult =
+            CustomConverterProcessor.findConverters(this, element).let { result ->
+                if (forceBuiltInConverters != null) {
+                    result.copy(
+                        builtInConverterFlags =
+                            result.builtInConverterFlags.withNext(forceBuiltInConverters)
+                    )
+                } else {
+                    result
+                }
+            }
         val subBuiltInConverterFlags = typeConverters.builtInConverterFlags.withNext(
             processConvertersResult.builtInConverterFlags
         )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
index e1fe4c3..ad2acae 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -184,16 +184,18 @@
     /**
      * Order of classes is important hence they are a LinkedHashSet not a set.
      */
-    open class ProcessResult(
+    data class ProcessResult(
         val classes: LinkedHashSet<XType>,
         val converters: List<CustomTypeConverterWrapper>,
         val builtInConverterFlags: BuiltInConverterFlags
     ) {
-        object EMPTY : ProcessResult(
-            classes = LinkedHashSet(),
-            converters = emptyList(),
-            builtInConverterFlags = BuiltInConverterFlags.DEFAULT
-        )
+        companion object {
+            val EMPTY = ProcessResult(
+                classes = LinkedHashSet(),
+                converters = emptyList(),
+                builtInConverterFlags = BuiltInConverterFlags.DEFAULT
+            )
+        }
 
         operator fun plus(other: ProcessResult): ProcessResult {
             val newClasses = LinkedHashSet<XType>()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 0781870..4f18b5d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -17,6 +17,7 @@
 package androidx.room.solver
 
 import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isEnum
@@ -598,24 +599,20 @@
                 immutableClassName = immutableClassName
             )
         } else if (typeMirror.isTypeOf(java.util.Map::class) ||
-            typeMirror.rawType.typeName == ARRAY_MAP ||
-            typeMirror.rawType.typeName == LONG_SPARSE_ARRAY ||
+            typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet() ||
+            typeMirror.rawType.typeName == LONG_SPARSE_ARRAY.toJavaPoet() ||
             typeMirror.rawType.typeName == INT_SPARSE_ARRAY
         ) {
-            val keyTypeArg = if (typeMirror.rawType.typeName == LONG_SPARSE_ARRAY) {
-                context.processingEnv.requireType(TypeName.LONG)
-            } else if (typeMirror.rawType.typeName == INT_SPARSE_ARRAY) {
-                context.processingEnv.requireType(TypeName.INT)
-            } else {
-                typeMirror.typeArguments[0].extendsBoundOrSelf()
+            val keyTypeArg = when (typeMirror.rawType.typeName) {
+                LONG_SPARSE_ARRAY.toJavaPoet() -> context.processingEnv.requireType(TypeName.LONG)
+                INT_SPARSE_ARRAY -> context.processingEnv.requireType(TypeName.INT)
+                else -> typeMirror.typeArguments[0].extendsBoundOrSelf()
             }
 
-            val isSparseArray = if (typeMirror.rawType.typeName == LONG_SPARSE_ARRAY) {
-                LONG_SPARSE_ARRAY
-            } else if (typeMirror.rawType.typeName == INT_SPARSE_ARRAY) {
-                INT_SPARSE_ARRAY
-            } else {
-                null
+            val isSparseArray = when (typeMirror.rawType.typeName) {
+                LONG_SPARSE_ARRAY.toJavaPoet() -> LONG_SPARSE_ARRAY.toJavaPoet()
+                INT_SPARSE_ARRAY -> INT_SPARSE_ARRAY
+                else -> null
             }
 
             val mapValueTypeArg = if (isSparseArray != null) {
@@ -673,7 +670,7 @@
                         keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
                         valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
                         valueCollectionType = mapValueTypeArg,
-                        isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP,
+                        isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet(),
                         isSparseArray = isSparseArray
                     )
                 } else {
@@ -709,7 +706,7 @@
                     keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
                     valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
                     valueCollectionType = null,
-                    isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP,
+                    isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet(),
                     isSparseArray = isSparseArray
                 )
             }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
index 6381625..9da6a49 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
@@ -17,6 +17,7 @@
 package androidx.room.solver.query.result
 
 import androidx.room.AmbiguousColumnResolver
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.DoubleArrayLiteral
 import androidx.room.ext.L
@@ -72,7 +73,7 @@
                 // query result column names from the Cursor and the result object column names in
                 // an array literal.
                 val rowMappings = DoubleArrayLiteral(
-                    type = CommonTypeNames.STRING,
+                    type = CommonTypeNames.STRING.toJavaPoet(),
                     rowSize = mappings.size,
                     columnSizeProducer = { i -> mappings[i].usedColumns.size },
                     valueProducer = { i, j -> mappings[i].usedColumns[j] }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
index 0139594..3f978e7 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
@@ -78,7 +78,7 @@
             cursorDelegateVarName = scope.getTmpVar("_wrappedCursor")
             val entityColumnNamesParam = CodeBlock.of(
                 "new $T[] { $L }",
-                CommonTypeNames.STRING,
+                CommonTypeNames.STRING.toJavaPoet(),
                 CodeBlock.join(entity.columnNames.map { CodeBlock.of(S, it) }, ",$W")
             )
             val entityColumnIndicesParam = CodeBlock.of(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
index a14741b..9b05f4a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CollectionTypeNames.ARRAY_MAP
 import androidx.room.ext.L
@@ -63,7 +64,7 @@
         )
     } else {
         ParameterizedTypeName.get(
-            if (isArrayMap) ARRAY_MAP else ClassName.get(Map::class.java),
+            if (isArrayMap) ARRAY_MAP.toJavaPoet() else ClassName.get(Map::class.java),
             keyTypeArg.typeName,
             declaredValueType
         )
@@ -77,7 +78,7 @@
     } else {
         // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
         ParameterizedTypeName.get(
-            if (isArrayMap) ARRAY_MAP else ClassName.get(LinkedHashMap::class.java),
+            if (isArrayMap) ARRAY_MAP.toJavaPoet() else ClassName.get(LinkedHashMap::class.java),
             keyTypeArg.typeName,
             declaredValueType
         )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
index 2320bba..72cbded 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
@@ -19,7 +19,7 @@
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.ext.AndroidTypeNames.CURSOR
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.L
 import androidx.room.ext.N
 import androidx.room.solver.CodeGenScope
@@ -76,7 +76,7 @@
         return MethodSpec.methodBuilder("convertRows").apply {
             addAnnotation(Override::class.java)
             addModifiers(Modifier.PROTECTED)
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
+            returns(ParameterizedTypeName.get(LIST.toJavaPoet(), itemTypeName))
             val cursorParam = ParameterSpec.builder(CURSOR.toJavaPoet(), "cursor")
                 .build()
             addParameter(cursorParam)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 2d1ac5e..d2b4ba1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -18,7 +18,6 @@
 
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.processor.ProcessorErrors
@@ -121,16 +120,16 @@
     private fun emitRelationCollectorsReady(cursorVarName: String, scope: CodeGenScope) {
         if (relationCollectors.isNotEmpty()) {
             relationCollectors.forEach { it.writeInitCode(scope) }
-            scope.builder().apply {
-                beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
+            scope.builder.apply {
+                beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
                     relationCollectors.forEach {
                         it.writeReadParentKeyCode(cursorVarName, fieldsWithIndices, scope)
                     }
                 }
                 endControlFlow()
+                addStatement("%L.moveToPosition(-1)", cursorVarName)
             }
-            scope.builder().addStatement("$L.moveToPosition(-1)", cursorVarName)
-            relationCollectors.forEach { it.writeCollectionCode(scope) }
+            relationCollectors.forEach { it.writeFetchRelationCall(scope) }
         }
     }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
index 58bc4d6..d5b08e8 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
@@ -19,7 +19,7 @@
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.ext.AndroidTypeNames.CURSOR
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.L
 import androidx.room.ext.N
 import androidx.room.ext.RoomTypeNames
@@ -72,7 +72,7 @@
         MethodSpec.methodBuilder("convertRows").apply {
             addAnnotation(Override::class.java)
             addModifiers(Modifier.PROTECTED)
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
+            returns(ParameterizedTypeName.get(LIST.toJavaPoet(), itemTypeName))
             val cursorParam = ParameterSpec.builder(CURSOR.toJavaPoet(), "cursor")
                 .build()
             addParameter(cursorParam)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
index caada40..828d0db 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/transaction/binder/CoroutineTransactionMethodBinder.kt
@@ -78,7 +78,7 @@
             XCodeBlock.of(
                 scope.language,
                 "(%L) -> %L",
-                innerContinuationParamName, adapterScope.builder().build()
+                innerContinuationParamName, adapterScope.generate()
             )
         } else {
             Function1TypeSpec(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
index 1bbe2a1..9ae6f33 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
@@ -17,12 +17,11 @@
 package androidx.room.solver.types
 
 import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.solver.CodeGenScope
-import java.nio.ByteBuffer
 
 class ByteBufferColumnTypeAdapter constructor(out: XType) : ColumnTypeAdapter(
     out = out,
@@ -39,7 +38,7 @@
                 addStatement(
                     "%L = %T.wrap(%L.getBlob(%L))",
                     outVarName,
-                    ByteBuffer::class.asClassName(),
+                    CommonTypeNames.BYTE_BUFFER,
                     cursorVarName,
                     indexVarName
                 )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
index 2bb2c0c..a07e21c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
@@ -27,7 +27,7 @@
     cost = conv1.cost + conv2.cost
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             val conv1Output = conv1.convert(inputVarName, scope)
             conv2.convert(
                 inputVarName = conv1Output,
@@ -38,7 +38,7 @@
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
-        scope.builder().apply {
+        scope.builder.apply {
             val conv1Output = conv1.convert(
                 inputVarName = inputVarName,
                 scope = scope
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
index a62cf47..a5269bb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.apply
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.decapitalize
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.CustomTypeConverter
@@ -85,21 +86,41 @@
     }
 
     private fun providedTypeConverter(scope: CodeGenScope): XFunSpec {
-        val className = custom.className
-        val baseName = className.simpleNames.last().decapitalize(Locale.US)
+        val fieldTypeName = when (scope.language) {
+            CodeLanguage.JAVA -> custom.className
+            CodeLanguage.KOTLIN -> KotlinTypeNames.LAZY.parametrizedBy(custom.className)
+        }
+        val baseName = custom.className.simpleNames.last().decapitalize(Locale.US)
         val converterClassName = custom.className
         scope.writer.addRequiredTypeConverter(converterClassName)
         val converterField = scope.writer.getOrCreateProperty(
             object : TypeWriter.SharedPropertySpec(
-                baseName, custom.className
+                baseName, fieldTypeName
             ) {
-                override val isMutable = true
+                override val isMutable = scope.language == CodeLanguage.JAVA
 
                 override fun getUniqueKey(): String {
                     return "converter_${custom.className}"
                 }
 
                 override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
+                    // For Kotlin we'll rely on kotlin.Lazy while for Java we'll memoize the
+                    // provided converter in the getter.
+                    if (builder.language == CodeLanguage.KOTLIN) {
+                        builder.apply {
+                            initializer(
+                                XCodeBlock.builder(language).apply {
+                                    beginControlFlow("lazy")
+                                    addStatement(
+                                        "checkNotNull(%L.getTypeConverter(%L))",
+                                        DaoWriter.DB_PROPERTY_NAME,
+                                        XCodeBlock.ofJavaClassLiteral(language, custom.className)
+                                    )
+                                    endControlFlow()
+                                }.build()
+                            )
+                        }
+                    }
                 }
             }
         )
@@ -115,39 +136,40 @@
             ) {
                 val body = buildConvertFunctionBody(builder.language)
                 builder.apply(
-                    // Apply synchronized modifier for Java
+                    // Apply synchronized modifier for Java since function checks and sets the
+                    // converter in the shared field.
                     javaMethodBuilder = {
                         addModifiers(Modifier.SYNCHRONIZED)
-                        builder.addCode(body)
                     },
-                    // Use synchronized std-lib function for Kotlin
-                    kotlinFunctionBuilder = {
-                        beginControlFlow("return synchronized")
-                        builder.addCode(body)
-                        endControlFlow()
-                    }
+                    kotlinFunctionBuilder = { }
                 )
+                builder.addCode(body)
                 builder.returns(custom.className)
             }
 
-            private fun buildConvertFunctionBody(language: CodeLanguage): XCodeBlock {
-                return XCodeBlock.builder(language).apply {
-                    beginControlFlow("if (%N == null)", converterField)
-                    addStatement(
-                        "%N = %L.getTypeConverter(%L)",
-                        converterField,
-                        DaoWriter.DB_PROPERTY_NAME,
-                        XCodeBlock.ofJavaClassLiteral(language, custom.className)
-                    )
-                    endControlFlow()
-                    when (language) {
-                        CodeLanguage.JAVA ->
-                            addStatement("return %N", converterField)
-                        CodeLanguage.KOTLIN ->
-                            addStatement("return@synchronized %N", converterField)
+            private fun buildConvertFunctionBody(
+                language: CodeLanguage
+            ) = XCodeBlock.builder(language).apply {
+                // For Java we implement the memoization logic in the converter getter, meanwhile
+                // for Kotlin we rely on kotlin.Lazy so the getter just delegates to it.
+                when (language) {
+                    CodeLanguage.JAVA -> {
+                        beginControlFlow("if (%N == null)", converterField).apply {
+                            addStatement(
+                                "%N = %L.getTypeConverter(%L)",
+                                converterField,
+                                DaoWriter.DB_PROPERTY_NAME,
+                                XCodeBlock.ofJavaClassLiteral(language, custom.className)
+                            )
+                        }
+                        endControlFlow()
+                        addStatement("return %N", converterField)
                     }
-                }.build()
-            }
+                    CodeLanguage.KOTLIN -> {
+                        addStatement("return %N.value", converterField)
+                    }
+                }
+            }.build()
         }
         return scope.writer.getOrCreateFunction(funSpec)
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
index b127c87..aafefb5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
@@ -16,7 +16,6 @@
 
 package androidx.room.solver.types
 
-import androidx.room.ext.L
 import androidx.room.compiler.processing.XType
 import androidx.room.solver.CodeGenScope
 
@@ -32,8 +31,7 @@
     type, type
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder()
-            .addStatement("$L = $L", outputVarName, inputVarName)
+        scope.builder.addStatement("%L = %L", outputVarName, inputVarName)
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
index 288899f..3b30e79 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
@@ -16,22 +16,19 @@
 
 package androidx.room.solver.types
 
-import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
-import java.lang.IllegalStateException
 
 /**
  * A type converter that checks if the input is null and returns null instead of calling the
  * [delegate].
  */
 class NullSafeTypeConverter(
-    @VisibleForTesting
-    internal val delegate: TypeConverter
+    val delegate: TypeConverter
 ) : TypeConverter(
     from = delegate.from.makeNullable(),
     to = delegate.to.makeNullable(),
@@ -44,11 +41,13 @@
     }
 
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement("$L = null", outputVarName)
-            nextControlFlow("else")
-            delegate.convert(inputVarName, outputVarName, scope)
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addStatement("%L = null", outputVarName)
+            }
+            nextControlFlow("else").apply {
+                delegate.convert(inputVarName, outputVarName, scope)
+            }
             endControlFlow()
         }
     }
@@ -71,28 +70,48 @@
     }
 
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement(
-                "throw new $T($S)", IllegalStateException::class.java,
-                "Expected non-null ${from.typeName}, but it was null."
-            )
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addIllegalStateException()
+            }
             nextControlFlow("else").apply {
-                addStatement("$L = $L", outputVarName, inputVarName)
+                addStatement("%L = %L", outputVarName, inputVarName)
             }
             endControlFlow()
         }
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement(
-                "throw new $T($S)", IllegalStateException::class.java,
-                "Expected non-null ${from.typeName}, but it was null."
-            )
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addIllegalStateException()
+            }
             endControlFlow()
         }
         return inputVarName
     }
+
+    private fun XCodeBlock.Builder.addIllegalStateException() {
+        val message = "Expected non-null ${from.typeName}, but it was null."
+        when (language) {
+            CodeLanguage.JAVA -> {
+                addStatement(
+                    "throw %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        ILLEGAL_STATE_EXCEPTION,
+                        "%S",
+                        message
+                    )
+                )
+            }
+            CodeLanguage.KOTLIN -> {
+                addStatement("error(%S)", message)
+            }
+        }
+    }
+
+    companion object {
+        private val ILLEGAL_STATE_EXCEPTION = IllegalStateException::class.asClassName()
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
index dfda989..d8bb5d3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.types
 
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
@@ -68,7 +69,7 @@
 
     companion object {
         fun create(env: XProcessingEnv): List<StringColumnTypeAdapter> {
-            val stringType = env.requireType(CommonTypeNames.STRING)
+            val stringType = env.requireType(CommonTypeNames.STRING.toJavaPoet())
             return if (env.backend == XProcessingEnv.Backend.KSP) {
                 listOf(
                     StringColumnTypeAdapter(stringType.makeNonNullable()),
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
index 2f8ae17..d702c1a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
@@ -18,8 +18,6 @@
 
 import androidx.annotation.VisibleForTesting
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -52,8 +50,8 @@
         scope: CodeGenScope
     ): String {
         val outVarName = scope.getTmpVar()
-        scope.builder().apply {
-            addStatement("final $T $L", to.typeName, outVarName)
+        scope.builder.apply {
+            addLocalVariable(outVarName, to.asTypeName())
         }
         doConvert(
             inputVarName = inputVarName,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
index ad07028..2465b26 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
@@ -17,7 +17,6 @@
 package androidx.room.solver.types
 
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -34,9 +33,7 @@
     cost = Cost.UP_CAST
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            addStatement("$L = $L", outputVarName, inputVarName)
-        }
+        scope.builder.addStatement("%L = %L", outputVarName, inputVarName)
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
index 3e60540..35248a6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
@@ -32,7 +32,7 @@
     val schemaDiff: SchemaDiffResult,
     val isSpecProvided: Boolean,
 ) {
-    val specClassName = specElement?.className
+    val specClassName = specElement?.asClassName()
 
     fun getImplTypeName(databaseClassName: XClassName): XClassName {
         return XClassName.get(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
index 1d2c0a2..a5d810d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
@@ -37,7 +37,7 @@
     // the projection for the query
     val projection: List<String>
 ) {
-    val pojoTypeName by lazy { pojoType.typeName }
+    val pojoTypeName by lazy { pojoType.asTypeName() }
 
     fun createLoadAllSql(): String {
         val resultFields = projection.toSet()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
index 606cfda..219cb0d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -16,18 +16,22 @@
 
 package androidx.room.vo
 
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.processing.XType
+import androidx.room.BuiltInTypeConverters
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.XNullability
 import androidx.room.ext.CollectionTypeNames
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.parser.ParsedQuery
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.parser.SqlParser
 import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors.ISSUE_TRACKER_LINK
 import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
 import androidx.room.processor.ProcessorErrors.relationAffinityMismatch
 import androidx.room.processor.ProcessorErrors.relationJunctionChildAffinityMismatch
@@ -36,13 +40,10 @@
 import androidx.room.solver.query.parameter.QueryParameterAdapter
 import androidx.room.solver.query.result.RowAdapter
 import androidx.room.solver.query.result.SingleColumnRowAdapter
+import androidx.room.solver.types.CursorValueReader
 import androidx.room.verifier.DatabaseVerificationErrors
 import androidx.room.writer.QueryWriter
 import androidx.room.writer.RelationCollectorFunctionWriter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
 import java.nio.ByteBuffer
 import java.util.Locale
 
@@ -51,14 +52,27 @@
  */
 data class RelationCollector(
     val relation: Relation,
+    // affinity between relation fields
     val affinity: SQLTypeAffinity,
-    val mapTypeName: ParameterizedTypeName,
-    val keyTypeName: TypeName,
-    val relationTypeName: TypeName,
+    // concrete map type name to store relationship
+    val mapTypeName: XTypeName,
+    // map key type name, not the same as the parent or entity field type
+    val keyTypeName: XTypeName,
+    // map value type name, it is assignable to the @Relation field
+    val relationTypeName: XTypeName,
+    // query writer for the relating entity query
     val queryWriter: QueryWriter,
+    // key reader for the parent field
+    val parentKeyColumnReader: CursorValueReader,
+    // key reader for the entity field
+    val entityKeyColumnReader: CursorValueReader,
+    // adapter for the relating pojo
     val rowAdapter: RowAdapter,
+    // parsed relating entity query
     val loadAllQuery: ParsedQuery,
-    val relationTypeIsCollection: Boolean
+    // true if `relationTypeName` is a Collection, when it is `relationTypeName` is always non null.
+    val relationTypeIsCollection: Boolean,
+    val javaLambdaSyntaxAvailable: Boolean
 ) {
     // variable name of map containing keys to relation collections, set when writing the code
     // generator in writeInitCode
@@ -68,8 +82,12 @@
         varName = scope.getTmpVar(
             "_collection${relation.field.getPath().stripNonJava().capitalize(Locale.US)}"
         )
-        scope.builder().apply {
-            addStatement("final $T $L = new $T()", mapTypeName, varName, mapTypeName)
+        scope.builder.apply {
+            addLocalVariable(
+                name = varName,
+                typeName = mapTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(language, mapTypeName)
+            )
         }
     }
 
@@ -79,26 +97,25 @@
         fieldsWithIndices: List<FieldWithIndex>,
         scope: CodeGenScope
     ) {
-        val indexVar = fieldsWithIndices.firstOrNull {
-            it.field === relation.parentField
-        }?.indexVar
-        scope.builder().apply {
-            readKey(cursorVarName, indexVar, scope) { tmpVar ->
+        val indexVar = fieldsWithIndices.firstOrNull { it.field === relation.parentField }?.indexVar
+        checkNotNull(indexVar) {
+            "Expected an index var for a column named '${relation.parentField.columnName}' to " +
+                "query the '${relation.pojoType}' @Relation but didn't. Please file a bug at " +
+                ISSUE_TRACKER_LINK
+        }
+        scope.builder.apply {
+            readKey(cursorVarName, indexVar, parentKeyColumnReader, scope) { tmpVar ->
+                // for relation collection put an empty collections in the map, otherwise put nulls
                 if (relationTypeIsCollection) {
-                    val tmpCollectionVar = scope.getTmpVar(
-                        "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}Collection"
-                    )
-                    addStatement(
-                        "$T $L = $L.get($L)", relationTypeName, tmpCollectionVar,
-                        varName, tmpVar
-                    )
-                    beginControlFlow("if ($L == null)", tmpCollectionVar).apply {
-                        addStatement("$L = new $T()", tmpCollectionVar, relationTypeName)
-                        addStatement("$L.put($L, $L)", varName, tmpVar, tmpCollectionVar)
+                    beginControlFlow("if (!%L.containsKey(%L))", varName, tmpVar).apply {
+                        addStatement(
+                            "%L.put(%L, %L)",
+                            varName, tmpVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                        )
                     }
                     endControlFlow()
                 } else {
-                    addStatement("$L.put($L, null)", varName, tmpVar)
+                    addStatement("%L.put(%L, null)", varName, tmpVar)
                 }
             }
         }
@@ -110,77 +127,116 @@
         fieldsWithIndices: List<FieldWithIndex>,
         scope: CodeGenScope
     ): Pair<String, Field> {
-        val indexVar = fieldsWithIndices.firstOrNull {
-            it.field === relation.parentField
-        }?.indexVar
-        val tmpvarNameSuffix = if (relationTypeIsCollection) "Collection" else ""
+        val indexVar = fieldsWithIndices.firstOrNull { it.field === relation.parentField }?.indexVar
+        checkNotNull(indexVar) {
+            "Expected an index var for a column named '${relation.parentField.columnName}' to " +
+                "query the '${relation.pojoType}' @Relation but didn't. Please file a bug at " +
+                ISSUE_TRACKER_LINK
+        }
+        val tmpVarNameSuffix = if (relationTypeIsCollection) "Collection" else ""
         val tmpRelationVar = scope.getTmpVar(
-            "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}$tmpvarNameSuffix"
+            "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}$tmpVarNameSuffix"
         )
-        scope.builder().apply {
-            addStatement("$T $L = null", relationTypeName, tmpRelationVar)
-            readKey(cursorVarName, indexVar, scope) { tmpVar ->
-                addStatement("$L = $L.get($L)", tmpRelationVar, varName, tmpVar)
-            }
-            if (relationTypeIsCollection) {
-                beginControlFlow("if ($L == null)", tmpRelationVar).apply {
-                    addStatement("$L = new $T()", tmpRelationVar, relationTypeName)
+        scope.builder.apply {
+            addLocalVariable(
+                name = tmpRelationVar,
+                typeName = relationTypeName
+            )
+            readKey(
+                cursorVarName = cursorVarName,
+                indexVar = indexVar,
+                keyReader = parentKeyColumnReader,
+                scope = scope,
+                onKeyReady = { tmpKeyVar ->
+                    if (relationTypeIsCollection) {
+                        // For Kotlin use getValue() as get() return a nullable value, when the
+                        // relation is a collection the map is pre-filled with empty collection
+                        // values for all keys, so this is safe. Special case for LongSParseArray
+                        // since it does not have a getValue() from Kotlin.
+                        val usingLongSparseArray =
+                            mapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+                        when (language) {
+                            CodeLanguage.JAVA -> addStatement(
+                                "%L = %L.get(%L)",
+                                tmpRelationVar, varName, tmpKeyVar
+                            )
+                            CodeLanguage.KOTLIN -> if (usingLongSparseArray) {
+                                addStatement(
+                                    "%L = checkNotNull(%L.get(%L))",
+                                    tmpRelationVar, varName, tmpKeyVar
+                                )
+                            } else {
+                                addStatement(
+                                    "%L = %L.getValue(%L)",
+                                    tmpRelationVar, varName, tmpKeyVar
+                                )
+                            }
+                        }
+                    } else {
+                        addStatement("%L = %L.get(%L)", tmpRelationVar, varName, tmpKeyVar)
+                        if (language == CodeLanguage.KOTLIN && relation.field.nonNull) {
+                            beginControlFlow("if (%L == null)", tmpRelationVar)
+                            // TODO(b/249984504): Generate / output a better message.
+                            addStatement("error(%S)", "Missing relationship item.")
+                            endControlFlow()
+                        }
+                    }
+                },
+                onKeyUnavailable = {
+                    if (relationTypeIsCollection) {
+                        addStatement(
+                            "%L = %L",
+                            tmpRelationVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                        )
+                    } else {
+                        addStatement("%L = null", tmpRelationVar)
+                    }
                 }
-                endControlFlow()
-            }
+            )
         }
         return tmpRelationVar to relation.field
     }
 
-    fun writeCollectionCode(scope: CodeGenScope) {
+    // called to write the invocation to the fetch relationship method
+    fun writeFetchRelationCall(scope: CodeGenScope) {
         val method = scope.writer
             .getOrCreateFunction(RelationCollectorFunctionWriter(this))
-        scope.builder().apply {
-            addStatement("$L($L)", method.name, varName)
-        }
+        scope.builder.addStatement("%L(%L)", method.name, varName)
     }
 
+    // called to read key and call `onKeyReady` to write code once it is successfully read
     fun readKey(
         cursorVarName: String,
-        indexVar: String?,
+        indexVar: String,
+        keyReader: CursorValueReader,
         scope: CodeGenScope,
-        postRead: CodeBlock.Builder.(String) -> Unit
+        onKeyReady: XCodeBlock.Builder.(String) -> Unit
     ) {
-        val cursorGetter = when (affinity) {
-            SQLTypeAffinity.INTEGER -> "getLong"
-            SQLTypeAffinity.REAL -> "getDouble"
-            SQLTypeAffinity.TEXT -> "getString"
-            SQLTypeAffinity.BLOB -> "getBlob"
-            else -> {
-                "getString"
-            }
-        }
-        scope.builder().apply {
-            val keyType = if (mapTypeName.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY) {
-                keyTypeName.unbox()
-            } else {
-                keyTypeName
-            }
+        readKey(cursorVarName, indexVar, keyReader, scope, onKeyReady, null)
+    }
+
+    // called to read key and call `onKeyReady` to write code once it is successfully read and
+    // `onKeyUnavailable` if the key is unavailable (missing column due to bad projection).
+    private fun readKey(
+        cursorVarName: String,
+        indexVar: String,
+        keyReader: CursorValueReader,
+        scope: CodeGenScope,
+        onKeyReady: XCodeBlock.Builder.(String) -> Unit,
+        onKeyUnavailable: (XCodeBlock.Builder.() -> Unit)?,
+    ) {
+        scope.builder.apply {
             val tmpVar = scope.getTmpVar("_tmpKey")
-            fun addKeyReadStatement() {
-                if (keyTypeName == TypeName.get(ByteBuffer::class.java)) {
-                    addStatement(
-                        "final $T $L = $T.wrap($L.$L($L))",
-                        keyType, tmpVar, keyTypeName, cursorVarName, cursorGetter, indexVar
-                    )
-                } else {
-                    addStatement(
-                        "final $T $L = $L.$L($L)",
-                        keyType, tmpVar, cursorVarName, cursorGetter, indexVar
-                    )
-                }
-                this.postRead(tmpVar)
-            }
-            if (relation.parentField.nonNull) {
-                addKeyReadStatement()
+            addLocalVariable(tmpVar, keyReader.typeMirror().asTypeName())
+            keyReader.readFromCursor(tmpVar, cursorVarName, indexVar, scope)
+            if (keyReader.typeMirror().nullability == XNullability.NONNULL) {
+                onKeyReady(tmpVar)
             } else {
-                beginControlFlow("if (!$L.isNull($L))", cursorVarName, indexVar).apply {
-                    addKeyReadStatement()
+                beginControlFlow("if (%L != null)", tmpVar)
+                onKeyReady(tmpVar)
+                if (onKeyUnavailable != null) {
+                    nextControlFlow("else")
+                    onKeyUnavailable()
                 }
                 endControlFlow()
             }
@@ -189,7 +245,7 @@
 
     /**
      * Adapter for binding a LongSparseArray keys into query arguments. This special adapter is only
-     * used for binding the relationship query who's keys have INTEGER affinity.
+     * used for binding the relationship query whose keys have INTEGER affinity.
      */
     private class LongSparseArrayKeyQueryParameterAdapter : QueryParameterAdapter(true) {
         override fun bindToStmt(
@@ -198,16 +254,27 @@
             startIndexVarName: String,
             scope: CodeGenScope
         ) {
-            scope.builder().apply {
+            scope.builder.apply {
                 val itrIndexVar = "i"
                 val itrItemVar = scope.getTmpVar("_item")
-                beginControlFlow(
-                    "for (int $L = 0; $L < $L.size(); i++)",
-                    itrIndexVar, itrIndexVar, inputVarName
-                ).apply {
-                    addStatement("long $L = $L.keyAt($L)", itrItemVar, inputVarName, itrIndexVar)
-                    addStatement("$L.bindLong($L, $L)", stmtVarName, startIndexVarName, itrItemVar)
-                    addStatement("$L ++", startIndexVarName)
+                when (language) {
+                    CodeLanguage.JAVA -> beginControlFlow(
+                        "for (int %L = 0; %L < %L.size(); i++)",
+                        itrIndexVar, itrIndexVar, inputVarName
+                    )
+                    CodeLanguage.KOTLIN -> beginControlFlow(
+                        "for (%L in 0 until %L.size())",
+                        itrIndexVar, inputVarName
+                    )
+                }.apply {
+                    addLocalVal(
+                        itrItemVar,
+                        XTypeName.PRIMITIVE_LONG,
+                        "%L.keyAt(%L)",
+                        inputVarName, itrIndexVar
+                    )
+                    addStatement("%L.bindLong(%L, %L)", stmtVarName, startIndexVarName, itrItemVar)
+                    addStatement("%L++", startIndexVarName)
                 }
                 endControlFlow()
             }
@@ -218,9 +285,11 @@
             outputVarName: String,
             scope: CodeGenScope
         ) {
-            scope.builder().addStatement(
-                "final $T $L = $L.size()",
-                TypeName.INT, outputVarName, inputVarName
+            scope.builder.addLocalVal(
+                outputVarName,
+                XTypeName.PRIMITIVE_INT,
+                "%L.size()",
+                inputVarName
             )
         }
     }
@@ -237,12 +306,16 @@
             return relations.map { relation ->
                 val context = baseContext.fork(
                     element = relation.field.element,
-                    forceSuppressedWarnings = setOf(Warning.CURSOR_MISMATCH)
+                    forceSuppressedWarnings = setOf(Warning.CURSOR_MISMATCH),
+                    forceBuiltInConverters = BuiltInConverterFlags.DEFAULT.copy(
+                        byteBuffer = BuiltInTypeConverters.State.ENABLED
+                    )
                 )
                 val affinity = affinityFor(context, relation)
-                val keyType = keyTypeFor(context, affinity)
-                val (relationTypeName, isRelationCollection) = relationTypeFor(relation)
-                val tmpMapType = temporaryMapTypeFor(context, affinity, keyType, relationTypeName)
+                val keyTypeName = keyTypeFor(context, affinity)
+                val (relationTypeName, isRelationCollection) = relationTypeFor(context, relation)
+                val tmpMapTypeName =
+                    temporaryMapTypeFor(context, affinity, keyTypeName, relationTypeName)
 
                 val loadAllQuery = relation.createLoadAllSql()
                 val parsedQuery = SqlParser.parse(loadAllQuery)
@@ -263,10 +336,10 @@
                 val resultInfo = parsedQuery.resultInfo
 
                 val usingLongSparseArray =
-                    tmpMapType.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY
+                    tmpMapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
                 val queryParam = if (usingLongSparseArray) {
                     val longSparseArrayElement = context.processingEnv
-                        .requireTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY)
+                        .requireTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName)
                     QueryParameter(
                         name = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
                         sqlName = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
@@ -274,8 +347,8 @@
                         queryParamAdapter = LONG_SPARSE_ARRAY_KEY_QUERY_PARAM_ADAPTER
                     )
                 } else {
-                    val keyTypeMirror = keyTypeMirrorFor(context, affinity)
-                    val set = context.processingEnv.requireTypeElement("java.util.Set")
+                    val keyTypeMirror = context.processingEnv.requireType(keyTypeName)
+                    val set = checkNotNull(context.COMMON_TYPES.SET.typeElement)
                     val keySet = context.processingEnv.getDeclaredType(set, keyTypeMirror)
                     QueryParameter(
                         name = RelationCollectorFunctionWriter.KEY_SET_VARIABLE,
@@ -299,11 +372,29 @@
                     query = parsedQuery
                 )
 
+                val parentKeyColumnReader = context.typeAdapterStore.findCursorValueReader(
+                    output = context.processingEnv.requireType(keyTypeName).let {
+                        if (!relation.parentField.nonNull) it.makeNullable() else it
+                    },
+                    affinity = affinity
+                )
+                val entityKeyColumnReader = context.typeAdapterStore.findCursorValueReader(
+                    output = context.processingEnv.requireType(keyTypeName).let { keyType ->
+                        if (!relation.entityField.nonNull) keyType.makeNullable() else keyType
+                    },
+                    affinity = affinity
+                )
+                // We should always find a readers since key types all have built in converters
+                check(parentKeyColumnReader != null && entityKeyColumnReader != null) {
+                    "Missing one of the relation key value reader for type $keyTypeName"
+                }
+
                 // row adapter that matches full response
                 fun getDefaultRowAdapter(): RowAdapter? {
                     return context.typeAdapterStore.findRowAdapter(relation.pojoType, parsedQuery)
                 }
-                val rowAdapter = if (relation.projection.size == 1 && resultInfo != null &&
+                val rowAdapter = if (
+                    relation.projection.size == 1 && resultInfo != null &&
                     (resultInfo.columns.size == 1 || resultInfo.columns.size == 2)
                 ) {
                     // check for a column adapter first
@@ -329,13 +420,16 @@
                     RelationCollector(
                         relation = relation,
                         affinity = affinity,
-                        mapTypeName = tmpMapType,
-                        keyTypeName = keyType,
+                        mapTypeName = tmpMapTypeName,
+                        keyTypeName = keyTypeName,
                         relationTypeName = relationTypeName,
                         queryWriter = queryWriter,
+                        parentKeyColumnReader = parentKeyColumnReader,
+                        entityKeyColumnReader = entityKeyColumnReader,
                         rowAdapter = rowAdapter,
                         loadAllQuery = parsedQuery,
-                        relationTypeIsCollection = isRelationCollection
+                        relationTypeIsCollection = isRelationCollection,
+                        javaLambdaSyntaxAvailable = context.processingEnv.jvmVersion >= 8
                     )
                 }
             }.filterNotNull()
@@ -399,87 +493,65 @@
         }
 
         // Gets the resulting relation type name. (i.e. the Pojo's @Relation field type name.)
-        private fun relationTypeFor(relation: Relation) =
-            if (relation.field.typeName.toJavaPoet() is ParameterizedTypeName) {
-                val paramType = relation.field.typeName.toJavaPoet() as ParameterizedTypeName
-                val paramTypeName = if (paramType.rawType == CommonTypeNames.LIST) {
-                    ParameterizedTypeName.get(
-                        ClassName.get(ArrayList::class.java),
-                        relation.pojoTypeName
-                    )
-                } else if (paramType.rawType == CommonTypeNames.SET) {
-                    ParameterizedTypeName.get(
-                        ClassName.get(HashSet::class.java),
-                        relation.pojoTypeName
-                    )
-                } else {
-                    ParameterizedTypeName.get(
-                        ClassName.get(ArrayList::class.java),
-                        relation.pojoTypeName
-                    )
-                }
+        private fun relationTypeFor(
+            context: Context,
+            relation: Relation
+        ) = relation.field.type.let { fieldType ->
+            if (fieldType.typeArguments.isNotEmpty()) {
+                val rawType = fieldType.rawType
+                val paramTypeName =
+                    if (context.COMMON_TYPES.LIST.rawType.isAssignableFrom(rawType)) {
+                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                    } else if (context.COMMON_TYPES.SET.rawType.isAssignableFrom(rawType)) {
+                        CommonTypeNames.HASH_SET.parametrizedBy(relation.pojoTypeName)
+                    } else {
+                        // Default to ArrayList and see how things go...
+                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                    }
                 paramTypeName to true
             } else {
-                relation.pojoTypeName to false
+                relation.pojoTypeName.copy(nullable = true) to false
             }
+        }
 
         // Gets the type name of the temporary key map.
         private fun temporaryMapTypeFor(
             context: Context,
             affinity: SQLTypeAffinity,
-            keyType: TypeName,
-            relationTypeName: TypeName
-        ): ParameterizedTypeName {
+            keyTypeName: XTypeName,
+            valueTypeName: XTypeName,
+        ): XTypeName {
             val canUseLongSparseArray = context.processingEnv
-                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY) != null
+                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
             val canUseArrayMap = context.processingEnv
-                .findTypeElement(CollectionTypeNames.ARRAY_MAP) != null
+                .findTypeElement(CollectionTypeNames.ARRAY_MAP.canonicalName) != null
             return when {
-                canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER -> {
-                    ParameterizedTypeName.get(
-                        CollectionTypeNames.LONG_SPARSE_ARRAY,
-                        relationTypeName
-                    )
-                }
-                canUseArrayMap -> {
-                    ParameterizedTypeName.get(
-                        CollectionTypeNames.ARRAY_MAP,
-                        keyType, relationTypeName
-                    )
-                }
-                else -> {
-                    ParameterizedTypeName.get(
-                        ClassName.get(java.util.HashMap::class.java),
-                        keyType, relationTypeName
-                    )
-                }
-            }
-        }
-
-        // Gets the type mirror of the relationship key.
-        private fun keyTypeMirrorFor(context: Context, affinity: SQLTypeAffinity): XType {
-            val processingEnv = context.processingEnv
-            return when (affinity) {
-                SQLTypeAffinity.INTEGER -> processingEnv.requireType("java.lang.Long")
-                SQLTypeAffinity.REAL -> processingEnv.requireType("java.lang.Double")
-                SQLTypeAffinity.TEXT -> context.COMMON_TYPES.STRING
-                SQLTypeAffinity.BLOB -> processingEnv.requireType("java.nio.ByteBuffer")
-                else -> {
-                    context.COMMON_TYPES.STRING
-                }
+                canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER ->
+                    CollectionTypeNames.LONG_SPARSE_ARRAY.parametrizedBy(valueTypeName)
+                canUseArrayMap ->
+                    CollectionTypeNames.ARRAY_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                else ->
+                    CommonTypeNames.HASH_MAP.parametrizedBy(keyTypeName, valueTypeName)
             }
         }
 
         // Gets the type name of the relationship key.
-        private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): TypeName {
+        private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): XTypeName {
+            val canUseLongSparseArray = context.processingEnv
+                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
             return when (affinity) {
-                SQLTypeAffinity.INTEGER -> TypeName.LONG.box()
-                SQLTypeAffinity.REAL -> TypeName.DOUBLE.box()
-                SQLTypeAffinity.TEXT -> TypeName.get(String::class.java)
-                SQLTypeAffinity.BLOB -> TypeName.get(ByteBuffer::class.java)
+                SQLTypeAffinity.INTEGER ->
+                    if (canUseLongSparseArray) {
+                        XTypeName.PRIMITIVE_LONG
+                    } else {
+                        Long::class.asClassName()
+                    }
+                SQLTypeAffinity.REAL -> Double::class.asClassName()
+                SQLTypeAffinity.TEXT -> String::class.asClassName()
+                SQLTypeAffinity.BLOB -> ByteBuffer::class.asClassName()
                 else -> {
-                    // no affinity select from type
-                    context.COMMON_TYPES.STRING.typeName
+                    // no affinity default to String
+                    String::class.asClassName()
                 }
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 290af1e..a302a22 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -24,7 +24,6 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addProperty
-import androidx.room.compiler.codegen.toXClassName
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.SupportDbTypeNames
@@ -53,17 +52,17 @@
         )
         builder.apply {
             addOriginatingElement(dbElement)
-            superclass(RoomTypeNames.MIGRATION.toXClassName())
+            superclass(RoomTypeNames.MIGRATION)
 
             if (autoMigration.specClassName != null) {
                 builder.addProperty(
                     name = "callback",
-                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC.toXClassName(),
+                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC,
                     visibility = VisibilityModifier.PRIVATE,
                     initExpr = if (!autoMigration.isSpecProvided) {
                         XCodeBlock.ofNewInstance(
                             codeLanguage,
-                            autoMigration.specClassName.toXClassName()
+                            autoMigration.specClassName
                         )
                     } else {
                         null
@@ -89,7 +88,7 @@
             )
             if (autoMigration.isSpecProvided) {
                 addParameter(
-                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC.toXClassName(),
+                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC,
                     name = "callback",
                 )
                 addStatement("this.callback = callback")
@@ -104,15 +103,15 @@
             visibility = VisibilityModifier.PUBLIC,
             isOverride = true,
         ).apply {
-                addParameter(
-                    typeName = SupportDbTypeNames.DB.toXClassName(),
-                    name = "database",
-                )
-                addMigrationStatements(this)
-                if (autoMigration.specClassName != null) {
-                    addStatement("callback.onPostMigrate(database)")
-                }
+            addParameter(
+                typeName = SupportDbTypeNames.DB,
+                name = "database",
+            )
+            addMigrationStatements(this)
+            if (autoMigration.specClassName != null) {
+                addStatement("callback.onPostMigrate(database)")
             }
+        }
         return migrateFunctionBuilder.build()
     }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
index 4d378b7..ea0d93b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DaoWriter.kt
@@ -134,7 +134,7 @@
             }
             addProperty(dbProperty)
 
-            addFunction(
+            setPrimaryConstructor(
                 createConstructor(
                     shortcutMethods,
                     dao.constructorParamType != null
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
index fe49b03..f1d09cf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
@@ -16,43 +16,31 @@
 
 package androidx.room.writer
 
-import androidx.annotation.NonNull
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XPropertySpec.Companion.apply
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
-import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.apply
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.processing.MethodSpecHelper
-import androidx.room.compiler.processing.addOriginatingElement
+import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
 import androidx.room.ext.SupportDbTypeNames
-import androidx.room.ext.T
-import androidx.room.ext.W
 import androidx.room.ext.decapitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.DaoMethod
 import androidx.room.vo.Database
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeSpec
-import com.squareup.javapoet.WildcardTypeName
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
 import java.util.Locale
-import javax.lang.model.element.Modifier.FINAL
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PROTECTED
-import javax.lang.model.element.Modifier.PUBLIC
-import javax.lang.model.element.Modifier.VOLATILE
+import javax.lang.model.element.Modifier
 
 /**
  * Writes implementation of classes that were annotated with @Database.
@@ -62,189 +50,171 @@
     codeLanguage: CodeLanguage
 ) : TypeWriter(codeLanguage) {
     override fun createTypeSpecBuilder(): XTypeSpec.Builder {
-        val builder = XTypeSpec.classBuilder(codeLanguage, database.implTypeName)
-        builder.apply(
-            javaTypeBuilder = {
-                addOriginatingElement(database.element)
-                addModifiers(PUBLIC)
-                addModifiers(FINAL)
-                superclass(database.typeName.toJavaPoet())
-                addMethod(createCreateOpenHelper())
-                addMethod(createCreateInvalidationTracker())
-                addMethod(createClearAllTables())
-                addMethod(createCreateTypeConvertersMap())
-                addMethod(createCreateAutoMigrationSpecsSet())
-                addMethod(getAutoMigrations())
-                addDaoImpls(this)
-            },
-            kotlinTypeBuilder = {
-                TODO("Kotlin codegen not yet implemented!")
-            }
-        )
-        return builder
+        return XTypeSpec.classBuilder(codeLanguage, database.implTypeName).apply {
+            addOriginatingElement(database.element)
+            superclass(database.typeName)
+            setVisibility(VisibilityModifier.PUBLIC)
+            addFunction(createCreateOpenHelper())
+            addFunction(createCreateInvalidationTracker())
+            addFunction(createClearAllTables())
+            addFunction(createCreateTypeConvertersMap())
+            addFunction(createCreateAutoMigrationSpecsSet())
+            addFunction(getAutoMigrations())
+            addDaoImpls(this)
+        }
     }
 
-    private fun createCreateTypeConvertersMap(): MethodSpec {
+    private fun createCreateTypeConvertersMap(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("getRequiredTypeConverters").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PROTECTED)
-            returns(
-                ParameterizedTypeName.get(
-                    CommonTypeNames.MAP,
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(Object::class.java)
-                    ),
-                    ParameterizedTypeName.get(
-                        CommonTypeNames.LIST,
-                        ParameterizedTypeName.get(
-                            ClassName.get(Class::class.java),
-                            WildcardTypeName.subtypeOf(Object::class.java)
-                        )
-                    )
-                )
-            )
+        val classOfAnyTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(Any::class.asClassName())
+        )
+        val typeConvertersTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
+            classOfAnyTypeName,
+            CommonTypeNames.LIST.parametrizedBy(classOfAnyTypeName)
+        )
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val typeConvertersVar = scope.getTmpVar("_typeConvertersMap")
-            val typeConvertersTypeName = ParameterizedTypeName.get(
-                ClassName.get(HashMap::class.java),
-                ParameterizedTypeName.get(
-                    ClassName.get(Class::class.java),
-                    WildcardTypeName.subtypeOf(Object::class.java)
-                ),
-                ParameterizedTypeName.get(
-                    ClassName.get(List::class.java),
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(Object::class.java)
-                    )
-                )
-            )
-            addStatement(
-                "final $T $L = new $T()",
-                typeConvertersTypeName,
-                typeConvertersVar,
-                typeConvertersTypeName
+            addLocalVariable(
+                name = typeConvertersVar,
+                typeName = typeConvertersTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, typeConvertersTypeName)
             )
             database.daoMethods.forEach {
                 addStatement(
-                    "$L.put($T.class, $T.$L())",
+                    "%L.put(%L, %T.%L())",
                     typeConvertersVar,
-                    it.dao.typeName.toJavaPoet(),
-                    it.dao.implTypeName.toJavaPoet(),
+                    XCodeBlock.ofJavaClassLiteral(codeLanguage, it.dao.typeName),
+                    it.dao.implTypeName,
                     DaoWriter.GET_LIST_OF_TYPE_CONVERTERS_METHOD
                 )
             }
-            addStatement("return $L", typeConvertersVar)
+            addStatement("return %L", typeConvertersVar)
         }.build()
-    }
-
-    private fun createCreateAutoMigrationSpecsSet(): MethodSpec {
-        val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("getRequiredAutoMigrationSpecs").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PUBLIC)
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getRequiredTypeConverters",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true
+        ).apply {
             returns(
-                ParameterizedTypeName.get(
-                    CommonTypeNames.SET,
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                    )
+                CommonTypeNames.MAP.parametrizedBy(
+                    classOfAnyTypeName,
+                    CommonTypeNames.LIST.parametrizedBy(classOfAnyTypeName)
                 )
             )
-            val autoMigrationSpecsVar = scope.getTmpVar("_autoMigrationSpecsSet")
-            val autoMigrationSpecsTypeName = ParameterizedTypeName.get(
-                ClassName.get(HashSet::class.java),
-                ParameterizedTypeName.get(
-                    ClassName.get(Class::class.java),
-                    WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                )
-            )
-            addStatement(
-                "final $T $L = new $T()",
-                autoMigrationSpecsTypeName,
-                autoMigrationSpecsVar,
-                autoMigrationSpecsTypeName
-            )
-            database.autoMigrations.map { autoMigrationResult ->
-                if (autoMigrationResult.isSpecProvided) {
-                    addStatement(
-                        "$L.add($T.class)",
-                        autoMigrationSpecsVar,
-                        autoMigrationResult.specClassName
-                    )
-                }
-            }
-            addStatement("return $L", autoMigrationSpecsVar)
+            addCode(body)
         }.build()
     }
 
-    private fun createClearAllTables(): MethodSpec {
+    private fun createCreateAutoMigrationSpecsSet(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("clearAllTables").apply {
+        val classOfAutoMigrationSpecTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(RoomTypeNames.AUTO_MIGRATION_SPEC)
+        )
+        val autoMigrationSpecsTypeName =
+            CommonTypeNames.HASH_SET.parametrizedBy(classOfAutoMigrationSpecTypeName)
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            val autoMigrationSpecsVar = scope.getTmpVar("_autoMigrationSpecsSet")
+            addLocalVariable(
+                name = autoMigrationSpecsVar,
+                typeName = autoMigrationSpecsTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, autoMigrationSpecsTypeName)
+            )
+            database.autoMigrations.filter { it.isSpecProvided }.map { autoMigration ->
+                val specClassName = checkNotNull(autoMigration.specClassName)
+                addStatement(
+                    "%L.add(%L)",
+                    autoMigrationSpecsVar,
+                    XCodeBlock.ofJavaClassLiteral(codeLanguage, specClassName)
+                )
+            }
+            addStatement("return %L", autoMigrationSpecsVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getRequiredAutoMigrationSpecs",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true,
+        ).apply {
+            returns(CommonTypeNames.SET.parametrizedBy(classOfAutoMigrationSpecTypeName))
+            addCode(body)
+        }.build()
+    }
+
+    private fun createClearAllTables(): XFunSpec {
+        val scope = CodeGenScope(this)
+        val body = XCodeBlock.builder(codeLanguage).apply {
             addStatement("super.assertNotMainThread()")
             val dbVar = scope.getTmpVar("_db")
-            addStatement(
-                "final $T $L = super.getOpenHelper().getWritableDatabase()",
-                SupportDbTypeNames.DB, dbVar
+            addLocalVal(
+                dbVar,
+                SupportDbTypeNames.DB,
+                when (language) {
+                    CodeLanguage.JAVA -> "super.getOpenHelper().getWritableDatabase()"
+                    CodeLanguage.KOTLIN -> "super.openHelper.writableDatabase"
+                }
             )
             val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
             if (database.enableForeignKeys) {
-                addStatement(
-                    "boolean $L = $L.VERSION.SDK_INT >= $L.VERSION_CODES.LOLLIPOP",
-                    deferVar, AndroidTypeNames.BUILD, AndroidTypeNames.BUILD
+                addLocalVal(
+                    deferVar,
+                    XTypeName.PRIMITIVE_BOOLEAN,
+                    "%L.VERSION.SDK_INT >= %L.VERSION_CODES.LOLLIPOP",
+                    AndroidTypeNames.BUILD,
+                    AndroidTypeNames.BUILD
                 )
             }
-            addAnnotation(Override::class.java)
-            addModifiers(PUBLIC)
-            returns(TypeName.VOID)
             beginControlFlow("try").apply {
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if (!$L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = FALSE")
+                    beginControlFlow("if (!%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = FALSE")
                     }
                     endControlFlow()
                 }
                 addStatement("super.beginTransaction()")
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if ($L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
+                    beginControlFlow("if (%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
                     }
                     endControlFlow()
                 }
                 database.entities.sortedWith(EntityDeleteComparator()).forEach {
-                    addStatement("$L.execSQL($S)", dbVar, "DELETE FROM `${it.tableName}`")
+                    addStatement("%L.execSQL(%S)", dbVar, "DELETE FROM `${it.tableName}`")
                 }
                 addStatement("super.setTransactionSuccessful()")
             }
             nextControlFlow("finally").apply {
                 addStatement("super.endTransaction()")
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if (!$L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = TRUE")
+                    beginControlFlow("if (!%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = TRUE")
                     }
                     endControlFlow()
                 }
-                addStatement("$L.query($S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
-                beginControlFlow("if (!$L.inTransaction())", dbVar).apply {
-                    addStatement("$L.execSQL($S)", dbVar, "VACUUM")
+                addStatement("%L.query(%S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
+                beginControlFlow("if (!%L.inTransaction())", dbVar).apply {
+                    addStatement("%L.execSQL(%S)", dbVar, "VACUUM")
                 }
                 endControlFlow()
             }
             endControlFlow()
         }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "clearAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addCode(body)
+        }.build()
     }
 
-    private fun createCreateInvalidationTracker(): MethodSpec {
+    private fun createCreateInvalidationTracker(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("createInvalidationTracker").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PROTECTED)
-            returns(RoomTypeNames.INVALIDATION_TRACKER)
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val shadowTablesVar = "_shadowTablesMap"
-            val shadowTablesTypeName = ParameterizedTypeName.get(
-                HashMap::class.typeName,
+            val shadowTablesTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
                 CommonTypeNames.STRING, CommonTypeNames.STRING
             )
             val tableNames = database.entities.joinToString(",") {
@@ -255,154 +225,266 @@
             }.map {
                 it.tableName to it.shadowTableName
             }
-            addStatement(
-                "final $T $L = new $T($L)", shadowTablesTypeName, shadowTablesVar,
-                shadowTablesTypeName, shadowTableNames.size
-            )
-            shadowTableNames.forEach { (tableName, shadowTableName) ->
-                addStatement("$L.put($S, $S)", shadowTablesVar, tableName, shadowTableName)
-            }
-            val viewTablesVar = scope.getTmpVar("_viewTables")
-            val tablesType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                CommonTypeNames.STRING
-            )
-            val viewTablesType = ParameterizedTypeName.get(
-                HashMap::class.typeName,
-                CommonTypeNames.STRING,
-                ParameterizedTypeName.get(
-                    CommonTypeNames.SET,
-                    CommonTypeNames.STRING
+            addLocalVariable(
+                name = shadowTablesVar,
+                typeName = shadowTablesTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    shadowTablesTypeName,
+                    "%L",
+                    shadowTableNames.size
                 )
             )
-            addStatement(
-                "$T $L = new $T($L)", viewTablesType, viewTablesVar, viewTablesType,
-                database.views.size
+            shadowTableNames.forEach { (tableName, shadowTableName) ->
+                addStatement("%L.put(%S, %S)", shadowTablesVar, tableName, shadowTableName)
+            }
+            val viewTablesVar = scope.getTmpVar("_viewTables")
+            val tablesType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+            val viewTablesType = CommonTypeNames.HASH_MAP.parametrizedBy(
+                CommonTypeNames.STRING,
+                CommonTypeNames.SET.parametrizedBy(CommonTypeNames.STRING)
+            )
+            addLocalVariable(
+                name = viewTablesVar,
+                typeName = viewTablesType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    viewTablesType,
+                    "%L", database.views.size
+                )
             )
             for (view in database.views) {
                 val tablesVar = scope.getTmpVar("_tables")
-                addStatement(
-                    "$T $L = new $T($L)", tablesType, tablesVar, tablesType,
-                    view.tables.size
+                addLocalVariable(
+                    name = tablesVar,
+                    typeName = tablesType,
+                    assignExpr = XCodeBlock.ofNewInstance(
+                        codeLanguage,
+                        tablesType,
+                        "%L", view.tables.size
+                    )
                 )
                 for (table in view.tables) {
-                    addStatement("$L.add($S)", tablesVar, table)
+                    addStatement("%L.add(%S)", tablesVar, table)
                 }
                 addStatement(
-                    "$L.put($S, $L)", viewTablesVar,
-                    view.viewName.lowercase(Locale.US), tablesVar
+                    "%L.put(%S, %L)",
+                    viewTablesVar, view.viewName.lowercase(Locale.US), tablesVar
                 )
             }
             addStatement(
-                "return new $T(this, $L, $L, $L)",
-                RoomTypeNames.INVALIDATION_TRACKER, shadowTablesVar, viewTablesVar, tableNames
+                "return %L",
+                XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    RoomTypeNames.INVALIDATION_TRACKER,
+                    "this, %L, %L, %L",
+                    shadowTablesVar, viewTablesVar, tableNames
+                )
             )
         }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "createInvalidationTracker",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true
+        ).apply {
+            returns(RoomTypeNames.INVALIDATION_TRACKER)
+            addCode(body)
+        }.build()
     }
 
-    private fun addDaoImpls(builder: TypeSpec.Builder) {
+    private fun addDaoImpls(builder: XTypeSpec.Builder) {
         val scope = CodeGenScope(this)
-        builder.apply {
-            database.daoMethods.forEach { method ->
-                val name = method.dao.typeName.toJavaPoet().simpleName()
-                    .decapitalize(Locale.US)
-                    .stripNonJava()
-                val fieldName = scope.getTmpVar("_$name")
-                val field = FieldSpec.builder(
-                    method.dao.typeName.toJavaPoet(), fieldName,
-                    PRIVATE, VOLATILE
-                ).build()
-                addField(field)
-                addMethod(createDaoGetter(field, method))
+        database.daoMethods.forEach { method ->
+            val name = method.dao.typeName.simpleNames.first()
+                .decapitalize(Locale.US)
+                .stripNonJava()
+            val privateDaoProperty = XPropertySpec.builder(
+                language = codeLanguage,
+                name = scope.getTmpVar("_$name"),
+                typeName = if (codeLanguage == CodeLanguage.KOTLIN) {
+                    KotlinTypeNames.LAZY.parametrizedBy(method.dao.typeName)
+                } else {
+                    method.dao.typeName
+                },
+                visibility = VisibilityModifier.PRIVATE,
+                isMutable = codeLanguage == CodeLanguage.JAVA
+            ).apply {
+                // For Kotlin we rely on kotlin.Lazy while for Java we'll memoize the dao impl in
+                // the getter.
+                if (language == CodeLanguage.KOTLIN) {
+                   initializer(
+                       XCodeBlock.of(
+                           language,
+                           "lazy { %L }",
+                           XCodeBlock.ofNewInstance(language, method.dao.implTypeName, "this")
+                       )
+                   )
+                }
+            }.apply(
+                javaFieldBuilder = {
+                    // The volatile modifier is needed since in Java the memoization is generated.
+                    addModifiers(Modifier.VOLATILE)
+                },
+                kotlinPropertyBuilder = { }
+            ).build()
+            builder.addProperty(privateDaoProperty)
+            val overrideProperty =
+                codeLanguage == CodeLanguage.KOTLIN && method.element.isKotlinPropertyMethod()
+            if (overrideProperty) {
+                builder.addProperty(createDaoProperty(method, privateDaoProperty))
+            } else {
+                builder.addFunction(createDaoGetter(method, privateDaoProperty))
             }
         }
     }
 
-    private fun createDaoGetter(field: FieldSpec, method: DaoMethod): MethodSpec {
-        return MethodSpecHelper.overridingWithFinalParams(
-            elm = method.element,
-            owner = database.element.type
-        ).apply {
-            beginControlFlow("if ($N != null)", field).apply {
-                addStatement("return $N", field)
-            }
-            nextControlFlow("else").apply {
-                beginControlFlow("synchronized(this)").apply {
-                    beginControlFlow("if($N == null)", field).apply {
-                        addStatement(
-                            "$N = new $T(this)",
-                            field,
-                            method.dao.implTypeName.toJavaPoet()
-                        )
+    private fun createDaoGetter(method: DaoMethod, daoProperty: XPropertySpec): XFunSpec {
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            // For Java we implement the memoization logic in the Dao getter, meanwhile for Kotlin
+            // we rely on kotlin.Lazy to the getter just delegates to it.
+            when (codeLanguage) {
+                CodeLanguage.JAVA -> {
+                    beginControlFlow("if (%N != null)", daoProperty).apply {
+                        addStatement("return %N", daoProperty)
+                    }
+                    nextControlFlow("else").apply {
+                        beginControlFlow("synchronized(this)").apply {
+                            beginControlFlow("if(%N == null)", daoProperty).apply {
+                                addStatement(
+                                    "%N = %L",
+                                    daoProperty,
+                                    XCodeBlock.ofNewInstance(
+                                        language,
+                                        method.dao.implTypeName,
+                                        "this"
+                                    )
+
+                                )
+                            }
+                            endControlFlow()
+                            addStatement("return %N", daoProperty)
+                        }
+                        endControlFlow()
                     }
                     endControlFlow()
-                    addStatement("return $N", field)
                 }
-                endControlFlow()
+                CodeLanguage.KOTLIN -> {
+                    addStatement("return %N.value", daoProperty)
+                }
             }
-            endControlFlow()
+        }
+        return XFunSpec.overridingBuilder(
+            language = codeLanguage,
+            element = method.element,
+            owner = database.element.type
+        ).apply {
+            addCode(body.build())
         }.build()
     }
 
-    private fun createCreateOpenHelper(): MethodSpec {
+    private fun createDaoProperty(method: DaoMethod, daoProperty: XPropertySpec): XPropertySpec {
+        // TODO(b/257967987): This has a few flaws that need to be fixed.
+        return XPropertySpec.builder(
+            language = codeLanguage,
+            name = method.element.name.drop(3).replaceFirstChar { it.lowercase(Locale.US) },
+            typeName = method.dao.typeName,
+            visibility = when {
+                method.element.isPublic() -> VisibilityModifier.PUBLIC
+                method.element.isProtected() -> VisibilityModifier.PROTECTED
+                else -> VisibilityModifier.PUBLIC // Might be internal... ?
+            }
+        ).apply(
+            javaFieldBuilder = { error("Overriding a property in Java is impossible!") },
+            kotlinPropertyBuilder = {
+                addModifiers(KModifier.OVERRIDE)
+                getter(
+                    FunSpec.getterBuilder()
+                        .addStatement("return %L.value", daoProperty.name)
+                        .build()
+                )
+            }
+        ).build()
+    }
+
+    private fun createCreateOpenHelper(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("createOpenHelper").apply {
-            addModifiers(PROTECTED)
-            addAnnotation(Override::class.java)
-            returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
-
-            val configParam = ParameterSpec.builder(
-                RoomTypeNames.ROOM_DB_CONFIG,
-                "configuration"
-            ).build()
-            addParameter(configParam)
-
+        val configParamName = "config"
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val openHelperVar = scope.getTmpVar("_helper")
             val openHelperCode = scope.fork()
             SQLiteOpenHelperWriter(database)
-                .write(openHelperVar, configParam, openHelperCode)
-            addCode(openHelperCode.builder().build())
-            addStatement("return $L", openHelperVar)
+                .write(openHelperVar, configParamName, openHelperCode)
+            add(openHelperCode.generate())
+            addStatement("return %L", openHelperVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "createOpenHelper",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true,
+        ).apply {
+            returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
+            addParameter(RoomTypeNames.ROOM_DB_CONFIG, configParamName)
+            addCode(body)
         }.build()
     }
 
-    private fun getAutoMigrations(): MethodSpec {
-        return MethodSpec.methodBuilder("getAutoMigrations").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(
-                ParameterSpec.builder(
-                    ParameterizedTypeName.get(
-                        CommonTypeNames.MAP,
-                        ParameterizedTypeName.get(
-                            ClassName.get(Class::class.java),
-                            WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                        ),
-                        RoomTypeNames.AUTO_MIGRATION_SPEC
-                    ),
-                    "autoMigrationSpecsMap"
-                ).addAnnotation(NonNull::class.java).build()
+    private fun getAutoMigrations(): XFunSpec {
+        val scope = CodeGenScope(this)
+        val classOfAutoMigrationSpecTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(RoomTypeNames.AUTO_MIGRATION_SPEC)
+        )
+        val autoMigrationsListTypeName =
+            CommonTypeNames.ARRAY_LIST.parametrizedBy(RoomTypeNames.MIGRATION)
+        val specsMapParamName = "autoMigrationSpecs"
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            val listVar = scope.getTmpVar("_autoMigrations")
+            addLocalVariable(
+                name = listVar,
+                typeName = CommonTypeNames.LIST.parametrizedBy(RoomTypeNames.MIGRATION),
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, autoMigrationsListTypeName)
             )
-
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, RoomTypeNames.MIGRATION))
-            val autoMigrationsList = database.autoMigrations.map { autoMigrationResult ->
+            database.autoMigrations.forEach { autoMigrationResult ->
                 val implTypeName =
                     autoMigrationResult.getImplTypeName(database.typeName)
-                if (autoMigrationResult.isSpecProvided) {
-                    CodeBlock.of(
-                        "new $T(autoMigrationSpecsMap.get($T.class))",
-                        implTypeName.toJavaPoet(),
-                        autoMigrationResult.specClassName
+                val newInstanceCode = if (autoMigrationResult.isSpecProvided) {
+                    val specClassName = checkNotNull(autoMigrationResult.specClassName)
+                    // For Kotlin use getValue() as the Map's values are never null.
+                    val getFunction = when (language) {
+                        CodeLanguage.JAVA -> "get"
+                        CodeLanguage.KOTLIN -> "getValue"
+                    }
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        implTypeName,
+                        "%L.%L(%L)",
+                        specsMapParamName,
+                        getFunction,
+                        XCodeBlock.ofJavaClassLiteral(language, specClassName)
                     )
                 } else {
-                    CodeBlock.of("new $T()", implTypeName.toJavaPoet())
+                    XCodeBlock.ofNewInstance(language, implTypeName)
                 }
+                addStatement("%L.add(%L)", listVar, newInstanceCode)
             }
-            addStatement(
-                "return $T.asList($L)",
-                CommonTypeNames.ARRAYS,
-                CodeBlock.join(autoMigrationsList, ",$W")
+            addStatement("return %L", listVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getAutoMigrations",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true,
+        ).apply {
+            returns(CommonTypeNames.LIST.parametrizedBy(RoomTypeNames.MIGRATION))
+            addParameter(
+                CommonTypeNames.MAP.parametrizedBy(
+                    classOfAutoMigrationSpecTypeName,
+                    RoomTypeNames.AUTO_MIGRATION_SPEC,
+                ),
+                specsMapParamName
             )
+            addCode(body)
         }.build()
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
index dcfd472..1c0dbbd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
@@ -16,58 +16,69 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.vo.FtsEntity
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
 import java.util.Locale
 
 class FtsTableInfoValidationWriter(val entity: FtsEntity) : ValidationWriter() {
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = entity.tableName.stripNonJava().capitalize(Locale.US)
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
-        scope.builder().apply {
-            val columnListVar = scope.getTmpVar("_columns$suffix")
-            val columnListType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                CommonTypeNames.STRING
-            )
-
-            addStatement(
-                "final $T $L = new $T($L)", columnListType, columnListVar,
-                columnListType, entity.fields.size
+        scope.builder.apply {
+            val columnSetVar = scope.getTmpVar("_columns$suffix")
+            val columnsSetType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+            addLocalVariable(
+                name = columnSetVar,
+                typeName = columnsSetType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    columnsSetType,
+                    "%L",
+                    entity.fields.size
+                )
             )
             entity.nonHiddenFields.forEach {
-                addStatement("$L.add($S)", columnListVar, it.columnName)
+                addStatement("%L.add(%S)", columnSetVar, it.columnName)
             }
 
-            addStatement(
-                "final $T $L = new $T($S, $L, $S)",
-                RoomTypeNames.FTS_TABLE_INFO, expectedInfoVar, RoomTypeNames.FTS_TABLE_INFO,
-                entity.tableName, columnListVar, entity.createTableQuery
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.FTS_TABLE_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.FTS_TABLE_INFO,
+                    "%S, %L, %S",
+                    entity.tableName, columnSetVar, entity.createTableQuery
+
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.FTS_TABLE_INFO, existingVar, RoomTypeNames.FTS_TABLE_INFO,
-                dbParam, entity.tableName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.FTS_TABLE_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.FTS_TABLE_INFO_READ, dbParamName, entity.tableName
             )
 
-            beginControlFlow("if (!$L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
index 3f06b01..b76318d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
@@ -16,29 +16,27 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.codegen.toXTypeName
-import androidx.room.ext.AndroidTypeNames.CURSOR
+import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CollectionTypeNames
+import androidx.room.ext.CollectionsSizeExprCode
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.RoomTypeNames.DB_UTIL
-import androidx.room.ext.RoomTypeNames.ROOM_DB
-import androidx.room.ext.S
-import androidx.room.ext.T
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.MapKeySetExprCode
+import androidx.room.ext.RoomMemberNames
+import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.stripNonJava
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.query.result.PojoRowAdapter
 import androidx.room.vo.RelationCollector
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.Modifier
 
 /**
  * Writes the function that fetches the relations of a POJO and assigns them into the given map.
@@ -47,12 +45,18 @@
     private val collector: RelationCollector
 ) : TypeWriter.SharedFunctionSpec(
     "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
-        "As${collector.relation.pojoTypeName.toString().stripNonJava()}"
+        "As${collector.relation.pojoTypeName.toJavaPoet().toString().stripNonJava()}"
 ) {
     companion object {
         const val PARAM_MAP_VARIABLE = "_map"
         const val KEY_SET_VARIABLE = "__mapKeySet"
     }
+
+    private val usingLongSparseArray =
+        collector.mapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+    private val usingArrayMap =
+        collector.mapTypeName.rawTypeName == CollectionTypeNames.ARRAY_MAP
+
     override fun getUniqueKey(): String {
         val relation = collector.relation
         return "RelationCollectorMethodWriter" +
@@ -65,196 +69,113 @@
 
     override fun prepare(methodName: String, writer: TypeWriter, builder: XFunSpec.Builder) {
         val scope = CodeGenScope(writer)
-        val relation = collector.relation
+        scope.builder.apply {
+            // Check the input map key set for emptiness, returning early as no fetching is needed.
+            addIsInputEmptyCheck()
 
-        val param = ParameterSpec.builder(collector.mapTypeName, PARAM_MAP_VARIABLE)
-            .addModifiers(Modifier.FINAL)
-            .build()
-        val sqlQueryVar = scope.getTmpVar("_sql")
-
-        val cursorVar = "_cursor"
-        val itemKeyIndexVar = "_itemKeyIndex"
-        val stmtVar = scope.getTmpVar("_stmt")
-        scope.builder().apply {
-            val usingLongSparseArray =
-                collector.mapTypeName.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY
-            val usingArrayMap =
-                collector.mapTypeName.rawType == CollectionTypeNames.ARRAY_MAP
-            fun CodeBlock.Builder.addBatchPutAllStatement(tmpMapVar: String) {
-                if (usingArrayMap) {
-                    // When using ArrayMap there is ambiguity in the putAll() method, clear the
-                    // confusion by casting the temporary map.
-                    val disambiguityTypeName =
-                        ParameterizedTypeName.get(
-                            CommonTypeNames.MAP,
-                            collector.mapTypeName.typeArguments[0],
-                            collector.mapTypeName.typeArguments[1]
-                        )
-                    addStatement(
-                        "$N.putAll(($T) $L)",
-                        param, disambiguityTypeName, tmpMapVar
-                    )
-                } else {
-                    addStatement("$N.putAll($L)", param, tmpMapVar)
-                }
-            }
-            if (usingLongSparseArray) {
-                beginControlFlow("if ($N.isEmpty())", param)
-            } else {
-                val keySetType = ParameterizedTypeName.get(
-                    ClassName.get(Set::class.java), collector.keyTypeName
-                )
-                addStatement("final $T $L = $N.keySet()", keySetType, KEY_SET_VARIABLE, param)
-                beginControlFlow("if ($L.isEmpty())", KEY_SET_VARIABLE)
-            }.apply {
-                addStatement("return")
-            }
-            endControlFlow()
-            addStatement("// check if the size is too big, if so divide")
+            // Check if the input map key set exceeds MAX_BIND_PARAMETER_CNT, if so do a recursive
+            // fetch.
             beginControlFlow(
-                "if($N.size() > $T.MAX_BIND_PARAMETER_CNT)",
-                param, ROOM_DB.toJavaPoet()
-            ).apply {
-                // divide it into chunks
-                val tmpMapVar = scope.getTmpVar("_tmpInnerMap")
-                addStatement(
-                    "$T $L = new $T($L.MAX_BIND_PARAMETER_CNT)",
-                    collector.mapTypeName, tmpMapVar,
-                    collector.mapTypeName, ROOM_DB.toJavaPoet()
-                )
-                val tmpIndexVar = scope.getTmpVar("_tmpIndex")
-                addStatement("$T $L = 0", TypeName.INT, tmpIndexVar)
-                if (usingLongSparseArray || usingArrayMap) {
-                    val mapIndexVar = scope.getTmpVar("_mapIndex")
-                    val limitVar = scope.getTmpVar("_limit")
-                    addStatement("$T $L = 0", TypeName.INT, mapIndexVar)
-                    addStatement("final $T $L = $N.size()", TypeName.INT, limitVar, param)
-                    beginControlFlow("while($L < $L)", mapIndexVar, limitVar).apply {
-                        if (collector.relationTypeIsCollection) {
-                            addStatement(
-                                "$L.put($N.keyAt($L), $N.valueAt($L))",
-                                tmpMapVar, param, mapIndexVar, param, mapIndexVar
-                            )
-                        } else {
-                            addStatement(
-                                "$L.put($N.keyAt($L), null)",
-                                tmpMapVar, param, mapIndexVar
-                            )
-                        }
-                        addStatement("$L++", mapIndexVar)
-                    }
+                "if (%L > %T.MAX_BIND_PARAMETER_CNT)",
+                if (usingLongSparseArray) {
+                    XCodeBlock.of(language, "%L.size()", PARAM_MAP_VARIABLE)
                 } else {
-                    val mapKeyVar = scope.getTmpVar("_mapKey")
-                    beginControlFlow(
-                        "for($T $L : $L)",
-                        collector.keyTypeName, mapKeyVar, KEY_SET_VARIABLE
-                    ).apply {
-                        if (collector.relationTypeIsCollection) {
-                            addStatement(
-                                "$L.put($L, $N.get($L))",
-                                tmpMapVar, mapKeyVar, param, mapKeyVar
-                            )
-                        } else {
-                            addStatement("$L.put($L, null)", tmpMapVar, mapKeyVar)
-                        }
-                    }
-                }.apply {
-                    addStatement("$L++", tmpIndexVar)
-                    beginControlFlow(
-                        "if($L == $T.MAX_BIND_PARAMETER_CNT)",
-                        tmpIndexVar, ROOM_DB.toJavaPoet()
-                    ).apply {
-                        // recursively load that batch
-                        addStatement("$L($L)", methodName, tmpMapVar)
-                        // for non collection relation, put the loaded batch in the original map,
-                        // not needed when dealing with collections since references are passed
-                        if (!collector.relationTypeIsCollection) {
-                            addBatchPutAllStatement(tmpMapVar)
-                        }
-                        // clear nukes the backing data hence we create a new one
-                        addStatement(
-                            "$L = new $T($T.MAX_BIND_PARAMETER_CNT)",
-                            tmpMapVar, collector.mapTypeName, ROOM_DB.toJavaPoet()
-                        )
-                        addStatement("$L = 0", tmpIndexVar)
-                    }.endControlFlow()
-                }.endControlFlow()
-                beginControlFlow("if($L > 0)", tmpIndexVar).apply {
-                    // load the last batch
-                    addStatement("$L($L)", methodName, tmpMapVar)
-                    // for non collection relation, put the last batch in the original map
-                    if (!collector.relationTypeIsCollection) {
-                        addBatchPutAllStatement(tmpMapVar)
-                    }
-                }.endControlFlow()
+                    CollectionsSizeExprCode(language, PARAM_MAP_VARIABLE)
+                },
+                RoomTypeNames.ROOM_DB
+            ).apply {
+                addRecursiveFetchCall(methodName)
                 addStatement("return")
             }.endControlFlow()
+
+            // Create SQL query, acquire statement and bind parameters.
+            val stmtVar = scope.getTmpVar("_stmt")
+            val sqlQueryVar = scope.getTmpVar("_sql")
             collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
 
+            // Perform query and get a Cursor
+            val cursorVar = "_cursor"
             val shouldCopyCursor = collector.rowAdapter.let {
                 it is PojoRowAdapter && it.relationCollectors.isNotEmpty()
             }
-            addStatement(
-                "final $T $L = $T.query($N, $L, $L, $L)",
-                CURSOR.toJavaPoet(),
-                cursorVar,
-                DB_UTIL.toJavaPoet(),
-                DaoWriter.DB_PROPERTY_NAME,
-                stmtVar,
-                if (shouldCopyCursor) "true" else "false",
-                "null"
+            addLocalVariable(
+                name = cursorVar,
+                typeName = AndroidTypeNames.CURSOR,
+                assignExpr = XCodeBlock.of(
+                    language,
+                    "%M(%N, %L, %L, %L)",
+                    RoomMemberNames.DB_UTIL_QUERY,
+                    DaoWriter.DB_PROPERTY_NAME,
+                    stmtVar,
+                    if (shouldCopyCursor) "true" else "false",
+                    "null"
+                )
             )
 
+            val relation = collector.relation
             beginControlFlow("try").apply {
+                // Gets index of the column to be used as key
+                val itemKeyIndexVar = "_itemKeyIndex"
                 if (relation.junction != null) {
-                    // when using a junction table the relationship map is keyed on the parent
+                    // When using a junction table the relationship map is keyed on the parent
                     // reference column of the junction table, the same column used in the WHERE IN
                     // clause, this column is the rightmost column in the generated SELECT
                     // clause.
                     val junctionParentColumnIndex = relation.projection.size
-                    addStatement(
-                        "final $T $L = $L; // _junction.$L",
-                        TypeName.INT, itemKeyIndexVar, junctionParentColumnIndex,
-                        relation.junction.parentField.columnName
+                    addStatement("// _junction.%L", relation.junction.parentField.columnName)
+                    addLocalVal(
+                        itemKeyIndexVar,
+                        XTypeName.PRIMITIVE_INT,
+                        "%L",
+                        junctionParentColumnIndex
                     )
                 } else {
-                    addStatement(
-                        "final $T $L = $T.getColumnIndex($L, $S)",
-                        TypeName.INT, itemKeyIndexVar, CURSOR_UTIL.toJavaPoet(), cursorVar,
+                    addLocalVal(
+                        itemKeyIndexVar,
+                        XTypeName.PRIMITIVE_INT,
+                        "%M(%L, %S)",
+                        RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX,
+                        cursorVar,
                         relation.entityField.columnName
                     )
                 }
-
-                beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
+                // Check if index of column is not -1, indicating the column for the key is not in
+                // the result, can happen if the user specified a bad projection in @Relation.
+                beginControlFlow("if (%L == -1)", itemKeyIndexVar).apply {
                     addStatement("return")
                 }
                 endControlFlow()
 
+                // Prepare item column indices
                 collector.rowAdapter.onCursorReady(cursorVarName = cursorVar, scope = scope)
+
                 val tmpVarName = scope.getTmpVar("_item")
-                beginControlFlow("while($L.moveToNext())", cursorVar).apply {
-                    // read key from the cursor
+                beginControlFlow("while (%L.moveToNext())", cursorVar).apply {
+                    // Read key from the cursor, convert row to item and place it on map
                     collector.readKey(
                         cursorVarName = cursorVar,
                         indexVar = itemKeyIndexVar,
+                        keyReader = collector.entityKeyColumnReader,
                         scope = scope
                     ) { keyVar ->
                         if (collector.relationTypeIsCollection) {
                             val relationVar = scope.getTmpVar("_tmpRelation")
-                            addStatement(
-                                "$T $L = $N.get($L)", collector.relationTypeName,
-                                relationVar, param, keyVar
+                            addLocalVal(
+                                relationVar,
+                                collector.relationTypeName.copy(nullable = true),
+                                "%L.get(%L)",
+                                PARAM_MAP_VARIABLE, keyVar
                             )
-                            beginControlFlow("if ($L != null)", relationVar)
-                            addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
+                            beginControlFlow("if (%L != null)", relationVar)
+                            addLocalVariable(tmpVarName, relation.pojoTypeName)
                             collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("$L.add($L)", relationVar, tmpVarName)
+                            addStatement("%L.add(%L)", relationVar, tmpVarName)
                             endControlFlow()
                         } else {
-                            beginControlFlow("if ($N.containsKey($L))", param, keyVar)
-                            addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
+                            beginControlFlow("if (%N.containsKey(%L))", PARAM_MAP_VARIABLE, keyVar)
+                            addLocalVariable(tmpVarName, relation.pojoTypeName)
                             collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("$N.put($L, $L)", param, keyVar, tmpVarName)
+                            addStatement("%N.put(%L, %L)", PARAM_MAP_VARIABLE, keyVar, tmpVarName)
                             endControlFlow()
                         }
                     }
@@ -262,13 +183,89 @@
                 endControlFlow()
             }
             nextControlFlow("finally").apply {
-                addStatement("$L.close()", cursorVar)
+                addStatement("%L.close()", cursorVar)
             }
             endControlFlow()
         }
         builder.apply {
-            addParameter(param.type.toXTypeName(), param.name)
+            addParameter(collector.mapTypeName, PARAM_MAP_VARIABLE)
             addCode(scope.generate())
         }
     }
+
+    private fun XCodeBlock.Builder.addIsInputEmptyCheck() {
+        if (usingLongSparseArray) {
+            beginControlFlow("if (%L.isEmpty())", PARAM_MAP_VARIABLE)
+        } else {
+            val keySetType = CommonTypeNames.SET.parametrizedBy(collector.keyTypeName)
+            addLocalVariable(
+                name = KEY_SET_VARIABLE,
+                typeName = keySetType,
+                assignExpr = MapKeySetExprCode(language, PARAM_MAP_VARIABLE)
+            )
+            beginControlFlow("if (%L.isEmpty())", KEY_SET_VARIABLE)
+        }.apply {
+            addStatement("return")
+        }
+        endControlFlow()
+    }
+
+    private fun XCodeBlock.Builder.addRecursiveFetchCall(methodName: String) {
+        fun getRecursiveCall(itVarName: String) =
+            XCodeBlock.of(
+                language,
+                "%L(%L)",
+                methodName, itVarName
+            )
+        val utilFunction =
+            RoomTypeNames.RELATION_UTIL.let {
+                when {
+                    usingLongSparseArray ->
+                        it.packageMember("recursiveFetchLongSparseArray")
+                    usingArrayMap ->
+                        it.packageMember("recursiveFetchArrayMap")
+                    else ->
+                        it.packageMember("recursiveFetchHashMap")
+                }
+            }
+        when (language) {
+            CodeLanguage.JAVA -> {
+                val paramName = "map"
+                if (collector.javaLambdaSyntaxAvailable) {
+                    add("%M(%L, %L, (%L) -> {\n",
+                        utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection,
+                        paramName
+                    )
+                    indent()
+                    addStatement("%L", getRecursiveCall(paramName))
+                    addStatement("return %T.INSTANCE", Unit::class.asClassName())
+                    unindent()
+                    addStatement("})")
+                } else {
+                    val functionImpl = Function1TypeSpec(
+                        language = language,
+                        parameterTypeName = collector.mapTypeName,
+                        parameterName = paramName,
+                        returnTypeName = Unit::class.asClassName(),
+                    ) {
+                        addStatement("%L", getRecursiveCall(paramName))
+                        addStatement("return %T.INSTANCE", Unit::class.asClassName())
+                    }
+                    addStatement(
+                        "%M(%L, %L, %L)",
+                        utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection,
+                        functionImpl
+                    )
+                }
+            }
+            CodeLanguage.KOTLIN -> {
+                beginControlFlow(
+                    "%M(%L, %L)",
+                    utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection
+                )
+                addStatement("%L", getRecursiveCall("it"))
+                endControlFlow()
+            }
+        }
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
index dd62e1c..dc152fb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
@@ -17,25 +17,25 @@
 package androidx.room.writer
 
 import androidx.annotation.VisibleForTesting
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.beginForEachControlFlow
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.XTypeSpec
+import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.RoomTypeNames.DB_UTIL
-import androidx.room.ext.S
 import androidx.room.ext.SupportDbTypeNames
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.Database
 import androidx.room.vo.DatabaseView
 import androidx.room.vo.Entity
 import androidx.room.vo.FtsEntity
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeSpec
 import java.util.ArrayDeque
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PUBLIC
 
 /**
  * The threshold amount of statements in a validateMigration() method before creating additional
@@ -47,215 +47,279 @@
  * Create an open helper using SupportSQLiteOpenHelperFactory
  */
 class SQLiteOpenHelperWriter(val database: Database) {
-    fun write(outVar: String, configuration: ParameterSpec, scope: CodeGenScope) {
-        scope.builder().apply {
+
+    private val dbParamName = "db"
+
+    fun write(outVar: String, configParamName: String, scope: CodeGenScope) {
+        scope.builder.apply {
             val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
             val callbackVar = scope.getTmpVar("_openCallback")
-            addStatement(
-                "final $T $L = new $T($N, $L, $S, $S)",
-                SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
-                callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
-                createOpenCallback(scope), database.identityHash, database.legacyIdentityHash
+            addLocalVariable(
+                name = callbackVar,
+                typeName = SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.OPEN_HELPER,
+                    "%L, %L, %S, %S",
+                    configParamName,
+                    createOpenCallback(scope),
+                    database.identityHash,
+                    database.legacyIdentityHash
+                )
             )
             // build configuration
-            addStatement(
-                """
-                    final $T $L = $T.builder($N.context)
-                    .name($N.name)
-                    .callback($L)
-                    .build()
-                """.trimIndent(),
-                SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG, sqliteConfigVar,
+            addLocalVal(
+                sqliteConfigVar,
                 SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
-                configuration, configuration, callbackVar
+                "%T.builder(%L.context).name(%L.name).callback(%L).build()",
+                SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
+                configParamName,
+                configParamName,
+                callbackVar
             )
-            addStatement(
-                "final $T $N = $N.sqliteOpenHelperFactory.create($L)",
-                SupportDbTypeNames.SQLITE_OPEN_HELPER, outVar,
-                configuration, sqliteConfigVar
+            addLocalVal(
+                outVar,
+                SupportDbTypeNames.SQLITE_OPEN_HELPER,
+                "%L.sqliteOpenHelperFactory.create(%L)",
+                configParamName,
+                sqliteConfigVar
             )
         }
     }
 
-    private fun createOpenCallback(scope: CodeGenScope): TypeSpec {
-        return TypeSpec.anonymousClassBuilder(L, database.version).apply {
+    private fun createOpenCallback(scope: CodeGenScope): XTypeSpec {
+        return XTypeSpec.anonymousClassBuilder(
+            scope.language, "%L", database.version
+        ).apply {
             superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
-            addMethod(createCreateAllTables())
-            addMethod(createDropAllTables(scope.fork()))
-            addMethod(createOnCreate(scope.fork()))
-            addMethod(createOnOpen(scope.fork()))
-            addMethod(createOnPreMigrate())
-            addMethod(createOnPostMigrate())
-            addMethods(createValidateMigration(scope.fork()))
+            addFunction(createCreateAllTables(scope))
+            addFunction(createDropAllTables(scope.fork()))
+            addFunction(createOnCreate(scope.fork()))
+            addFunction(createOnOpen(scope.fork()))
+            addFunction(createOnPreMigrate(scope))
+            addFunction(createOnPostMigrate(scope))
+            createValidateMigration(scope.fork()).forEach {
+                addFunction(it)
+            }
         }.build()
     }
 
-    private fun createValidateMigration(scope: CodeGenScope): List<MethodSpec> {
-        val methodSpecs = mutableListOf<MethodSpec>()
+    private fun createValidateMigration(scope: CodeGenScope): List<XFunSpec> {
+        val methodBuilders = mutableListOf<XFunSpec.Builder>()
         val entities = ArrayDeque(database.entities)
         val views = ArrayDeque(database.views)
-        val dbParam = ParameterSpec.builder(SupportDbTypeNames.DB, "_db").build()
         while (!entities.isEmpty() || !views.isEmpty()) {
-            val isPrimaryMethod = methodSpecs.isEmpty()
+            val isPrimaryMethod = methodBuilders.isEmpty()
             val methodName = if (isPrimaryMethod) {
                 "onValidateSchema"
             } else {
-                "onValidateSchema${methodSpecs.size + 1}"
+                "onValidateSchema${methodBuilders.size + 1}"
             }
-            methodSpecs.add(
-                MethodSpec.methodBuilder(methodName).apply {
-                    if (isPrimaryMethod) {
-                        addModifiers(PUBLIC)
-                        addAnnotation(Override::class.java)
-                    } else {
-                        addModifiers(PRIVATE)
+            val validateMethod = XFunSpec.builder(
+                language = scope.language,
+                name = methodName,
+                visibility = if (isPrimaryMethod) {
+                    VisibilityModifier.PUBLIC
+                } else {
+                    VisibilityModifier.PRIVATE
+                },
+                isOverride = isPrimaryMethod
+            ).apply {
+                returns(RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT)
+                addParameter(SupportDbTypeNames.DB, dbParamName)
+                var statementCount = 0
+                while (!entities.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
+                    val methodScope = scope.fork()
+                    val entity = entities.poll()
+                    val validationWriter = when (entity) {
+                        is FtsEntity -> FtsTableInfoValidationWriter(entity)
+                        else -> TableInfoValidationWriter(entity)
                     }
-                    returns(RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT)
-                    addParameter(dbParam)
-                    var statementCount = 0
-                    while (!entities.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
-                        val methodScope = scope.fork()
-                        val entity = entities.poll()
-                        val validationWriter = when (entity) {
-                            is FtsEntity -> FtsTableInfoValidationWriter(entity)
-                            else -> TableInfoValidationWriter(entity)
-                        }
-                        validationWriter.write(dbParam, methodScope)
-                        addCode(methodScope.builder().build())
-                        statementCount += validationWriter.statementCount()
-                    }
-                    while (!views.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
-                        val methodScope = scope.fork()
-                        val view = views.poll()
-                        val validationWriter = ViewInfoValidationWriter(view)
-                        validationWriter.write(dbParam, methodScope)
-                        addCode(methodScope.builder().build())
-                        statementCount += validationWriter.statementCount()
-                    }
-                    if (!isPrimaryMethod) {
-                        addStatement(
-                            "return new $T(true, null)",
-                            RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+                    validationWriter.write(dbParamName, methodScope)
+                    addCode(methodScope.generate())
+                    statementCount += validationWriter.statementCount()
+                }
+                while (!views.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
+                    val methodScope = scope.fork()
+                    val view = views.poll()
+                    val validationWriter = ViewInfoValidationWriter(view)
+                    validationWriter.write(dbParamName, methodScope)
+                    addCode(methodScope.generate())
+                    statementCount += validationWriter.statementCount()
+                }
+                if (!isPrimaryMethod) {
+                    addStatement(
+                        "return %L",
+                        XCodeBlock.ofNewInstance(
+                            scope.language,
+                            RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                            "true, null"
                         )
-                    }
-                }.build()
-            )
+                    )
+                }
+            }
+            methodBuilders.add(validateMethod)
         }
 
         // If there are secondary validate methods then add invocation statements to all of them
         // from the primary method.
-        if (methodSpecs.size > 1) {
-            methodSpecs[0] = methodSpecs[0].toBuilder().apply {
+        if (methodBuilders.size > 1) {
+            val body = XCodeBlock.builder(scope.language).apply {
                 val resultVar = scope.getTmpVar("_result")
-                addStatement("$T $L", RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT, resultVar)
-                methodSpecs.drop(1).forEach {
-                    addStatement("$L = ${it.name}($N)", resultVar, dbParam)
-                    beginControlFlow("if (!$L.isValid)", resultVar)
-                    addStatement("return $L", resultVar)
+                addLocalVariable(
+                    name = resultVar,
+                    typeName = RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                    isMutable = true
+                )
+                methodBuilders.drop(1).forEach {
+                    addStatement("%L = %L(%L)", resultVar, it.name, dbParamName)
+                    beginControlFlow("if (!%L.isValid)", resultVar).apply {
+                        addStatement("return %L", resultVar)
+                    }
                     endControlFlow()
                 }
                 addStatement(
-                    "return new $T(true, null)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        scope.language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "true, null"
+                    )
                 )
             }.build()
-        } else if (methodSpecs.size == 1) {
-            methodSpecs[0] = methodSpecs[0].toBuilder().apply {
-                addStatement(
-                    "return new $T(true, null)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+            methodBuilders.first().addCode(body)
+        } else if (methodBuilders.size == 1) {
+            methodBuilders.first().addStatement(
+                "return %L",
+                XCodeBlock.ofNewInstance(
+                    scope.language,
+                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                    "true, null"
                 )
-            }.build()
+            )
         }
-
-        return methodSpecs
+        return methodBuilders.map { it.build() }
     }
 
-    private fun createOnCreate(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("onCreate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            invokeCallbacks(scope, "onCreate")
+    private fun createOnCreate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onCreate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addCode(createInvokeCallbacksCode(scope, "onCreate"))
         }.build()
     }
 
-    private fun createOnOpen(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("onOpen").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("mDatabase = _db")
+    private fun createOnOpen(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onOpen",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addStatement("mDatabase = %L", dbParamName)
             if (database.enableForeignKeys) {
-                addStatement("_db.execSQL($S)", "PRAGMA foreign_keys = ON")
+                addStatement("%L.execSQL(%S)", dbParamName, "PRAGMA foreign_keys = ON")
             }
-            addStatement("internalInitInvalidationTracker(_db)")
-            invokeCallbacks(scope, "onOpen")
+            addStatement("internalInitInvalidationTracker(%L)", dbParamName)
+            addCode(createInvokeCallbacksCode(scope, "onOpen"))
         }.build()
     }
 
-    private fun createCreateAllTables(): MethodSpec {
-        return MethodSpec.methodBuilder("createAllTables").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createCreateAllTables(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "createAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.bundle.buildCreateQueries().forEach {
-                addStatement("_db.execSQL($S)", it)
+                addStatement("%L.execSQL(%S)", dbParamName, it)
             }
         }.build()
     }
 
-    private fun createDropAllTables(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("dropAllTables").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createDropAllTables(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "dropAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.entities.forEach {
-                addStatement("_db.execSQL($S)", createDropTableQuery(it))
+                addStatement("%L.execSQL(%S)", dbParamName, createDropTableQuery(it))
             }
             database.views.forEach {
-                addStatement("_db.execSQL($S)", createDropViewQuery(it))
+                addStatement("%L.execSQL(%S)", dbParamName, createDropViewQuery(it))
             }
-            invokeCallbacks(scope, "onDestructiveMigration")
+            addCode(createInvokeCallbacksCode(scope, "onDestructiveMigration"))
         }.build()
     }
 
-    private fun createOnPreMigrate(): MethodSpec {
-        return MethodSpec.methodBuilder("onPreMigrate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("$T.dropFtsSyncTriggers($L)", DB_UTIL.toJavaPoet(), "_db")
+    private fun createOnPreMigrate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onPreMigrate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addStatement("%M(%L)", RoomMemberNames.DB_UTIL_DROP_FTS_SYNC_TRIGGERS, dbParamName)
         }.build()
     }
 
-    private fun createOnPostMigrate(): MethodSpec {
-        return MethodSpec.methodBuilder("onPostMigrate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createOnPostMigrate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onPostMigrate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.entities.filterIsInstance(FtsEntity::class.java)
                 .filter { it.ftsOptions.contentEntity != null }
                 .flatMap { it.contentSyncTriggerCreateQueries }
                 .forEach { syncTriggerQuery ->
-                    addStatement("_db.execSQL($S)", syncTriggerQuery)
+                    addStatement("%L.execSQL(%S)", dbParamName, syncTriggerQuery)
                 }
         }.build()
     }
 
-    private fun MethodSpec.Builder.invokeCallbacks(scope: CodeGenScope, methodName: String) {
-        val iVar = scope.getTmpVar("_i")
-        val sizeVar = scope.getTmpVar("_size")
-        beginControlFlow("if (mCallbacks != null)").apply {
-            beginControlFlow(
-                "for (int $N = 0, $N = mCallbacks.size(); $N < $N; $N++)",
-                iVar, sizeVar, iVar, sizeVar, iVar
-            ).apply {
-                addStatement("mCallbacks.get($N).$N(_db)", iVar, methodName)
+    private fun createInvokeCallbacksCode(scope: CodeGenScope, methodName: String): XCodeBlock {
+        val localCallbackListVarName = scope.getTmpVar("_callbacks")
+        val callbackVarName = scope.getTmpVar("_callback")
+        return XCodeBlock.builder(scope.language).apply {
+            addLocalVal(
+                localCallbackListVarName,
+                CommonTypeNames.LIST.parametrizedBy(
+                    // For Kotlin, the variance is redundant, but for Java, due to `mCallbacks`
+                    // not having @JvmSuppressWildcards, we use a wildcard name.
+                    if (language == CodeLanguage.KOTLIN) {
+                        RoomTypeNames.ROOM_DB_CALLBACK
+                    } else {
+                        XTypeName.getProducerExtendsName(RoomTypeNames.ROOM_DB_CALLBACK)
+                    }
+                ).copy(nullable = true),
+                "mCallbacks"
+            )
+            beginControlFlow("if (%L != null)", localCallbackListVarName).apply {
+                beginForEachControlFlow(
+                    itemVarName = callbackVarName,
+                    typeName = RoomTypeNames.ROOM_DB_CALLBACK,
+                    iteratorVarName = localCallbackListVarName
+                ).apply {
+                    addStatement("%L.%L(%L)", callbackVarName, methodName, dbParamName)
+                }
+                endControlFlow()
             }
             endControlFlow()
-        }
-        endControlFlow()
+        }.build()
     }
 
     @VisibleForTesting
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
index 1edc663..c7c18fd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
@@ -16,138 +16,177 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.vo.Entity
 import androidx.room.vo.columnNames
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
 import java.util.Arrays
-import java.util.HashMap
-import java.util.HashSet
 import java.util.Locale
 
 class TableInfoValidationWriter(val entity: Entity) : ValidationWriter() {
 
     companion object {
         const val CREATED_FROM_ENTITY = "CREATED_FROM_ENTITY"
+        val ARRAY_TYPE_NAME = Arrays::class.asClassName()
     }
 
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = entity.tableName.stripNonJava().capitalize(Locale.US)
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
-        scope.builder().apply {
+        scope.builder.apply {
             val columnListVar = scope.getTmpVar("_columns$suffix")
-            val columnListType = ParameterizedTypeName.get(
-                HashMap::class.typeName,
-                CommonTypeNames.STRING, RoomTypeNames.TABLE_INFO_COLUMN
+            val columnListType = CommonTypeNames.HASH_MAP.parametrizedBy(
+                CommonTypeNames.STRING,
+                RoomTypeNames.TABLE_INFO_COLUMN
             )
-
-            addStatement(
-                "final $T $L = new $T($L)", columnListType, columnListVar,
-                columnListType, entity.fields.size
+            addLocalVariable(
+                name = columnListVar,
+                typeName = columnListType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    columnListType,
+                    "%L",
+                    entity.fields.size
+                )
             )
             entity.fields.forEach { field ->
                 addStatement(
-                    "$L.put($S, new $T($S, $S, $L, $L, $S, $T.$L))",
-                    columnListVar, field.columnName, RoomTypeNames.TABLE_INFO_COLUMN,
-                    /*name*/ field.columnName,
-                    /*type*/ field.affinity?.name ?: SQLTypeAffinity.TEXT.name,
-                    /*nonNull*/ field.nonNull,
-                    /*pkeyPos*/ entity.primaryKey.fields.indexOf(field) + 1,
-                    /*defaultValue*/ field.defaultValue,
-                    /*createdFrom*/ RoomTypeNames.TABLE_INFO, CREATED_FROM_ENTITY
+                    "%L.put(%S, %L)",
+                    columnListVar,
+                    field.columnName,
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_COLUMN,
+                        "%S, %S, %L, %L, %S, %T.%L",
+                        field.columnName, // name
+                        field.affinity?.name ?: SQLTypeAffinity.TEXT.name, // type
+                        field.nonNull, // nonNull
+                        entity.primaryKey.fields.indexOf(field) + 1, // pkeyPos
+                        field.defaultValue, // defaultValue
+                        RoomTypeNames.TABLE_INFO, CREATED_FROM_ENTITY // createdFrom
+                    )
                 )
             }
 
             val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
-            val foreignKeySetType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                RoomTypeNames.TABLE_INFO_FOREIGN_KEY
-            )
-            addStatement(
-                "final $T $L = new $T($L)", foreignKeySetType, foreignKeySetVar,
-                foreignKeySetType, entity.foreignKeys.size
+            val foreignKeySetType =
+                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
+            addLocalVariable(
+                name = foreignKeySetVar,
+                typeName = foreignKeySetType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    foreignKeySetType,
+                    "%L",
+                    entity.foreignKeys.size
+                )
             )
             entity.foreignKeys.forEach {
-                val myColumnNames = it.childFields
-                    .joinToString(",") { "\"${it.columnName}\"" }
-                val refColumnNames = it.parentColumns
-                    .joinToString(",") { "\"$it\"" }
                 addStatement(
-                    "$L.add(new $T($S, $S, $S," +
-                        "$T.asList($L), $T.asList($L)))",
+                    "%L.add(%L)",
                     foreignKeySetVar,
-                    RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
-                    /*parent table*/ it.parentTable,
-                    /*on delete*/ it.onDelete.sqlName,
-                    /*on update*/ it.onUpdate.sqlName,
-                    Arrays::class.typeName,
-                    /*parent names*/ myColumnNames,
-                    Arrays::class.typeName,
-                    /*parent column names*/ refColumnNames
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
+                        "%S, %S, %S, %L, %L",
+                        it.parentTable, // parent table
+                        it.onDelete.sqlName, // on delete
+                        it.onUpdate.sqlName, // on update
+                        listOfStrings(it.childFields.map { it.columnName }), // parent names
+                        listOfStrings(it.parentColumns) // parent column names
+                    )
                 )
             }
 
             val indicesSetVar = scope.getTmpVar("_indices$suffix")
-            val indicesType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                RoomTypeNames.TABLE_INFO_INDEX
-            )
-            addStatement(
-                "final $T $L = new $T($L)", indicesType, indicesSetVar,
-                indicesType, entity.indices.size
+            val indicesType =
+                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
+            addLocalVariable(
+                name = indicesSetVar,
+                typeName = indicesType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    indicesType,
+                    "%L",
+                    entity.indices.size
+                )
             )
             entity.indices.forEach { index ->
-                val columnNames = index.columnNames.joinToString(",") { "\"$it\"" }
                 val orders = if (index.orders.isEmpty()) {
-                    index.columnNames.map { "ASC" }.joinToString(",") { "\"$it\"" }
+                    index.columnNames.map { "ASC" }
                 } else {
-                    index.orders.joinToString(",") { "\"$it\"" }
+                    index.orders.map { it.name }
                 }
                 addStatement(
-                    "$L.add(new $T($S, $L, $T.asList($L), $T.asList($L)))",
+                    "%L.add(%L)",
                     indicesSetVar,
-                    RoomTypeNames.TABLE_INFO_INDEX,
-                    index.name,
-                    index.unique,
-                    Arrays::class.typeName,
-                    columnNames,
-                    Arrays::class.typeName,
-                    orders,
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_INDEX,
+                        "%S, %L, %L, %L",
+                        index.name, // name
+                        index.unique, // unique
+                        listOfStrings(index.columnNames), // columns
+                        listOfStrings(orders) // orders
+                    )
                 )
             }
 
-            addStatement(
-                "final $T $L = new $T($S, $L, $L, $L)",
-                RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
-                entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.TABLE_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.TABLE_INFO,
+                    "%S, %L, %L, %L",
+                    entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.TABLE_INFO, existingVar, RoomTypeNames.TABLE_INFO,
-                dbParam, entity.tableName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.TABLE_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.TABLE_INFO_READ, dbParamName, entity.tableName
             )
 
-            beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
         }
     }
+
+    private fun CodeBlockWrapper.listOfStrings(strings: List<String>): XCodeBlock {
+        val placeholders = List(strings.size) { "%S" }.joinToString()
+        val function: Any = when (language) {
+            CodeLanguage.JAVA -> XCodeBlock.of(language, "%T.asList", ARRAY_TYPE_NAME)
+            CodeLanguage.KOTLIN -> "listOf"
+        }
+        return XCodeBlock.of(
+            language,
+            "%L($placeholders)",
+            function, *strings.toTypedArray()
+        )
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
index 7499770..cede6be 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
@@ -16,9 +16,9 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterSpec
 
 /**
  * Common interface for database validation witters.
@@ -27,53 +27,65 @@
 
     private lateinit var countingScope: CountingCodeGenScope
 
-    fun write(dbParam: ParameterSpec, scope: CodeGenScope) {
+    fun write(dbParamName: String, scope: CodeGenScope) {
         countingScope = CountingCodeGenScope(scope)
-        write(dbParam, countingScope)
+        write(dbParamName, countingScope)
     }
 
-    protected abstract fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope)
+    protected abstract fun write(dbParamName: String, scope: CountingCodeGenScope)
 
     /**
      * The estimated amount of statements this writer will write.
      */
     fun statementCount() = countingScope.statementCount()
 
-    protected class CountingCodeGenScope(val scope: CodeGenScope) {
+    protected class CountingCodeGenScope(private val scope: CodeGenScope) {
 
-        private var builder: CodeBlockWrapper? = null
+        val builder = CodeBlockWrapper(scope.builder)
 
         fun getTmpVar(prefix: String) = scope.getTmpVar(prefix)
 
-        fun builder(): CodeBlockWrapper {
-            if (builder == null) {
-                builder = CodeBlockWrapper(scope.builder())
-            }
-            return builder!!
-        }
-
-        fun statementCount() = builder?.statementCount ?: 0
+        fun statementCount() = builder.statementCount
     }
 
     // A wrapper class that counts statements added to a CodeBlock
-    protected class CodeBlockWrapper(val builder: CodeBlock.Builder) {
+    protected class CodeBlockWrapper(
+        private val builder: XCodeBlock.Builder
+    ) : XCodeBlock.Builder by builder {
 
         var statementCount = 0
             private set
 
-        fun addStatement(format: String, vararg args: Any?): CodeBlockWrapper {
+        override fun add(format: String, vararg args: Any?): XCodeBlock.Builder {
+            statementCount++
+            builder.add(format, *args)
+            return this
+        }
+
+        override fun addLocalVariable(
+            name: String,
+            typeName: XTypeName,
+            isMutable: Boolean,
+            assignExpr: XCodeBlock?
+        ): XCodeBlock.Builder {
+            statementCount++
+            builder.addLocalVariable(name, typeName, isMutable, assignExpr)
+            return this
+        }
+
+        override fun addStatement(format: String, vararg args: Any?): CodeBlockWrapper {
             statementCount++
             builder.addStatement(format, *args)
             return this
         }
 
-        fun beginControlFlow(controlFlow: String, vararg args: Any): CodeBlockWrapper {
+        override fun beginControlFlow(controlFlow: String, vararg args: Any?): CodeBlockWrapper {
             statementCount++
             builder.beginControlFlow(controlFlow, *args)
             return this
         }
 
-        fun endControlFlow(): CodeBlockWrapper {
+        override fun endControlFlow(): CodeBlockWrapper {
             builder.endControlFlow()
             return this
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
index 5e2b463..25fd562 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
@@ -16,42 +16,52 @@
 
 package androidx.room.writer
 
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.vo.DatabaseView
-import com.squareup.javapoet.ParameterSpec
 import java.util.Locale
 
 class ViewInfoValidationWriter(val view: DatabaseView) : ValidationWriter() {
 
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = view.viewName.stripNonJava().capitalize(Locale.US)
-        scope.builder().apply {
+        scope.builder.apply {
             val expectedInfoVar = scope.getTmpVar("_info$suffix")
-            addStatement(
-                "final $T $L = new $T($S, $S)",
-                RoomTypeNames.VIEW_INFO, expectedInfoVar, RoomTypeNames.VIEW_INFO,
-                view.viewName, view.createViewQuery
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.VIEW_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.VIEW_INFO,
+                    "%S, %S",
+                    view.viewName, view.createViewQuery
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.VIEW_INFO, existingVar, RoomTypeNames.VIEW_INFO,
-                dbParam, view.viewName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.VIEW_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.VIEW_INFO_READ, dbParamName, view.viewName
             )
 
-            beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${view.viewName}(${view.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${view.viewName}(${view.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
index ee95322..6c0353f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
@@ -27,7 +27,7 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.STRING
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -308,7 +308,7 @@
                 `is`(
                     ParameterizedTypeName.get(
                         ClassName.get("foo.bar", "MyClass.MyList"),
-                        CommonTypeNames.STRING, COMMON.USER_TYPE_NAME
+                        STRING.toJavaPoet(), COMMON.USER_TYPE_NAME
                     ) as TypeName
                 )
             )
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
index 9964afe..4458240 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
@@ -340,7 +340,7 @@
                 .isEqualTo(
                     ParameterizedTypeName.get(
                         ClassName.get("foo.bar", "MyClass.MyList"),
-                        CommonTypeNames.STRING, USER_TYPE_NAME
+                        CommonTypeNames.STRING.toJavaPoet(), USER_TYPE_NAME
                     ) as TypeName
                 )
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index caedbe7..78038eb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -19,12 +19,13 @@
 import COMMON
 import androidx.room.Dao
 import androidx.room.Query
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -332,7 +333,7 @@
         singleQueryMethod<ReadQueryMethod>(
             """
                 @Query("select * from User")
-                abstract public <T> ${CommonTypeNames.LIST}<T> foo(int x);
+                abstract public <T> ${LIST.toJavaPoet()}<T> foo(int x);
                 """
         ) { parsedQuery, invocation ->
             val expected: TypeName = ParameterizedTypeName.get(
@@ -369,7 +370,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${LIST.toJavaPoet()}<Integer>>
                 getFactorialLiveData();
                 """
         ) { parsedQuery, _ ->
@@ -404,7 +405,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${LIST.toJavaPoet()}<Integer>>
                 getFactorialLiveData();
                 """
         ) { _, invocation ->
@@ -872,7 +873,7 @@
             val pojoRowAdapter = listAdapter.rowAdapters.single() as PojoRowAdapter
             assertThat(pojoRowAdapter.relationCollectors.size, `is`(1))
             assertThat(
-                pojoRowAdapter.relationCollectors[0].relationTypeName,
+                pojoRowAdapter.relationCollectors[0].relationTypeName.toJavaPoet(),
                 `is`(
                     ParameterizedTypeName.get(
                         ClassName.get(ArrayList::class.java),
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
index 702663d..93a15df 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
@@ -60,6 +60,19 @@
     }
 
     @Test
+    fun byteBuffer_disabledInDb() {
+        compile(
+            dbAnnotation = createTypeConvertersCode(
+                byteBuffer = DISABLED
+            )
+        ) {
+            hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val blob: ByteBuffer")
+            hasError(CANNOT_FIND_CURSOR_READER, "val blob: ByteBuffer")
+            hasErrorCount(2)
+        }
+    }
+
+    @Test
     fun all_disabledInDb_enabledInDao_enabledInEntity() {
         compile(
             dbAnnotation = createTypeConvertersCode(
@@ -84,16 +97,19 @@
         compile(
             entityAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             )
         ) {
             hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val uuid: UUID")
             hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val myEnum: MyEnum")
+            hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val blob: ByteBuffer")
             // even though it is enabled in dao or db, since pojo processing will visit the pojo,
-            // we'll still get erros for these because entity disabled them
+            // we'll still get errors for these because entity disabled them
             hasError(CANNOT_FIND_CURSOR_READER, "val uuid: UUID")
             hasError(CANNOT_FIND_CURSOR_READER, "val myEnum: MyEnum")
-            hasErrorCount(4)
+            hasError(CANNOT_FIND_CURSOR_READER, "val blob: ByteBuffer")
+            hasErrorCount(6)
         }
     }
 
@@ -102,15 +118,18 @@
         compile(
             dbAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             ),
             daoAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             ),
             entityAnnotation = createTypeConvertersCode(
                 enums = ENABLED,
-                uuid = ENABLED
+                uuid = ENABLED,
+                byteBuffer = ENABLED
             )
         ) {
             // success since we only fetch full objects.
@@ -184,7 +203,7 @@
         return Source.kotlin(
             "Foo.kt",
             """
-            import androidx.room.*
+            import androidx.room.*import java.nio.ByteBuffer
             import java.util.UUID
             enum class MyEnum {
                 VAL_1,
@@ -197,7 +216,8 @@
                 @PrimaryKey
                 val id:Int,
                 val uuid: UUID,
-                val myEnum: MyEnum
+                val myEnum: MyEnum,
+                val blob: ByteBuffer
             )
 
             $daoAnnotation
@@ -218,11 +238,13 @@
 
     private fun createTypeConvertersCode(
         enums: BuiltInTypeConverters.State? = null,
-        uuid: BuiltInTypeConverters.State? = null
+        uuid: BuiltInTypeConverters.State? = null,
+        byteBuffer: BuiltInTypeConverters.State? = null
     ): String {
         val builtIns = listOfNotNull(
             enums?.let { "enums = BuiltInTypeConverters.State.${enums.name}" },
             uuid?.let { "uuid = BuiltInTypeConverters.State.${uuid.name}" },
+            byteBuffer?.let { "byteBuffer = BuiltInTypeConverters.State.${byteBuffer.name}" },
         ).joinToString(",")
         return if (builtIns.isBlank()) {
             ""
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
index 84a98ce..45d6eab 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/testing/test_util.kt
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.ext.CollectionTypeNames
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -306,6 +307,20 @@
     val ROOM_DATABASE_KTX by lazy {
         loadKotlinCode("common/input/RoomDatabaseExt.kt")
     }
+
+    val LONG_SPARSE_ARRAY by lazy {
+        loadJavaCode(
+            "common/input/collection/LongSparseArray.java",
+            CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName
+        )
+    }
+
+    val ARRAY_MAP by lazy {
+        loadJavaCode(
+            "common/input/collection/ArrayMap.java",
+            CollectionTypeNames.ARRAY_MAP.canonicalName
+        )
+    }
 }
 
 fun testCodeGenScope(): CodeGenScope {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.kt
new file mode 100644
index 0000000..602825b
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.writer
+
+import androidx.room.DatabaseProcessingStep
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.processor.Context
+import java.io.File
+import loadTestSource
+import org.jetbrains.kotlin.config.JvmDefaultMode
+
+abstract class BaseDaoKotlinCodeGenTest {
+    protected fun getTestGoldenPath(testName: String): String {
+        return "kotlinCodeGen/$testName.kt"
+    }
+
+    protected fun runTest(
+        sources: List<Source>,
+        expectedFilePath: String,
+        compiledFiles: List<File> = emptyList(),
+        jvmDefaultMode: JvmDefaultMode = JvmDefaultMode.DEFAULT,
+        handler: (XTestInvocation) -> Unit = { }
+    ) {
+        runKspTest(
+            sources = sources,
+            classpath = compiledFiles,
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+            kotlincArguments = listOf("-Xjvm-default=${jvmDefaultMode.description}")
+        ) {
+            val databaseFqn = "androidx.room.Database"
+            DatabaseProcessingStep().process(
+                it.processingEnv,
+                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
+                it.roundEnv.isProcessingOver
+            )
+            it.assertCompilationResult {
+                this.generatedSource(
+                    loadTestSource(
+                        expectedFilePath,
+                        "MyDao_Impl"
+                    )
+                )
+                this.hasNoWarnings()
+            }
+            handler.invoke(it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
similarity index 85%
rename from room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
rename to room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index c58df04..8c1d50c 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -17,21 +17,15 @@
 package androidx.room.writer
 
 import COMMON
-import androidx.room.DatabaseProcessingStep
 import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.XTestInvocation
-import androidx.room.compiler.processing.util.runKspTest
-import androidx.room.processor.Context
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
-import loadTestSource
 import org.jetbrains.kotlin.config.JvmDefaultMode
 import org.junit.Test
 import org.junit.runner.RunWith
 
-// Dany's Kotlin codegen test playground (and tests too)
 @RunWith(TestParameterInjector::class)
-class KotlinCodeGenTest {
+class DaoKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
 
     val databaseSrc = Source.kotlin(
         "MyDatabase.kt",
@@ -397,7 +391,6 @@
             "MyDao.kt",
             """
             import androidx.room.*
-            import java.util.UUID
 
             @Dao
             interface MyDao {
@@ -437,7 +430,6 @@
             "MyDao.kt",
             """
             import androidx.room.*
-            import java.util.UUID
 
             @Dao
             interface MyDao {
@@ -473,6 +465,143 @@
     }
 
     @Test
+    fun pojoRowAdapter_customTypeConverter_provided() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val foo: Foo,
+            )
+
+            data class Foo(val data: String)
+
+            @ProvidedTypeConverter
+            class FooConverter(val default: String) {
+                @TypeConverter
+                fun fromString(data: String?): Foo = Foo(data ?: default)
+                @TypeConverter
+                fun toString(foo: Foo): String = foo.data
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun pojoRowAdapter_customTypeConverter_composite() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooBarConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val bar: Bar,
+            )
+
+            data class Foo(val data: String)
+            data class Bar(val data: String)
+
+            object FooBarConverter {
+                @TypeConverter
+                fun fromString(data: String): Foo = Foo(data)
+                @TypeConverter
+                fun toString(foo: Foo): String = foo.data
+
+                @TypeConverter
+                fun fromFoo(foo: Foo): Bar = Bar(foo.data)
+                @TypeConverter
+                fun toFoo(bar: Bar): Foo = Foo(bar.data)
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun pojoRowAdapter_customTypeConverter_nullAware() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooBarConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val foo: Foo,
+                val bar: Bar
+            )
+
+            data class Foo(val data: String)
+            data class Bar(val data: String)
+
+            object FooBarConverter {
+                @TypeConverter
+                fun fromString(data: String?): Foo? = data?.let { Foo(it) }
+                @TypeConverter
+                fun toString(foo: Foo?): String? = foo?.data
+
+                @TypeConverter
+                fun fromFoo(foo: Foo): Bar = Bar(foo.data)
+                @TypeConverter
+                fun toFoo(bar: Bar): Foo = Foo(bar.data)
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
     fun coroutineResultBinder() {
         val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
@@ -1009,37 +1138,30 @@
         )
     }
 
-    private fun getTestGoldenPath(testName: String): String {
-        return "kotlinCodeGen/$testName.kt"
-    }
+    @Test
+    fun abstractClassWithParam() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
 
-    private fun runTest(
-        sources: List<Source>,
-        expectedFilePath: String,
-        jvmDefaultMode: JvmDefaultMode = JvmDefaultMode.DEFAULT,
-        handler: (XTestInvocation) -> Unit = { }
-    ) {
-        runKspTest(
-            sources = sources,
-            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
-            kotlincArguments = listOf("-Xjvm-default=${jvmDefaultMode.description}")
-        ) {
-            val databaseFqn = "androidx.room.Database"
-            DatabaseProcessingStep().process(
-                it.processingEnv,
-                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
-                it.roundEnv.isProcessingOver
-            )
-            it.assertCompilationResult {
-                this.generatedSource(
-                    loadTestSource(
-                        expectedFilePath,
-                        "MyDao_Impl"
-                    )
-                )
-                this.hasNoWarnings()
+            @Dao
+            abstract class MyDao(val db: RoomDatabase) {
+              @Query("SELECT * FROM MyEntity")
+              abstract fun getEntity(): MyEntity
             }
-            handler.invoke(it)
-        }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
new file mode 100644
index 0000000..6e12f2c
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
@@ -0,0 +1,460 @@
+/*
+ * 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.writer
+
+import COMMON
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compileFiles
+import org.junit.Test
+
+class DaoRelationshipKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
+
+    val databaseSrc = Source.kotlin(
+        "MyDatabase.kt",
+        """
+        import androidx.room.*
+
+        @Database(
+            entities = [
+                Artist::class,
+                Song::class,
+                Playlist::class,
+                PlaylistSongXRef::class
+            ],
+            version = 1,
+            exportSchema = false
+        )
+        abstract class MyDatabase : RoomDatabase() {
+          abstract fun getDao(): MyDao
+        }
+        """.trimIndent()
+    )
+
+    @Test
+    fun relations() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_nullable() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist?
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long?
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_longSparseArray() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            compiledFiles = compileFiles(listOf(COMMON.LONG_SPARSE_ARRAY)),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_arrayMap() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            compiledFiles = compileFiles(listOf(COMMON.ARRAY_MAP)),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_byteBufferKey() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(
+                entities = [Artist::class, Song::class],
+                version = 1,
+                exportSchema = false
+            )
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            // To validate ByteBuffer converter is forced
+            @TypeConverters(
+                builtInTypeConverters = BuiltInTypeConverters(
+                    byteBuffer = BuiltInTypeConverters.State.DISABLED
+                )
+            )
+            interface MyDao {
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: ByteArray
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: ByteArray
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
new file mode 100644
index 0000000..5a51e79
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.writer
+
+import androidx.room.DatabaseProcessingStep
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.processor.Context
+import loadTestSource
+import org.junit.Test
+
+class DatabaseKotlinCodeGenTest {
+
+    @Test
+    fun database_simple() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [MyEntity::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun database_propertyDao() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [MyEntity::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract val dao: MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun database_withFtsAndView() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(
+                entities = [
+                    MyParentEntity::class,
+                    MyEntity::class,
+                    MyFtsEntity::class,
+                ],
+                views = [ MyView::class ],
+                version = 1,
+                exportSchema = false
+            )
+            abstract class MyDatabase : RoomDatabase() {
+              abstract val dao: MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyParentEntity(@PrimaryKey val parentKey: Long)
+
+            @Entity(
+                foreignKeys = [
+                    ForeignKey(
+                        entity = MyParentEntity::class,
+                        parentColumns = ["parentKey"],
+                        childColumns = ["indexedCol"],
+                        onDelete = ForeignKey.CASCADE
+                    )
+                ],
+                indices = [Index("indexedCol")]
+            )
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val indexedCol: String
+            )
+
+            @Fts4
+            @Entity
+            data class MyFtsEntity(
+                @PrimaryKey
+                @ColumnInfo(name = "rowid")
+                val pk: Int,
+                val text: String
+            )
+
+            @DatabaseView("SELECT text FROM MyFtsEntity")
+            data class MyView(val text: String)
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    private fun getTestGoldenPath(testName: String): String {
+        return "kotlinCodeGen/$testName.kt"
+    }
+
+    private fun runTest(
+        sources: List<Source>,
+        expectedFilePath: String,
+        handler: (XTestInvocation) -> Unit = { }
+    ) {
+        runKspTest(
+            sources = sources,
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+        ) {
+            val databaseFqn = "androidx.room.Database"
+            DatabaseProcessingStep().process(
+                it.processingEnv,
+                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
+                it.roundEnv.isProcessingOver
+            )
+            it.assertCompilationResult {
+                this.generatedSource(
+                    loadTestSource(
+                        expectedFilePath,
+                        "MyDatabase_Impl"
+                    )
+                )
+                this.hasNoWarnings()
+            }
+            handler.invoke(it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java b/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java
new file mode 100644
index 0000000..2b7666f
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java
@@ -0,0 +1,111 @@
+package androidx.collection;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class ArrayMap<K, V> implements Map<K, V> {
+    public ArrayMap() {
+
+    }
+
+    public ArrayMap(int capacity) {
+
+    }
+
+    @Override
+    public void putAll(@NonNull Map<? extends K, ? extends V> map) {
+
+    }
+
+    @NonNull
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public Set<K> keySet() {
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public Collection<V> values() {
+        return null;
+    }
+
+    @Override
+    public void clear() {
+
+    }
+
+    @Override
+    public boolean containsKey(@Nullable Object key) {
+        return false;
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V get(Object key) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V getOrDefault(Object key, V defaultValue) {
+        return null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V put(K key, V value) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V putIfAbsent(K key, V value) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V remove(Object key) {
+        return null;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V replace(K key, V value) {
+        return null;
+    }
+
+    @Override
+    public boolean replace(K key, V oldValue, V newValue) {
+        return false;
+    }
+
+    @Override
+    public int size() {
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java b/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java
new file mode 100644
index 0000000..3e48191
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java
@@ -0,0 +1,52 @@
+package androidx.collection;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+
+public class LongSparseArray<E> {
+
+    public LongSparseArray() {
+
+    }
+
+    public LongSparseArray(int initialCapacity) {
+
+    }
+
+    @Nullable
+    public E get(long key) {
+        return null;
+    }
+
+    public void put(long key, E value) {
+
+    }
+
+    public void putAll(@NonNull LongSparseArray<? extends E> other) {
+
+    }
+
+    public int size() {
+        return 0;
+    }
+
+    public boolean isEmpty() {
+        return false;
+    }
+
+    public long keyAt(int index) {
+        return 0;
+    }
+
+    public E valueAt(int index) {
+        return null;
+    }
+
+    public boolean containsKey(long key) {
+        return false;
+    }
+
+    public void clear() {
+
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
index c4529bd..f56e512 100644
--- a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
+++ b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
@@ -3,26 +3,20 @@
 import androidx.annotation.NonNull;
 import androidx.room.DatabaseConfiguration;
 import androidx.room.InvalidationTracker;
+import androidx.room.RoomDatabase;
 import androidx.room.RoomOpenHelper;
-import androidx.room.RoomOpenHelper.Delegate;
-import androidx.room.RoomOpenHelper.ValidationResult;
 import androidx.room.migration.AutoMigrationSpec;
 import androidx.room.migration.Migration;
 import androidx.room.util.DBUtil;
 import androidx.room.util.TableInfo;
-import androidx.room.util.TableInfo.Column;
-import androidx.room.util.TableInfo.ForeignKey;
-import androidx.room.util.TableInfo.Index;
 import androidx.room.util.ViewInfo;
 import androidx.sqlite.db.SupportSQLiteDatabase;
 import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback;
-import androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration;
 import java.lang.Class;
 import java.lang.Override;
 import java.lang.String;
 import java.lang.SuppressWarnings;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -36,62 +30,68 @@
     private volatile ComplexDao _complexDao;
 
     @Override
-    protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
-        final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1923) {
+    @NonNull
+    protected SupportSQLiteOpenHelper createOpenHelper(@NonNull final DatabaseConfiguration config) {
+        final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(config, new RoomOpenHelper.Delegate(1923) {
             @Override
-            public void createAllTables(SupportSQLiteDatabase _db) {
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `Child1` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `Child2` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
-                _db.execSQL("CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
-                _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12b646c55443feeefb567521e2bece85')");
+            public void createAllTables(@NonNull final SupportSQLiteDatabase db) {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
+                db.execSQL("CREATE TABLE IF NOT EXISTS `Child1` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
+                db.execSQL("CREATE TABLE IF NOT EXISTS `Child2` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
+                db.execSQL("CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12b646c55443feeefb567521e2bece85')");
             }
 
             @Override
-            public void dropAllTables(SupportSQLiteDatabase _db) {
-                _db.execSQL("DROP TABLE IF EXISTS `User`");
-                _db.execSQL("DROP TABLE IF EXISTS `Child1`");
-                _db.execSQL("DROP TABLE IF EXISTS `Child2`");
-                _db.execSQL("DROP VIEW IF EXISTS `UserSummary`");
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onDestructiveMigration(_db);
+            public void dropAllTables(@NonNull final SupportSQLiteDatabase db) {
+                db.execSQL("DROP TABLE IF EXISTS `User`");
+                db.execSQL("DROP TABLE IF EXISTS `Child1`");
+                db.execSQL("DROP TABLE IF EXISTS `Child2`");
+                db.execSQL("DROP VIEW IF EXISTS `UserSummary`");
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onDestructiveMigration(db);
                     }
                 }
             }
 
             @Override
-            public void onCreate(SupportSQLiteDatabase _db) {
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onCreate(_db);
+            public void onCreate(@NonNull final SupportSQLiteDatabase db) {
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onCreate(db);
                     }
                 }
             }
 
             @Override
-            public void onOpen(SupportSQLiteDatabase _db) {
-                mDatabase = _db;
-                internalInitInvalidationTracker(_db);
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onOpen(_db);
+            public void onOpen(@NonNull final SupportSQLiteDatabase db) {
+                mDatabase = db;
+                internalInitInvalidationTracker(db);
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onOpen(db);
                     }
                 }
             }
 
             @Override
-            public void onPreMigrate(SupportSQLiteDatabase _db) {
-                DBUtil.dropFtsSyncTriggers(_db);
+            public void onPreMigrate(@NonNull final SupportSQLiteDatabase db) {
+                DBUtil.dropFtsSyncTriggers(db);
             }
 
             @Override
-            public void onPostMigrate(SupportSQLiteDatabase _db) {
+            public void onPostMigrate(@NonNull final SupportSQLiteDatabase db) {
             }
 
             @Override
-            public RoomOpenHelper.ValidationResult onValidateSchema(SupportSQLiteDatabase _db) {
+            @NonNull
+            public RoomOpenHelper.ValidationResult onValidateSchema(
+                    @NonNull final SupportSQLiteDatabase db) {
                 final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
                 _columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsUser.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
@@ -100,8 +100,8 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
-                final TableInfo _existingUser = TableInfo.read(_db, "User");
-                if (! _infoUser.equals(_existingUser)) {
+                final TableInfo _existingUser = TableInfo.read(db, "User");
+                if (!_infoUser.equals(_existingUser)) {
                     return new RoomOpenHelper.ValidationResult(false, "User(foo.bar.User).\n"
                             + " Expected:\n" + _infoUser + "\n"
                             + " Found:\n" + _existingUser);
@@ -114,8 +114,8 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild1 = new TableInfo("Child1", _columnsChild1, _foreignKeysChild1, _indicesChild1);
-                final TableInfo _existingChild1 = TableInfo.read(_db, "Child1");
-                if (! _infoChild1.equals(_existingChild1)) {
+                final TableInfo _existingChild1 = TableInfo.read(db, "Child1");
+                if (!_infoChild1.equals(_existingChild1)) {
                     return new RoomOpenHelper.ValidationResult(false, "Child1(foo.bar.Child1).\n"
                             + " Expected:\n" + _infoChild1 + "\n"
                             + " Found:\n" + _existingChild1);
@@ -128,15 +128,15 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild2 = new TableInfo("Child2", _columnsChild2, _foreignKeysChild2, _indicesChild2);
-                final TableInfo _existingChild2 = TableInfo.read(_db, "Child2");
-                if (! _infoChild2.equals(_existingChild2)) {
+                final TableInfo _existingChild2 = TableInfo.read(db, "Child2");
+                if (!_infoChild2.equals(_existingChild2)) {
                     return new RoomOpenHelper.ValidationResult(false, "Child2(foo.bar.Child2).\n"
                             + " Expected:\n" + _infoChild2 + "\n"
                             + " Found:\n" + _existingChild2);
                 }
                 final ViewInfo _infoUserSummary = new ViewInfo("UserSummary", "CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
-                final ViewInfo _existingUserSummary = ViewInfo.read(_db, "UserSummary");
-                if (! _infoUserSummary.equals(_existingUserSummary)) {
+                final ViewInfo _existingUserSummary = ViewInfo.read(db, "UserSummary");
+                if (!_infoUserSummary.equals(_existingUserSummary)) {
                     return new RoomOpenHelper.ValidationResult(false, "UserSummary(foo.bar.UserSummary).\n"
                             + " Expected:\n" + _infoUserSummary + "\n"
                             + " Found:\n" + _existingUserSummary);
@@ -144,19 +144,17 @@
                 return new RoomOpenHelper.ValidationResult(true, null);
             }
         }, "12b646c55443feeefb567521e2bece85", "2f1dbf49584f5d6c91cb44f8a6ecfee2");
-        final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
-                .name(configuration.name)
-                .callback(_openCallback)
-                .build();
-        final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
+        final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build();
+        final SupportSQLiteOpenHelper _helper = config.sqliteOpenHelperFactory.create(_sqliteConfig);
         return _helper;
     }
 
     @Override
+    @NonNull
     protected InvalidationTracker createInvalidationTracker() {
         final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
-        HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
-        HashSet<String> _tables = new HashSet<String>(1);
+        final HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
+        final HashSet<String> _tables = new HashSet<String>(1);
         _tables.add("User");
         _viewTables.put("usersummary", _tables);
         return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User","Child1","Child2");
@@ -182,6 +180,7 @@
     }
 
     @Override
+    @NonNull
     protected Map<Class<?>, List<Class<?>>> getRequiredTypeConverters() {
         final HashMap<Class<?>, List<Class<?>>> _typeConvertersMap = new HashMap<Class<?>, List<Class<?>>>();
         _typeConvertersMap.put(ComplexDao.class, ComplexDao_Impl.getRequiredConverters());
@@ -189,15 +188,18 @@
     }
 
     @Override
+    @NonNull
     public Set<Class<? extends AutoMigrationSpec>> getRequiredAutoMigrationSpecs() {
         final HashSet<Class<? extends AutoMigrationSpec>> _autoMigrationSpecsSet = new HashSet<Class<? extends AutoMigrationSpec>>();
         return _autoMigrationSpecsSet;
     }
 
     @Override
+    @NonNull
     public List<Migration> getAutoMigrations(
-            @NonNull Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> autoMigrationSpecsMap) {
-        return Arrays.asList();
+            @NonNull final Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> autoMigrationSpecs) {
+        final List<Migration> _autoMigrations = new ArrayList<Migration>();
+        return _autoMigrations;
     }
 
     @Override
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
new file mode 100644
index 0000000..52f9323
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
@@ -0,0 +1,51 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao(__db) {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                _result = MyEntity(_tmpPk)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
index 2d6cc5f..83e8aee 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
@@ -19,10 +19,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
index 11211aa..ebeb1fe 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
@@ -14,10 +14,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
index 2a605b7..c2c2d0d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
@@ -18,10 +18,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
index a23b4769..629b4d5 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
@@ -18,10 +18,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt
new file mode 100644
index 0000000..75cb1b2
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt
@@ -0,0 +1,150 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    public override val dao: MyDao
+        get() = _myDao.value
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '195d7974660177325bd1a32d2c7b8b8c')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        try {
+            super.beginTransaction()
+            _db.execSQL("DELETE FROM `MyEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
new file mode 100644
index 0000000..e22f870
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
@@ -0,0 +1,149 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '195d7974660177325bd1a32d2c7b8b8c')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        try {
+            super.beginTransaction()
+            _db.execSQL("DELETE FROM `MyEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+
+    public override fun getDao(): MyDao = _myDao.value
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
new file mode 100644
index 0000000..fa8e301
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
@@ -0,0 +1,230 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.FtsTableInfo
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.ViewInfo
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Boolean
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    public override val dao: MyDao
+        get() = _myDao.value
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyParentEntity` (`parentKey` INTEGER NOT NULL, PRIMARY KEY(`parentKey`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, `indexedCol` TEXT NOT NULL, PRIMARY KEY(`pk`), FOREIGN KEY(`indexedCol`) REFERENCES `MyParentEntity`(`parentKey`) ON UPDATE NO ACTION ON DELETE CASCADE )")
+                db.execSQL("CREATE INDEX IF NOT EXISTS `index_MyEntity_indexedCol` ON `MyEntity` (`indexedCol`)")
+                db.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
+                db.execSQL("CREATE VIEW `MyView` AS SELECT text FROM MyFtsEntity")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89ba16fb8b062b50acf0eb06c853efcb')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyParentEntity`")
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                db.execSQL("DROP TABLE IF EXISTS `MyFtsEntity`")
+                db.execSQL("DROP VIEW IF EXISTS `MyView`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                db.execSQL("PRAGMA foreign_keys = ON")
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyParentEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyParentEntity.put("parentKey", TableInfo.Column("parentKey", "INTEGER", true, 1,
+                    null, TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyParentEntity: HashSet<TableInfo.ForeignKey> =
+                    HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyParentEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyParentEntity: TableInfo = TableInfo("MyParentEntity", _columnsMyParentEntity,
+                    _foreignKeysMyParentEntity, _indicesMyParentEntity)
+                val _existingMyParentEntity: TableInfo = read(db, "MyParentEntity")
+                if (!_infoMyParentEntity.equals(_existingMyParentEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyParentEntity(MyParentEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyParentEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyParentEntity)
+                }
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(2)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                _columnsMyEntity.put("indexedCol", TableInfo.Column("indexedCol", "TEXT", true, 0, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(1)
+                _foreignKeysMyEntity.add(TableInfo.ForeignKey("MyParentEntity", "CASCADE", "NO ACTION",
+                    listOf("indexedCol"), listOf("parentKey")))
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(1)
+                _indicesMyEntity.add(TableInfo.Index("index_MyEntity_indexedCol", false,
+                    listOf("indexedCol"), listOf("ASC")))
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                val _columnsMyFtsEntity: HashSet<String> = HashSet<String>(2)
+                _columnsMyFtsEntity.add("text")
+                val _infoMyFtsEntity: FtsTableInfo = FtsTableInfo("MyFtsEntity", _columnsMyFtsEntity,
+                    "CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
+                val _existingMyFtsEntity: FtsTableInfo = FtsTableInfo.Companion.read(db, "MyFtsEntity")
+                if (!_infoMyFtsEntity.equals(_existingMyFtsEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyFtsEntity(MyFtsEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyFtsEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyFtsEntity)
+                }
+                val _infoMyView: ViewInfo = ViewInfo("MyView",
+                    "CREATE VIEW `MyView` AS SELECT text FROM MyFtsEntity")
+                val _existingMyView: ViewInfo = ViewInfo.Companion.read(db, "MyView")
+                if (!_infoMyView.equals(_existingMyView)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyView(MyView).
+                  | Expected:
+                  |""".trimMargin() + _infoMyView + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyView)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "89ba16fb8b062b50acf0eb06c853efcb", "8a71a68e07bdd62aa8c8324d870cf804")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(1)
+        _shadowTablesMap.put("MyFtsEntity", "MyFtsEntity_content")
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(1)
+        val _tables: HashSet<String> = HashSet<String>(1)
+        _tables.add("MyFtsEntity")
+        _viewTables.put("myview", _tables)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables,
+            "MyParentEntity","MyEntity","MyFtsEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        val _supportsDeferForeignKeys: Boolean = android.os.Build.VERSION.SDK_INT >=
+            android.os.Build.VERSION_CODES.LOLLIPOP
+        try {
+            if (!_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA foreign_keys = FALSE")
+            }
+            super.beginTransaction()
+            if (_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA defer_foreign_keys = TRUE")
+            }
+            _db.execSQL("DELETE FROM `MyParentEntity`")
+            _db.execSQL("DELETE FROM `MyEntity`")
+            _db.execSQL("DELETE FROM `MyFtsEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            if (!_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA foreign_keys = TRUE")
+            }
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
index 74c1fe5..da53b2a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
@@ -17,12 +17,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __preparedStmtOfInsertEntity: SharedSQLiteStatement
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__preparedStmtOfInsertEntity = object : SharedSQLiteStatement(__db) {
             public override fun createQuery(): String {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
index 4fdc4e8..3e6fa5a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
@@ -15,10 +15,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
index da2116d4..376ee20 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
@@ -12,14 +12,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __deletionAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
 
     private val __updateAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__deletionAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
             public override fun createQuery(): String = "DELETE FROM `MyEntity` WHERE `pk` = ?"
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
index 7e60516..8b0298b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
@@ -14,14 +14,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
 
     private val __upsertionAdapterOfMyEntity: EntityUpsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
index 576955b..d647d87 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
index 5fe939f..259d8b0 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
index e63b2d2..418314d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
@@ -17,14 +17,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
 
     private val __fooConverter: FooConverter = FooConverter()
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
new file mode 100644
index 0000000..d717c5e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
@@ -0,0 +1,84 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`bar`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: Foo = FooBarConverter.toFoo(entity.bar)
+                val _tmp_1: String = FooBarConverter.toString(_tmp)
+                statement.bindString(2, _tmp_1)
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpBar: Bar
+                val _tmp: String
+                _tmp = _cursor.getString(_cursorIndexOfBar)
+                val _tmp_1: Foo = FooBarConverter.fromString(_tmp)
+                _tmpBar = FooBarConverter.fromFoo(_tmp_1)
+                _result = MyEntity(_tmpPk,_tmpBar)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
new file mode 100644
index 0000000..792a573
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
@@ -0,0 +1,122 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`,`bar`) VALUES (?,?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String? = FooBarConverter.toString(entity.foo)
+                if (_tmp == null) {
+                    statement.bindNull(2)
+                } else {
+                    statement.bindString(2, _tmp)
+                }
+                val _tmp_1: Foo = FooBarConverter.toFoo(entity.bar)
+                val _tmp_2: String? = FooBarConverter.toString(_tmp_1)
+                if (_tmp_2 == null) {
+                    statement.bindNull(3)
+                } else {
+                    statement.bindString(3, _tmp_2)
+                }
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo
+                val _tmp: String?
+                if (_cursor.isNull(_cursorIndexOfFoo)) {
+                    _tmp = null
+                } else {
+                    _tmp = _cursor.getString(_cursorIndexOfFoo)
+                }
+                val _tmp_1: Foo? = FooBarConverter.fromString(_tmp)
+                if (_tmp_1 == null) {
+                    error("Expected non-null Foo, but it was null.")
+                } else {
+                    _tmpFoo = _tmp_1
+                }
+                val _tmpBar: Bar
+                val _tmp_2: String?
+                if (_cursor.isNull(_cursorIndexOfBar)) {
+                    _tmp_2 = null
+                } else {
+                    _tmp_2 = _cursor.getString(_cursorIndexOfBar)
+                }
+                val _tmp_3: Foo? = FooBarConverter.fromString(_tmp_2)
+                val _tmp_4: Bar?
+                if (_tmp_3 == null) {
+                    _tmp_4 = null
+                } else {
+                    _tmp_4 = FooBarConverter.fromFoo(_tmp_3)
+                }
+                if (_tmp_4 == null) {
+                    error("Expected non-null Bar, but it was null.")
+                } else {
+                    _tmpBar = _tmp_4
+                }
+                _result = MyEntity(_tmpPk,_tmpFoo,_tmpBar)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
new file mode 100644
index 0000000..3e7273a
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
@@ -0,0 +1,90 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+
+    private val __fooConverter: Lazy<FooConverter> = lazy {
+        checkNotNull(__db.getTypeConverter(FooConverter::class.java))
+    }
+
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String = __fooConverter().toString(entity.foo)
+                statement.bindString(2, _tmp)
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo
+                val _tmp: String
+                _tmp = _cursor.getString(_cursorIndexOfFoo)
+                _tmpFoo = __fooConverter().fromString(_tmp)
+                _result = MyEntity(_tmpPk,_tmpFoo)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fooConverter(): FooConverter = __fooConverter.value
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = listOf(FooConverter::class.java)
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt
new file mode 100644
index 0000000..ac5044e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt
@@ -0,0 +1,91 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String = FooConverter.nullableFooToString(entity.foo)
+                if (_tmp == null) {
+                    statement.bindNull(2)
+                } else {
+                    statement.bindString(2, _tmp)
+                }
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo?
+                val _tmp: String?
+                if (_cursor.isNull(_cursorIndexOfFoo)) {
+                    _tmp = null
+                } else {
+                    _tmp = _cursor.getString(_cursorIndexOfFoo)
+                }
+                val _tmp_1: Foo = FooConverter.nullableStringToFoo(_tmp)
+                _tmpFoo = _tmp_1
+                _result = MyEntity(_tmpPk,_tmpFoo)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
index 104d7ff..0326a4c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
index eb1927d..8eca87d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
index accb714..7180121 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
index 6c30089..f1b3503 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
@@ -23,12 +23,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
index 74db90a..f1a820e 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
@@ -23,12 +23,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
index 32d3380..4a47bc6 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
@@ -17,12 +17,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
index b0f3f3b..5b24625 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
@@ -20,12 +20,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index 0b825dd..b34fd6a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
@@ -17,12 +17,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
index cafdfc6..e5e7baa 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
@@ -15,10 +15,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
index b545e8b..cd5c218 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
@@ -13,7 +13,9 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __preparedStmtOfInsertEntity: SharedSQLiteStatement
@@ -23,8 +25,7 @@
     private val __preparedStmtOfUpdateEntityReturnInt: SharedSQLiteStatement
 
     private val __preparedStmtOfDeleteEntity: SharedSQLiteStatement
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__preparedStmtOfInsertEntity = object : SharedSQLiteStatement(__db) {
             public override fun createQuery(): String {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
index 98c21c1..11a7cef 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
@@ -16,10 +16,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
index 688d74c..b42aaa4 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
@@ -13,10 +13,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
new file mode 100644
index 0000000..4e5ad57
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
@@ -0,0 +1,307 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
new file mode 100644
index 0000000..56c6b76
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
@@ -0,0 +1,307 @@
+import android.database.Cursor
+import androidx.collection.ArrayMap
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchArrayMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: ArrayMap<Long, Artist?> = ArrayMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: ArrayMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: ArrayMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: ArrayMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
new file mode 100644
index 0000000..498e033
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
@@ -0,0 +1,129 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.nio.ByteBuffer
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.ByteArray
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<ByteBuffer, Artist?> = HashMap<ByteBuffer, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: ByteBuffer
+                _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_cursorIndexOfArtistKey))
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: ByteArray
+                _tmpArtistKey = _cursor.getBlob(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: ByteBuffer
+                _tmpKey_1 = ByteBuffer.wrap(_cursor.getBlob(_cursorIndexOfArtistKey))
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<ByteBuffer, Artist?>): Unit {
+        val __mapKeySet: Set<ByteBuffer> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: ByteBuffer in __mapKeySet) {
+            _stmt.bindBlob(_argIndex, _item.array())
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: ByteBuffer
+                _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_itemKeyIndex))
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: ByteArray
+                    _tmpArtistId = _cursor.getBlob(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
new file mode 100644
index 0000000..f1b082d7
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
@@ -0,0 +1,306 @@
+import android.database.Cursor
+import androidx.collection.LongSparseArray
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchLongSparseArray
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: LongSparseArray<Artist?> = LongSparseArray<Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: LongSparseArray<Artist?>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: LongSparseArray<ArrayList<Song>>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: LongSparseArray<ArrayList<Song>>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
new file mode 100644
index 0000000..a01fe75
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
@@ -0,0 +1,336 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpKey = null
+                } else {
+                    _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                if (_tmpKey != null) {
+                    _collectionArtist.put(_tmpKey, null)
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpArtistKey = null
+                } else {
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpKey_1 = null
+                } else {
+                    _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                if (_tmpKey_1 != null) {
+                    _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                } else {
+                    _tmpArtist = null
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist?
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long?
+                if (_cursor.isNull(_itemKeyIndex)) {
+                    _tmpKey = null
+                } else {
+                    _tmpKey = _cursor.getLong(_itemKeyIndex)
+                }
+                if (_tmpKey != null) {
+                    val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                    if (_tmpRelation != null) {
+                        val _item_1: Song
+                        val _tmpSongId: Long
+                        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                        val _tmpArtistKey: Long?
+                        if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                            _tmpArtistKey = null
+                        } else {
+                            _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                        }
+                        _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                        _tmpRelation.add(_item_1)
+                    }
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long?
+                    if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                        _tmpArtistKey = null
+                    } else {
+                        _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    }
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
index 3658d37..fcd26fa 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -9,10 +9,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao() {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
index 15e154e..2975e78 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
@@ -13,10 +13,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
index a65fb24..eb7e8ba 100644
--- a/room/room-runtime/api/restricted_current.ignore
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -1,3 +1,17 @@
 // Baseline format: 1.0
 InvalidNullConversion: androidx.room.EntityInsertionAdapter#bind(androidx.sqlite.db.SupportSQLiteStatement, T) parameter #0:
     Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter statement in androidx.room.EntityInsertionAdapter.bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity)
+
+
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#createAllTables(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.createAllTables
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.dropAllTables
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onCreate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onCreate
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onOpen(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onOpen
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onPostMigrate
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onPreMigrate
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 715749d..3f55e9d 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -211,12 +211,12 @@
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class RoomOpenHelper.Delegate {
     ctor public RoomOpenHelper.Delegate(int version);
-    method public abstract void createAllTables(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void onOpen(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public void onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public void onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase database);
+    method public abstract void createAllTables(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onOpen(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase db);
     method public androidx.room.RoomOpenHelper.ValidationResult onValidateSchema(androidx.sqlite.db.SupportSQLiteDatabase db);
     method @Deprecated protected void validateMigration(androidx.sqlite.db.SupportSQLiteDatabase db);
     field public final int version;
@@ -341,6 +341,12 @@
     method public androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
   }
 
+  @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class RelationUtil {
+    method public static <K, V> void recursiveFetchArrayMap(androidx.collection.ArrayMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.ArrayMap<K,V>,kotlin.Unit> fetchBlock);
+    method public static <K, V> void recursiveFetchHashMap(java.util.HashMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super java.util.HashMap<K,V>,kotlin.Unit> fetchBlock);
+    method public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
+  }
+
   @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class StringUtil {
     method public static void appendPlaceholders(StringBuilder builder, int count);
     method public static String? joinIntoString(java.util.List<java.lang.Integer>? input);
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index c9153f0..49b6c29 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -40,6 +40,7 @@
     api(project(":sqlite:sqlite-framework"))
     api(project(":sqlite:sqlite"))
     implementation("androidx.arch.core:core-runtime:2.0.1")
+    compileOnly("androidx.collection:collection:1.2.0")
     compileOnly("androidx.paging:paging-common:2.0.0")
     compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
     implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
diff --git a/room/room-runtime/src/main/java/androidx/room/Room.kt b/room/room-runtime/src/main/java/androidx/room/Room.kt
index fcd1527..3aa15d5 100644
--- a/room/room-runtime/src/main/java/androidx/room/Room.kt
+++ b/room/room-runtime/src/main/java/androidx/room/Room.kt
@@ -61,11 +61,11 @@
             )
         } catch (e: IllegalAccessException) {
             throw RuntimeException(
-                "Cannot access the constructor $klass.canonicalName"
+                "Cannot access the constructor ${klass.canonicalName}"
             )
         } catch (e: InstantiationException) {
             throw RuntimeException(
-                "Failed to create an instance of $klass.canonicalName"
+                "Failed to create an instance of ${klass.canonicalName}"
             )
         }
     }
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
index c3008b3..929dcca 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
@@ -179,10 +179,10 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     abstract class Delegate(@JvmField val version: Int) {
-        abstract fun dropAllTables(database: SupportSQLiteDatabase)
-        abstract fun createAllTables(database: SupportSQLiteDatabase)
-        abstract fun onOpen(database: SupportSQLiteDatabase)
-        abstract fun onCreate(database: SupportSQLiteDatabase)
+        abstract fun dropAllTables(db: SupportSQLiteDatabase)
+        abstract fun createAllTables(db: SupportSQLiteDatabase)
+        abstract fun onOpen(db: SupportSQLiteDatabase)
+        abstract fun onCreate(db: SupportSQLiteDatabase)
 
         /**
          * Called after a migration run to validate database integrity.
@@ -209,13 +209,13 @@
          * Called before migrations execute to perform preliminary work.
          * @param database The SQLite database.
          */
-        open fun onPreMigrate(database: SupportSQLiteDatabase) {}
+        open fun onPreMigrate(db: SupportSQLiteDatabase) {}
 
         /**
          * Called after migrations execute to perform additional work.
          * @param database The SQLite database.
          */
-        open fun onPostMigrate(database: SupportSQLiteDatabase) {}
+        open fun onPostMigrate(db: SupportSQLiteDatabase) {}
     }
 
     /**
diff --git a/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt b/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt
new file mode 100644
index 0000000..cefa581
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+
+@file:JvmName("RelationUtil")
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.collection.ArrayMap
+import androidx.collection.LongSparseArray
+import androidx.room.RoomDatabase
+
+/**
+ * Utility function used in generated code to recursively fetch relationships when the amount of
+ * keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT].
+ *
+ * @param map - The map containing the relationship keys to fill-in.
+ * @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
+ * @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
+ */
+fun <K : Any, V> recursiveFetchHashMap(
+    map: HashMap<K, V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (HashMap<K, V>) -> Unit
+) {
+    val tmpMap = HashMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    for (key in map.keys) {
+        // Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa
+        @Suppress("UNCHECKED_CAST")
+        if (isRelationCollection) {
+            tmpMap[key] = map[key] as V
+        } else {
+            tmpMap[key] = null as V
+        }
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            // recursively load that batch
+            fetchBlock(tmpMap)
+            // for non collection relation, put the loaded batch in the original map,
+            // not needed when dealing with collections since references are passed
+            if (!isRelationCollection) {
+                map.putAll(tmpMap)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        // load the last batch
+        fetchBlock(tmpMap)
+        // for non collection relation, put the last batch in the original map
+        if (!isRelationCollection) {
+            map.putAll(tmpMap)
+        }
+    }
+}
+
+/**
+ * Same as [recursiveFetchHashMap] but for [LongSparseArray].
+ */
+fun <V> recursiveFetchLongSparseArray(
+    map: LongSparseArray<V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (LongSparseArray<V>) -> Unit
+) {
+    val tmpMap = LongSparseArray<V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    var mapIndex = 0
+    val limit = map.size()
+    while (mapIndex < limit) {
+        if (isRelationCollection) {
+            tmpMap.put(map.keyAt(mapIndex), map.valueAt(mapIndex))
+        } else {
+            // Safe because `V` is a nullable type arg when isRelationCollection == false
+            @Suppress("UNCHECKED_CAST")
+            tmpMap.put(map.keyAt(mapIndex), null as V)
+        }
+        mapIndex++
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            fetchBlock(tmpMap)
+            if (!isRelationCollection) {
+                map.putAll(tmpMap)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        fetchBlock(tmpMap)
+        if (!isRelationCollection) {
+            map.putAll(tmpMap)
+        }
+    }
+}
+
+/**
+ * Same as [recursiveFetchHashMap] but for [ArrayMap].
+ */
+fun <K : Any, V> recursiveFetchArrayMap(
+    map: ArrayMap<K, V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (ArrayMap<K, V>) -> Unit
+) {
+    val tmpMap = ArrayMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    var mapIndex = 0
+    val limit = map.size
+    while (mapIndex < limit) {
+        if (isRelationCollection) {
+            tmpMap[map.keyAt(mapIndex)] = map.valueAt(mapIndex)
+        } else {
+            tmpMap[map.keyAt(mapIndex)] = null
+        }
+        mapIndex++
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            fetchBlock(tmpMap)
+            if (!isRelationCollection) {
+                // Cast needed to disambiguate from putAll(SimpleArrayMap)
+                map.putAll(tmpMap as Map<K, V>)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        fetchBlock(tmpMap)
+        if (!isRelationCollection) {
+            // Cast needed to disambiguate from putAll(SimpleArrayMap)
+            map.putAll(tmpMap as Map<K, V>)
+        }
+    }
+}
diff --git a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
index 0f33171..1627a03 100644
--- a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
+++ b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
@@ -458,7 +458,7 @@
         databaseBundle: DatabaseBundle,
         private val mVerifyDroppedTables: Boolean
     ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(database: SupportSQLiteDatabase) {
+        override fun createAllTables(db: SupportSQLiteDatabase) {
             throw UnsupportedOperationException(
                 "Was expecting to migrate but received create." +
                     "Make sure you have created the database first."
@@ -547,9 +547,9 @@
     internal class CreatingDelegate(
         databaseBundle: DatabaseBundle
     ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(database: SupportSQLiteDatabase) {
+        override fun createAllTables(db: SupportSQLiteDatabase) {
             mDatabaseBundle.buildCreateQueries().forEach { query ->
-                database.execSQL(query)
+                db.execSQL(query)
             }
         }
 
@@ -567,12 +567,12 @@
     ) : RoomOpenHelper.Delegate(
             mDatabaseBundle.version
         ) {
-        override fun dropAllTables(database: SupportSQLiteDatabase) {
+        override fun dropAllTables(db: SupportSQLiteDatabase) {
             throw UnsupportedOperationException("cannot drop all tables in the test")
         }
 
-        override fun onCreate(database: SupportSQLiteDatabase) {}
-        override fun onOpen(database: SupportSQLiteDatabase) {}
+        override fun onCreate(db: SupportSQLiteDatabase) {}
+        override fun onOpen(db: SupportSQLiteDatabase) {}
     }
 
     internal companion object {
diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle
index c282a18..cc2a5a6 100644
--- a/samples/Support4Demos/build.gradle
+++ b/samples/Support4Demos/build.gradle
@@ -15,6 +15,9 @@
     implementation(project(":viewpager:viewpager"))
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesAndroid)
+    implementation(project(":coordinatorlayout:coordinatorlayout"))
+    implementation("com.google.android.material:material:1.6.0")
+    implementation(project(":appcompat:appcompat"))
 }
 
 android {
diff --git a/samples/Support4Demos/src/main/AndroidManifest.xml b/samples/Support4Demos/src/main/AndroidManifest.xml
index a757919..6865741 100644
--- a/samples/Support4Demos/src/main/AndroidManifest.xml
+++ b/samples/Support4Demos/src/main/AndroidManifest.xml
@@ -427,6 +427,17 @@
         </activity>
 
         <activity
+            android:name=".widget.NestedScrollActivity3LevelsWithCollapsingToolbar"
+            android:exported="true"
+            android:theme="@style/Theme.AppCompat.Light"
+            android:label="@string/nested_scroll_3_levels_collapsing">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
+        <activity
             android:name=".graphics.RoundedBitmapDrawableActivity"
             android:exported="true"
             android:label="Graphics/RoundedBitmapDrawable">
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
index b484606..6792cfb 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
@@ -26,10 +26,10 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.webkit.WebView;
-import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.appcompat.widget.AppCompatEditText;
 import androidx.core.view.inputmethod.EditorInfoCompat;
 import androidx.core.view.inputmethod.InputConnectionCompat;
 import androidx.core.view.inputmethod.InputContentInfoCompat;
@@ -188,17 +188,17 @@
     }
 
     /**
-     * Creates a new instance of {@link EditText} that is configured to specify the given content
-     * MIME types to {@link EditorInfo#contentMimeTypes} so that developers
+     * Creates a new instance of {@link AppCompatEditText} that is configured to specify the given
+     * content MIME types to {@link EditorInfo#contentMimeTypes} so that developers
      * can locally test how the current input method behaves for such content MIME types.
      *
      * @param contentMimeTypes A {@link String} array that indicates the supported content MIME
      *                         types
-     * @return a new instance of {@link EditText}, which specifies
+     * @return a new instance of {@link AppCompatEditText}, which specifies
      * {@link EditorInfo#contentMimeTypes} with the given content
      * MIME types
      */
-    private EditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
+    private AppCompatEditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
         final CharSequence hintText;
         final String[] mimeTypes;  // our own copy of contentMimeTypes.
         if (contentMimeTypes == null || contentMimeTypes.length == 0) {
@@ -208,7 +208,7 @@
             hintText = "MIME: " + Arrays.toString(contentMimeTypes);
             mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length);
         }
-        EditText exitText = new EditText(this) {
+        AppCompatEditText exitText = new AppCompatEditText(this) {
             @Override
             public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
                 final InputConnection ic = super.onCreateInputConnection(editorInfo);
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.java
new file mode 100644
index 0000000..a6df13d
--- /dev/null
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.java
@@ -0,0 +1,43 @@
+/*
+ * 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 com.example.android.supportv4.widget;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.android.supportv4.R;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+/**
+ * This activity demonstrates the use of nested scrolling in the v4 support library along with
+ * a collapsing app bar. See the associated layout file for details.
+ */
+public class NestedScrollActivity3LevelsWithCollapsingToolbar extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.nested_scroll_3_levels_collapsing_toolbar);
+
+        CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar_layout);
+        collapsingToolbar.setTitle(
+                getResources().getString(R.string.nested_scroll_3_levels_collapsing_appbar_title)
+        );
+        collapsingToolbar.setContentScrimColor(getResources().getColor(R.color.color1));
+    }
+}
diff --git a/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml b/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml
new file mode 100644
index 0000000..6551ffb
--- /dev/null
+++ b/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+-->
+<!--
+    A NestedScrollView behaves like a ScrollView, but it can be placed into
+    other nested scrolling containers or have other nested scrolling containers
+    placed into it. This demo places a NestedScrollView in a CollapsingToolbarLayout.
+-->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:fitsSystemWindows="true"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="widget.NestedScrollActivity3LevelsWithCollapsingToolbar">
+
+    <!-- App Bar -->
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="200dp">
+
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:expandedTitleMarginStart="48dp"
+            app:expandedTitleMarginEnd="64dp"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed">
+
+            <View android:layout_width="match_parent"
+                android:layout_height="200dp"
+                android:background="@color/color2"/>
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin" />
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <!-- Content -->
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:padding="16dp">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/nested_scroll_long_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+            <androidx.core.widget.NestedScrollView
+                xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="400dp"
+                android:padding="16dp">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical">
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/nested_scroll_short_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+                    <androidx.core.widget.NestedScrollView
+                        android:layout_width="match_parent"
+                        android:layout_height="200dp"
+                        android:padding="16dp">
+                        <TextView
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:text="@string/nested_scroll_long_text"
+                            android:textAppearance="?android:attr/textAppearance"/>
+                    </androidx.core.widget.NestedScrollView>
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/nested_scroll_short_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+                </LinearLayout>
+            </androidx.core.widget.NestedScrollView>
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/nested_scroll_long_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+        </LinearLayout>
+    </androidx.core.widget.NestedScrollView>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml b/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
index 214c637..7e3cc1f 100644
--- a/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
+++ b/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
@@ -14,9 +14,11 @@
      limitations under the License.
 -->
 
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:compat="http://schemas.android.com/apk/res-auto" >
     <item android:id="@+id/force_refresh"
-        android:showAsAction="ifRoom"
+        compat:showAsAction="ifRoom"
         android:icon="@drawable/refresh"
         android:title="Refresh" />
 </menu>
\ No newline at end of file
diff --git a/samples/Support4Demos/src/main/res/values/strings.xml b/samples/Support4Demos/src/main/res/values/strings.xml
index 143b556..3ebe008 100644
--- a/samples/Support4Demos/src/main/res/values/strings.xml
+++ b/samples/Support4Demos/src/main/res/values/strings.xml
@@ -233,6 +233,8 @@
 
     <string name="nested_scroll">Widget/Nested Scrolling</string>
     <string name="nested_scroll_3_levels">Widget/Nested Scrolling (3 levels)</string>
+    <string name="nested_scroll_3_levels_collapsing">Widget/Nested Scrolling (3 levels) with Collapsing Toolbar</string>
+    <string name="nested_scroll_3_levels_collapsing_appbar_title">Collapsing Appbar</string>
 
     <string name="nested_scroll_long_text">This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it.</string>
     <string name="nested_scroll_short_text">This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close.</string>
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
index 0d0c707..94ef7cf 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
@@ -87,11 +87,11 @@
                     dumpNodeRec(child, serializer, i, width, height);
                     child.recycle();
                 } else {
-                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
+                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child));
                 }
             } else {
                 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
-                        i, count, node.toString()));
+                        i, count, node));
             }
         }
         serializer.endTag("", "node");
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index 99d6228..ba9e851 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -159,7 +159,7 @@
             AccessibilityNodeInfo child = node.getChild(i);
             if (child == null) {
                 if (!hasNullChild) {
-                    Log.w(TAG, String.format("Node returned null child: %s", node.toString()));
+                    Log.w(TAG, String.format("Node returned null child: %s", node));
                 }
                 hasNullChild = true;
                 Log.w(TAG, String.format("Skipping null child (%s of %s)", i, numChildren));
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 87a787c..23add70 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -196,11 +196,7 @@
                 active.add(gesture);
             }
 
-            try {
-                Thread.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
-            } catch (InterruptedException e) {
-                Log.e(TAG, "Interrupted while sleeping between events in performGesture");
-            }
+            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
         }
     }
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
index 452757f..5ddfce9 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
@@ -339,7 +339,7 @@
                 Log.w(LOG_TAG, String.format(
                         "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
                 if (!hasNullChild) {
-                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+                    Log.w(LOG_TAG, String.format("parent = %s", fromNode));
                 }
                 hasNullChild = true;
                 continue;
@@ -347,7 +347,7 @@
             if (!childNode.isVisibleToUser()) {
                 if (VERBOSE)
                     Log.v(LOG_TAG,
-                            String.format("Skipping invisible child: %s", childNode.toString()));
+                            String.format("Skipping invisible child: %s", childNode));
                 continue;
             }
             AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i);
@@ -469,7 +469,7 @@
                 Log.w(LOG_TAG, String.format(
                         "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
                 if (!hasNullChild) {
-                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+                    Log.w(LOG_TAG, String.format("parent = %s", fromNode));
                 }
                 hasNullChild = true;
                 continue;
@@ -477,7 +477,7 @@
             if (!childNode.isVisibleToUser()) {
                 if (DEBUG)
                     Log.d(LOG_TAG,
-                        String.format("Skipping invisible child: %s", childNode.toString()));
+                        String.format("Skipping invisible child: %s", childNode));
                 continue;
             }
             AccessibilityNodeInfo retNode = findNodePatternRecursive(
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java
deleted file mode 100644
index e4dc963..0000000
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * Copyright (C) 2012 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.test.uiautomator;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Class that creates traces of the calls to the UiAutomator API and outputs the
- * traces either to logcat or a logfile. Each public method in the UiAutomator
- * that needs to be traced should include a call to Tracer.trace in the
- * beginning. Tracing is turned off by default and needs to be enabled
- * explicitly.
- * @hide
- */
-public class Tracer {
-    private static final String UNKNOWN_METHOD_STRING = "(unknown method)";
-    private static final String UIAUTOMATOR_PACKAGE = Tracer.class.getPackage().getName();
-    private static final int CALLER_LOCATION = 6;
-    private static final int METHOD_TO_TRACE_LOCATION = 5;
-    private static final int MIN_STACK_TRACE_LENGTH = 7;
-
-    /**
-     * Enum that determines where the trace output goes. It can go to either
-     * logcat, log file or both.
-     */
-    public enum Mode {
-        NONE,
-        FILE,
-        LOGCAT,
-        ALL
-    }
-
-    private interface TracerSink {
-        public void log(String message);
-
-        public void close();
-    }
-
-    private static class FileSink implements TracerSink {
-        private final PrintWriter mOut;
-        private final SimpleDateFormat mDateFormat;
-
-        public FileSink(File file) throws FileNotFoundException {
-            mOut = new PrintWriter(file);
-            mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
-        }
-
-        @Override
-        public void log(String message) {
-            mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
-        }
-
-        @Override
-        public void close() {
-            mOut.close();
-        }
-    }
-
-    static class LogcatSink implements TracerSink {
-
-        private static final String LOGCAT_TAG = "UiAutomatorTrace";
-
-        @Override
-        public void log(String message) {
-            Log.i(LOGCAT_TAG, message);
-        }
-
-        @Override
-        public void close() {
-            // nothing is needed
-        }
-    }
-
-    private Mode mCurrentMode = Mode.NONE;
-    private final List<TracerSink> mSinks = new ArrayList<>();
-    private File mOutputFile;
-
-    private static Tracer mInstance = null;
-
-    /**
-     * Returns a reference to an instance of the tracer. Useful to set the
-     * parameters before the trace is collected.
-     *
-     * @return
-     */
-    @NonNull
-    public static Tracer getInstance() {
-        if (mInstance == null) {
-            mInstance = new Tracer();
-        }
-        return mInstance;
-    }
-
-    /**
-     * Sets where the trace output will go. Can be either be logcat or a file or
-     * both. Setting this to NONE will turn off tracing.
-     *
-     * @param mode
-     */
-    public void setOutputMode(@NonNull Mode mode) {
-        closeSinks();
-        mCurrentMode = mode;
-        try {
-            switch (mode) {
-                case FILE:
-                    if (mOutputFile == null) {
-                        throw new IllegalArgumentException("Please provide a filename before " +
-                                "attempting write trace to a file");
-                    }
-                    mSinks.add(new FileSink(mOutputFile));
-                    break;
-                case LOGCAT:
-                    mSinks.add(new LogcatSink());
-                    break;
-                case ALL:
-                    mSinks.add(new LogcatSink());
-                    if (mOutputFile == null) {
-                        throw new IllegalArgumentException("Please provide a filename before " +
-                                "attempting write trace to a file");
-                    }
-                    mSinks.add(new FileSink(mOutputFile));
-                    break;
-                default:
-                    break;
-            }
-        } catch (FileNotFoundException e) {
-            Log.w("Tracer", "Could not open log file: " + e.getMessage());
-        }
-    }
-
-    private void closeSinks() {
-        for (TracerSink sink : mSinks) {
-            sink.close();
-        }
-        mSinks.clear();
-    }
-
-    /**
-     * Sets the name of the log file where tracing output will be written if the
-     * tracer is set to write to a file.
-     *
-     * @param filename name of the log file.
-     */
-    public void setOutputFilename(@NonNull String filename) {
-        mOutputFile = new File(filename);
-    }
-
-    private void doTrace(Object[] arguments) {
-        if (mCurrentMode == Mode.NONE) {
-            return;
-        }
-
-        String caller = getCaller();
-        if (caller == null) {
-            return;
-        }
-
-        log(String.format("%s (%s)", caller, join(", ", arguments)));
-    }
-
-    private void log(String message) {
-        for (TracerSink sink : mSinks) {
-            sink.log(message);
-        }
-    }
-
-    /**
-     * Queries whether the tracing is enabled.
-     * @return true if tracing is enabled, false otherwise.
-     */
-    public boolean isTracingEnabled() {
-        return mCurrentMode != Mode.NONE;
-    }
-
-    /**
-     * Public methods in the UiAutomator should call this function to generate a
-     * trace. The trace will include the method that's is being called, it's
-     * arguments and where in the user's code the method is called from. If a
-     * public method is called internally from UIAutomator then this will not
-     * output a trace entry. Only calls from outside the UiAutomator package will
-     * produce output.
-     *
-     * Special note about array arguments. You can safely pass arrays of reference types
-     * to this function. Like String[] or Integer[]. The trace function will print their
-     * contents by calling toString() on each of the elements. This will not work for
-     * array of primitive types like int[] or float[]. Before passing them to this function
-     * convert them to arrays of reference types manually. Example: convert int[] to Integer[].
-     *
-     * @param arguments arguments of the method being traced.
-     */
-    public static void trace(@NonNull Object... arguments) {
-        Tracer.getInstance().doTrace(arguments);
-    }
-
-    private static String join(String separator, Object[] strings) {
-        if (strings.length == 0) {
-            return "";
-        }
-
-        StringBuilder builder = new StringBuilder(objectToString(strings[0]));
-        for (int i = 1; i < strings.length; i++) {
-            builder.append(separator);
-            builder.append(objectToString(strings[i]));
-        }
-        return builder.toString();
-    }
-
-    /**
-     * Special toString method to handle arrays. If the argument is a normal object then this will
-     * return normal output of obj.toString(). If the argument is an array this will return a
-     * string representation of the elements of the array.
-     *
-     * This method will not work for arrays of primitive types. Arrays of primitive types are
-     * expected to be converted manually by the caller. If the array is not converter then
-     * this function will only output "[...]" instead of the contents of the array.
-     *
-     * @param obj object to convert to a string
-     * @return String representation of the object.
-     */
-    private static String objectToString(Object obj) {
-        if (obj.getClass().isArray()) {
-            if (obj instanceof Object[]) {
-                return Arrays.deepToString((Object[]) obj);
-            } else {
-                return "[...]";
-            }
-        } else {
-            return obj.toString();
-        }
-    }
-
-    /**
-     * This method outputs which UiAutomator method was called and where in the
-     * user code it was called from. If it can't decide which method is called
-     * it will output "(unknown method)". If the method was called from inside
-     * the UiAutomator then it returns null.
-     *
-     * @return name of the method called and where it was called from. Null if
-     *         method was called from inside UiAutomator.
-     */
-    private static String getCaller() {
-        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
-        if (stackTrace.length < MIN_STACK_TRACE_LENGTH) {
-            return UNKNOWN_METHOD_STRING;
-        }
-
-        StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION];
-        StackTraceElement previousCaller = stackTrace[CALLER_LOCATION];
-
-        if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) {
-            return null;
-        }
-
-        int indexOfDot = caller.getClassName().lastIndexOf('.');
-        if (indexOfDot < 0) {
-            indexOfDot = 0;
-        }
-
-        if (indexOfDot + 1 >= caller.getClassName().length()) {
-            return UNKNOWN_METHOD_STRING;
-        }
-
-        String shortClassName = caller.getClassName().substring(indexOfDot + 1);
-        return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(),
-                previousCaller.getMethodName(), previousCaller.getFileName(),
-                previousCaller.getLineNumber());
-    }
-}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
index 2ec04ba..274e7d9 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
@@ -29,26 +29,6 @@
  */
 public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner {
 
-    @Override
-    public void onStart() {
-        // process runner arguments before test starts
-        String traceType = getArguments().getString("traceOutputMode");
-        if(traceType != null) {
-            Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType);
-            if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) {
-                String filename = getArguments().getString("traceLogFilename");
-                if (filename == null) {
-                    throw new RuntimeException("Name of log file not specified. " +
-                            "Please specify it using traceLogFilename parameter");
-                }
-                Tracer.getInstance().setOutputFilename(filename);
-            }
-            Tracer.getInstance().setOutputMode(mode);
-        }
-        super.onStart();
-    }
-
-
     /**
      * Perform initialization specific to UiAutomator test. It sets up the test case so that
      * it can access the UiDevice and gives it access to the command line arguments.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index e1c2e70..e330872 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -165,7 +165,7 @@
      * was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -180,8 +180,8 @@
     public <U> U performActionAndWait(@NonNull Runnable action,
             @NonNull EventCondition<U> condition, long timeout) {
         AccessibilityEvent event = null;
-        Log.d(TAG, String.format("Performing action %s and waiting %dms for condition %s.",
-                action, timeout, condition));
+        Log.d(TAG, String.format("Performing action %s and waiting %dms for %s.", action, timeout,
+                condition));
         try {
             event = getUiAutomation().executeAndWaitForEvent(
                 action, new EventForwardingFilter(condition), timeout);
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cb83ac0ab..cc5a089 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -153,7 +153,7 @@
      * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull UiObject2Condition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -166,7 +166,7 @@
      * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -494,8 +494,8 @@
      */
     public <U> U clickAndWait(@NonNull EventCondition<U> condition, long timeout) {
         Point center = getVisibleCenter();
-        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for condition %s.",
-                center.x, center.y, timeout, condition));
+        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for %s.", center.x,
+                center.y, timeout, condition));
         return mGestureController.performGestureAndWait(condition, timeout,
                 Gestures.click(center, getDisplayId()));
     }
@@ -511,8 +511,8 @@
     public <U> U clickAndWait(@NonNull Point point, @NonNull EventCondition<U> condition,
             long timeout) {
         clipToGestureBounds(point);
-        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for condition %s.",
-                point.x, point.y, timeout, condition));
+        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for %s.", point.x,
+                point.y, timeout, condition));
         return mGestureController.performGestureAndWait(
                 condition, timeout, Gestures.click(point, getDisplayId()));
     }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
index 53c6471..0a3529b 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
@@ -946,7 +946,7 @@
 
     String dumpToString(boolean all) {
         StringBuilder builder = new StringBuilder();
-        builder.append(UiSelector.class.getSimpleName() + "[");
+        builder.append(UiSelector.class.getSimpleName()).append("[");
         final int criterionCount = mSelectorAttributes.size();
         for (int i = 0; i < criterionCount; i++) {
             if (i > 0) {
@@ -1064,7 +1064,7 @@
                     builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i));
                     break;
                 default:
-                    builder.append("UNDEFINED=" + criterion + " ").append(
+                    builder.append("UNDEFINED=").append(criterion).append(" ").append(
                             mSelectorAttributes.valueAt(i));
             }
         }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index c680d4f..5a6d9b5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -44,6 +44,12 @@
             Boolean apply(Searchable container) {
                 return !container.hasObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[gone=%s]", selector);
+            }
         };
     }
 
@@ -58,6 +64,12 @@
             Boolean apply(Searchable container) {
                 return container.hasObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[hasObject=%s]", selector);
+            }
         };
     }
 
@@ -72,6 +84,12 @@
             UiObject2 apply(Searchable container) {
                 return container.findObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[findObject=%s]", selector);
+            }
         };
     }
 
@@ -87,6 +105,12 @@
                 List<UiObject2> ret = container.findObjects(selector);
                 return ret.isEmpty() ? null : ret;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[findObjects=%s]", selector);
+            }
         };
     }
 
@@ -105,6 +129,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isCheckable() == isCheckable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[checkable=%b]", isCheckable);
+            }
         };
     }
 
@@ -120,6 +150,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isChecked() == isChecked;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[checked=%b]", isChecked);
+            }
         };
     }
 
@@ -135,6 +171,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isClickable() == isClickable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[clickable=%b]", isClickable);
+            }
         };
     }
 
@@ -150,6 +192,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isEnabled() == isEnabled;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[enabled=%b]", isEnabled);
+            }
         };
     }
 
@@ -165,6 +213,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isFocusable() == isFocusable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[focusable=%b]", isFocusable);
+            }
         };
     }
 
@@ -180,6 +234,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isFocused() == isFocused;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[focused=%b]", isFocused);
+            }
         };
     }
 
@@ -195,6 +255,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isLongClickable() == isLongClickable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[longClickable=%b]", isLongClickable);
+            }
         };
     }
 
@@ -210,6 +276,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isScrollable() == isScrollable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[scrollable=%b]", isScrollable);
+            }
         };
     }
 
@@ -225,6 +297,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isSelected() == isSelected;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[selected=%b]", isSelected);
+            }
         };
     }
 
@@ -240,6 +318,12 @@
                 String desc = object.getContentDescription();
                 return regex.matcher(desc != null ? desc : "").matches();
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[descMatches='%s']", regex);
+            }
         };
     }
 
@@ -299,6 +383,12 @@
                 String text = object.getText();
                 return regex.matcher(text != null ? text : "").matches();
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[textMatches='%s']", regex);
+            }
         };
     }
 
@@ -321,6 +411,12 @@
             Boolean apply(UiObject2 object) {
                 return !text.equals(object.getText());
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[textNotEquals='%s']", text);
+            }
         };
     }
 
@@ -380,6 +476,12 @@
             Boolean getResult() {
                 return mMask == 0;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("EventCondition[newWindow]");
+            }
         };
     }
 
@@ -449,6 +551,12 @@
                 // the end and return true.
                 return mResult == null || mResult;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("EventCondition[scrollFinished=%s]", direction.name());
+            }
         };
     }
 }
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index 2584bf1..29362d9 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -30,9 +30,9 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    def composeVersion = '1.2.1'
+    def composeVersion = '1.3.0-rc01'
 
-    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.annotation:annotation:1.5.0")
     api("androidx.compose.animation:animation:$composeVersion")
     api("androidx.compose.runtime:runtime:$composeVersion")
     api("androidx.compose.ui:ui:$composeVersion")
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 24ca3bd..61d6bc4 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1,4 +1,40 @@
 // Signature format: 4.0
+package androidx.tv.material {
+
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+  }
+
+}
+
 package androidx.tv.material.carousel {
 
   public final class CarouselItemKt {
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 8712654..dc3de1c 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,7 +1,39 @@
 // Signature format: 4.0
 package androidx.tv.material {
 
-  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") public @interface ExperimentalTvMaterialApi {
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvMaterialApi {
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 24ca3bd..61d6bc4 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1,4 +1,40 @@
 // Signature format: 4.0
+package androidx.tv.material {
+
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+  }
+
+}
+
 package androidx.tv.material.carousel {
 
   public final class CarouselItemKt {
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 19f04632..50fc402 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -27,9 +27,9 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    def composeVersion = '1.2.1'
+    def composeVersion = '1.3.0-rc01'
 
-    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.annotation:annotation:1.5.0")
     api("androidx.compose.animation:animation:$composeVersion")
     api("androidx.compose.runtime:runtime:$composeVersion")
 
diff --git a/tv/tv-material/samples/build.gradle b/tv/tv-material/samples/build.gradle
index f78e2c8..2dfd344 100644
--- a/tv/tv-material/samples/build.gradle
+++ b/tv/tv-material/samples/build.gradle
@@ -26,11 +26,10 @@
 dependencies {
     implementation(project(":tv:tv-material"))
     implementation(libs.kotlinStdlib)
-    implementation("androidx.leanback:leanback:1.0.0")
     implementation(project(":activity:activity"))
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-runtime"))
-    implementation("androidx.activity:activity-compose:1.5.1")
+    implementation("androidx.activity:activity-compose:1.6.1")
     implementation(project(":tv:tv-foundation"))
     implementation("androidx.appcompat:appcompat:1.6.0-alpha05")
 }
@@ -56,4 +55,4 @@
         }
     }
     namespace "androidx.tv.tvmaterial.samples"
-}
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt
new file mode 100644
index 0000000..773c124
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun App() {
+    var selectedTab by remember { mutableStateOf(navigationMap[Navigation.FeaturedCarousel]) }
+
+    Column(
+        modifier = Modifier.padding(20.dp),
+        verticalArrangement = Arrangement.spacedBy(20.dp),
+    ) {
+        TopNavigation(updateSelectedTab = { selectedTab = it })
+        when (reverseNavigationMap[selectedTab]) {
+            Navigation.FeaturedCarousel -> FeaturedCarousel()
+            Navigation.ImmersiveList -> SampleImmersiveList()
+            Navigation.LazyRowsAndColumns -> LazyRowsAndColumns()
+            else -> { }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index f1b3668..a0f499c 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -16,26 +16,15 @@
 
 package androidx.tv.tvmaterial.samples
 
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
-import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -44,107 +33,70 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Color.Companion.Cyan
-import androidx.compose.ui.graphics.Color.Companion.Gray
-import androidx.compose.ui.graphics.Color.Companion.Yellow
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.tv.material.ExperimentalTvMaterialApi
 import androidx.tv.material.carousel.Carousel
 import androidx.tv.material.carousel.CarouselItem
-import androidx.tv.material.carousel.CarouselState
 
 @OptIn(ExperimentalTvMaterialApi::class)
 @Composable
 fun FeaturedCarousel() {
-    val carouselState = remember { CarouselState(0) }
+    val backgrounds = listOf(
+        Color.Red.copy(alpha = 0.3f),
+        Color.Yellow.copy(alpha = 0.3f),
+        Color.Green.copy(alpha = 0.3f)
+    )
+
     Carousel(
-        modifier = Modifier.height(130.dp).fillMaxWidth().border(1.dp, Color.Black),
-        carouselState = carouselState,
-        slideCount = mediaItems.size
-    ) { SampleFrame(it) }
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-fun SampleFrame(idx: Int) {
-    val item = mediaItems[idx]
-
-    CarouselItem(
-        background = {
-            Box(
-                Modifier.background(item.backgroundColor).fillMaxSize()
-            )
-        }) {
-        Box {
-            Column(modifier = Modifier.align(Alignment.BottomStart)) {
-                Text(
-                    text = item.title,
-                    style = MaterialTheme.typography.headlineSmall,
-                    color = Color.Black,
-                    fontWeight = FontWeight.Bold
+        slideCount = backgrounds.size,
+        modifier = Modifier
+            .height(300.dp)
+            .fillMaxWidth(),
+    ) { itemIndex ->
+        CarouselItem(
+            overlayEnterTransitionStartDelayMillis = 0,
+            background = {
+                Box(
+                    modifier = Modifier
+                        .background(backgrounds[itemIndex])
+                        .border(2.dp, Color.White.copy(alpha = 0.5f))
+                        .fillMaxSize()
                 )
-                Text(
-                    text = item.description,
-                    style = MaterialTheme.typography.bodyMedium,
-                    color = Color.Black,
-                    fontWeight = FontWeight.Normal,
-                    modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
-                )
-
-                Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
-                    SampleButton(text = "PLAY")
-                    SampleButton(text = "INFO")
-                }
             }
+        ) {
+            Card()
         }
     }
 }
 
 @Composable
-fun SampleButton(text: String) {
-    var cardScale
-        by remember { mutableStateOf(0.5f) }
-    val borderGlowColorTransition =
-        rememberInfiniteTransition()
-    var initialValue
-        by remember { mutableStateOf(Color.Transparent) }
-    val glowingColor
-        by borderGlowColorTransition.animateColor(
-            initialValue = initialValue,
-            targetValue = Color.Transparent,
-            animationSpec = infiniteRepeatable(
-                animation = tween(1000, easing = LinearEasing),
-                repeatMode = RepeatMode.Reverse
-            )
-        )
-
-    Button(
-        onClick = {},
+private fun Card() {
+    Box(
         modifier = Modifier
-            .scale(cardScale)
-            .border(
-                2.dp, glowingColor,
-                RoundedCornerShape(12.dp)
-            )
-            .onFocusChanged { focusState ->
-                if (focusState.isFocused) {
-                    cardScale = 1.0f
-                    initialValue = Color.White
-                } else {
-                    cardScale = 0.5f
-                    initialValue = Color.Transparent
-                }
-            }) {
-        Text(text = text)
+            .fillMaxSize()
+            .padding(40.dp),
+        contentAlignment = Alignment.CenterStart
+    ) {
+        var isFocused by remember { mutableStateOf(false) }
+
+        Box(
+            modifier = Modifier
+                .border(
+                    width = 2.dp,
+                    color = if (isFocused) Color.Red else Color.Transparent,
+                    shape = RoundedCornerShape(50)
+                )
+        ) {
+            Button(
+                onClick = { },
+                modifier = Modifier
+                    .onFocusChanged { isFocused = it.isFocused }
+                    .padding(vertical = 2.dp, horizontal = 5.dp)
+            ) {
+                Text(text = "Play")
+            }
+        }
     }
 }
-
-val mediaItems = listOf(
-    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Gray),
-    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Yellow),
-    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Cyan)
-)
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt
new file mode 100644
index 0000000..cb4847f
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.immersivelist.ImmersiveList
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+fun SampleImmersiveList() {
+    val immersiveListHeight = 300.dp
+    val cardSpacing = 10.dp
+    val cardWidth = 200.dp
+    val cardHeight = 150.dp
+    val backgrounds = listOf(
+        Color.Red,
+        Color.Blue,
+        Color.Magenta,
+    )
+
+    Box(
+        modifier = Modifier
+            .height(immersiveListHeight + cardHeight / 2)
+            .fillMaxWidth()
+    ) {
+        ImmersiveList(
+            modifier = Modifier
+                .height(immersiveListHeight)
+                .fillMaxWidth(),
+            listAlignment = Alignment.BottomEnd,
+            background = { index, _ ->
+                Box(
+                    modifier = Modifier
+                        .background(backgrounds[index].copy(alpha = 0.3f))
+                        .fillMaxSize()
+                )
+            }
+        ) {
+            Row(
+                horizontalArrangement = Arrangement.spacedBy(cardSpacing),
+                modifier = Modifier.offset(y = cardHeight / 2)
+            ) {
+                backgrounds.forEachIndexed { index, backgroundColor ->
+                    var isFocused by remember { mutableStateOf(false) }
+
+                    Box(
+                        modifier = Modifier
+                            .background(backgroundColor)
+                            .width(cardWidth)
+                            .height(cardHeight)
+                            .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
+                            .onFocusChanged { isFocused = it.isFocused }
+                            .focusableItem(index)
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
new file mode 100644
index 0000000..bbf0111
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyColumn
+import androidx.tv.foundation.lazy.list.TvLazyRow
+
+const val rowsCount = 20
+const val columnsCount = 100
+
+@Composable
+fun LazyRowsAndColumns() {
+    TvLazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+        repeat((0 until rowsCount).count()) {
+            item { LazyRow() }
+        }
+    }
+}
+
+@Composable
+private fun LazyRow() {
+    val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
+    val backgroundColors = (0 until columnsCount).map { colors.random() }
+
+    TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+        backgroundColors.forEach { backgroundColor ->
+            item {
+                var isFocused by remember { mutableStateOf(false) }
+
+                Box(
+                    modifier = Modifier
+                        .background(backgroundColor.copy(alpha = 0.3f))
+                        .width(200.dp)
+                        .height(150.dp)
+                        .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.2f))
+                        .onFocusChanged { isFocused = it.isFocused }
+                        .focusable()
+                )
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
index b3f2c76..e8ebd3a 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
@@ -19,111 +19,12 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
 
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContent {
-            // A surface container using the 'background' color from the theme
-            Surface(color = MaterialTheme.colorScheme.background) {
-                LazyColumn {
-                    item { FeaturedCarousel() }
-                    item { SampleImmersiveList() }
-
-                    items(7) { SampleLazyRow() }
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun SampleLazyRow() {
-        LazyRow(
-            state = rememberLazyListState(),
-            contentPadding = PaddingValues(2.dp),
-            horizontalArrangement = Arrangement.spacedBy(4.dp),
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(100.dp)) {
-            items((1..10).map { it.toString() }) { SampleCard(it) }
-        }
-    }
-
-    @Composable
-    private fun SampleCard(it: String) {
-        var cardScale by remember { mutableStateOf(0.5f) }
-        val borderGlowColorTransition = rememberInfiniteTransition()
-        var initialValue by remember { mutableStateOf(Color.Transparent) }
-        val glowingColor by borderGlowColorTransition.animateColor(
-            initialValue = initialValue,
-            targetValue = Color.Transparent,
-            animationSpec = infiniteRepeatable(
-                animation = tween(1000, easing = LinearEasing),
-                repeatMode = RepeatMode.Reverse
-            )
-        )
-
-        Card(
-            modifier = Modifier
-                .width(100.dp)
-                .height(100.dp)
-                .scale(cardScale)
-                .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
-                .onFocusChanged { focusState ->
-                    if (focusState.isFocused) {
-                        cardScale = 1.0f
-                        initialValue = Color.White
-                    } else {
-                        cardScale = 0.5f
-                        initialValue = Color.Transparent
-                    }
-                }
-                .focusable()
-        ) {
-            Text(
-                text = it,
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .height(100.dp)
-                    .padding(12.dp),
-                color = Color.Red,
-                fontWeight = FontWeight.Bold
-
-            )
+            App()
         }
     }
 }
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
deleted file mode 100644
index cab1518..0000000
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * 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.tv.tvmaterial.samples
-
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.immersivelist.ImmersiveList
-
-@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
-@Composable
-fun SampleImmersiveList() {
-    ImmersiveList(
-        modifier = Modifier
-            .height(130.dp)
-            .fillMaxWidth()
-            .border(1.dp, Color.Black),
-        background = { index, _ ->
-            AnimatedContent(targetState = index) { SampleBackground(it) } },
-    ) {
-        TvLazyRow {
-            items(immersiveClusterMediaItems.size) {
-                SampleCard(Modifier.focusableItem(it), (it + 1).toString())
-            }
-        }
-    }
-}
-
-@Composable
-fun SampleBackground(idx: Int) {
-    val item = immersiveClusterMediaItems[idx]
-
-    Box(
-        Modifier
-            .background(item.backgroundColor)
-            .fillMaxWidth()
-            .height(90.dp)) {
-        Text(
-            text = item.title,
-            style = MaterialTheme.typography.headlineSmall,
-            color = Color.Black,
-            fontWeight = FontWeight.Bold
-        )
-    }
-}
-
-@Composable
-private fun SampleCard(modifier: Modifier, cardText: String) {
-    var cardScale by remember { mutableStateOf(0.5f) }
-    val borderGlowColorTransition = rememberInfiniteTransition()
-    var initialValue by remember { mutableStateOf(Color.Transparent) }
-    val glowingColor by borderGlowColorTransition.animateColor(
-        initialValue = initialValue,
-        targetValue = Color.Transparent,
-        animationSpec = infiniteRepeatable(
-            animation = tween(1000, easing = LinearEasing),
-            repeatMode = RepeatMode.Reverse
-        )
-    )
-
-    Card(
-        modifier = modifier
-            .width(100.dp)
-            .height(100.dp)
-            .scale(cardScale)
-            .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
-            .onFocusChanged { focusState ->
-                if (focusState.isFocused) {
-                    cardScale = 1.0f
-                    initialValue = Color.White
-                } else {
-                    cardScale = 0.5f
-                    initialValue = Color.Transparent
-                }
-            }
-            .focusable()
-    ) {
-        Text(
-            text = cardText,
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(100.dp)
-                .padding(12.dp),
-            color = Color.Red,
-            fontWeight = FontWeight.Bold
-
-        )
-    }
-}
-
-val immersiveClusterMediaItems = listOf(
-    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Color.Gray),
-    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Color.Blue),
-    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Color.Cyan)
-)
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
new file mode 100644
index 0000000..8cee8db
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material.LocalContentColor
+import androidx.tv.material.Tab
+import androidx.tv.material.TabDefaults
+import androidx.tv.material.TabRow
+import androidx.tv.material.TabRowDefaults
+
+enum class Navigation {
+  FeaturedCarousel,
+  ImmersiveList,
+  LazyRowsAndColumns,
+}
+
+val navigationMap =
+  hashMapOf(
+    Navigation.FeaturedCarousel to "Featured Carousel",
+    Navigation.ImmersiveList to "Immersive List",
+    Navigation.LazyRowsAndColumns to "Lazy Rows and Columns",
+  )
+val reverseNavigationMap = navigationMap.entries.associate { it.value to it.key }
+
+@Composable
+internal fun TopNavigation(
+  updateSelectedTab: (String) -> Unit = {},
+) {
+  var selectedTabIndex by remember { mutableStateOf(0) }
+  val tabs = navigationMap.entries.map { it.value }
+
+  // Pill indicator
+  PillIndicatorTabRow(
+    tabs = tabs,
+    selectedTabIndex = selectedTabIndex,
+    updateSelectedTab = { selectedTabIndex = it }
+  )
+
+  LaunchedEffect(selectedTabIndex) { updateSelectedTab(tabs[selectedTabIndex]) }
+}
+
+/**
+ * Pill indicator tab row for reference
+ */
+@Composable
+fun PillIndicatorTabRow(
+  tabs: List<String>,
+  selectedTabIndex: Int,
+  updateSelectedTab: (Int) -> Unit
+) {
+  TabRow(
+    selectedTabIndex = selectedTabIndex,
+    separator = { Spacer(modifier = Modifier.width(12.dp)) },
+  ) {
+    tabs.forEachIndexed { index, tab ->
+      Tab(
+        selected = index == selectedTabIndex,
+        onSelect = { updateSelectedTab(index) },
+      ) {
+        Text(
+          text = tab,
+          fontSize = 12.sp,
+          color = LocalContentColor.current,
+          modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
+        )
+      }
+    }
+  }
+}
+
+/**
+ * Underlined indicator tab row for reference
+ */
+@Composable
+fun UnderlinedIndicatorTabRow(
+  tabs: List<String>,
+  selectedTabIndex: Int,
+  updateSelectedTab: (Int) -> Unit
+) {
+  TabRow(
+    selectedTabIndex = selectedTabIndex,
+    separator = { Spacer(modifier = Modifier.width(12.dp)) },
+    indicator = { tabPositions ->
+      TabRowDefaults.UnderlinedIndicator(
+        currentTabPosition = tabPositions[selectedTabIndex]
+      )
+    }
+  ) {
+    tabs.forEachIndexed { index, tab ->
+      Tab(
+        selected = index == selectedTabIndex,
+        onSelect = { updateSelectedTab(index) },
+        colors = TabDefaults.underlinedIndicatorTabColors(),
+      ) {
+        Text(
+          text = tab,
+          fontSize = 12.sp,
+          color = LocalContentColor.current,
+          modifier = Modifier.padding(bottom = 4.dp)
+        )
+      }
+    }
+  }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
new file mode 100644
index 0000000..8459060
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+
+class TabRowTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun tabRow_firstTabIsSelected() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+
+        setContent(tabs)
+
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadRightMovesFocusToSecondTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadLeftMovesFocusToPreviousTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+        val thirdTab = tabs[2]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Third tab should be focused
+        rule.onNodeWithTag(thirdTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    private fun setContent(tabs: List<String>) {
+        val fr = FocusRequester()
+
+        rule.setContent {
+            Column(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Black)
+            ) {
+                var selectedTabIndex by remember { mutableStateOf(0) }
+
+                TabRow(
+                    selectedTabIndex = selectedTabIndex,
+                    separator = { Spacer(modifier = Modifier.width(12.dp)) }
+                ) {
+                    tabs.forEachIndexed { index, tab ->
+                        Tab(
+                            selected = index == selectedTabIndex,
+                            onSelect = { selectedTabIndex = index },
+                            modifier = Modifier
+                                .width(100.dp)
+                                .height(50.dp)
+                                .testTag(tab)
+                                .border(2.dp, Color.White, RoundedCornerShape(50))
+                        ) {}
+                    }
+                }
+
+                // Added so that this can get focus and pass it to the tab row
+                Box(
+                    modifier = Modifier
+                        .size(50.dp)
+                        .focusRequester(fr)
+                        .background(Color.White)
+                        .focusable()
+                )
+
+                // Send focus to button
+                LaunchedEffect(Unit) {
+                    fr.requestFocus()
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        // Move the focus TabRow
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP)
+
+        rule.waitForIdle()
+    }
+
+    private fun performKeyPress(keyCode: Int, count: Int = 1) {
+        for (i in 1..count) {
+            InstrumentationRegistry
+                .getInstrumentation()
+                .sendKeyDownUpSync(keyCode)
+        }
+    }
+
+    private fun constructTabs(
+        count: Int = 3,
+        buildTab: (index: Int) -> String = { "Season $it" }
+    ): List<String> = (0 until count).map(buildTab)
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index 16787a1..b20e1c6a 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.animation.core.infiniteRepeatable
 import androidx.compose.animation.core.rememberInfiniteTransition
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.focusable
@@ -33,6 +34,7 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -58,16 +60,19 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
 import androidx.compose.ui.test.assertIsNotFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tv.material.ExperimentalTvMaterialApi
+import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 
@@ -410,6 +415,102 @@
         rule.onNodeWithText("Button 1").assertIsDisplayed()
     }
 
+    @Test
+    fun carousel_scrollToRegainFocus_checkBringIntoView() {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            LazyColumn {
+                items(3) {
+                    val modifier =
+                        if (it == 0) Modifier.focusRequester(focusRequester)
+                        else Modifier
+                    var isFocused by remember {
+                        mutableStateOf(false)
+                    }
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = modifier
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .border(2.dp, if (isFocused) Color.Red else Color.Black)
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
+                }
+                item {
+                    val carouselState = remember { CarouselState() }
+                    val slideCount = remember { 3 }
+                    Carousel(
+                        modifier = Modifier
+                            .height(400.dp)
+                            .fillMaxWidth()
+                            .testTag("featured-carousel")
+                            .border(2.dp, Color.Black),
+                        carouselState = carouselState,
+                        slideCount = slideCount,
+                        timeToDisplaySlideMillis = delayBetweenSlides
+                    ) {
+                        Frame(text = "carousel-frame")
+                    }
+                }
+                items(2) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(250.dp)
+                            .border(
+                                2.dp,
+                                if (isFocused) Color.Red else Color.Black
+                            )
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Carousel and check if it's brought into view on gaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("featured-carousel")).isTrue()
+
+        // Scroll down to last element, making sure the carousel is partially visible
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+
+        // Scroll back to the carousel to check if it's brought into view on regaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("featured-carousel")).isTrue()
+    }
+
+    private fun checkNodeCompletelyVisible(tag: String): Boolean {
+        rule.waitForIdle()
+
+        val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+        val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+        return itemRect.left >= rootRect.left &&
+            itemRect.right <= rootRect.right &&
+            itemRect.top >= rootRect.top &&
+            itemRect.bottom <= rootRect.bottom
+    }
+
     private fun performKeyPress(keyCode: Int, count: Int = 1) {
         for (i in 1..count) {
             InstrumentationRegistry
@@ -489,28 +590,24 @@
 
     @Composable
     fun Frame(text: String) {
-        val focusRequester = FocusRequester()
         CarouselItem(
             overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
-            background = {}) {
-            Column(modifier = Modifier
-                .onFocusChanged {
-                    if (it.isFocused) {
-                        focusRequester.requestFocus()
+            background = {
+                Box(
+                    Modifier
+                        .background(Color.Yellow)
+                        .fillMaxSize()
+                )
+            }) {
+            Box {
+                Column(modifier = Modifier
+                    .align(Alignment.BottomStart)) {
+                    BasicText(text = text)
+                    Row(modifier = Modifier
+                        .horizontalScroll(rememberScrollState())) {
+                        TestButton(text = "PLAY")
                     }
                 }
-                .focusable()) {
-                BasicText(text = text)
-                Row(modifier = Modifier
-                    .horizontalScroll(rememberScrollState())
-                    .onFocusChanged {
-                        if (it.isFocused) {
-                            focusRequester.requestFocus()
-                        }
-                    }
-                    .focusable()) {
-                    TestButton(text = "PLAY", focusRequester)
-                }
             }
         }
     }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
index 5b22bdd..1aeefeb 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
@@ -17,22 +17,34 @@
 package androidx.tv.material.immersivelist
 
 import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.input.key.NativeKeyEvent
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.dp
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tv.foundation.lazy.list.TvLazyRow
 import androidx.tv.material.ExperimentalTvMaterialApi
+import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 
@@ -100,6 +112,113 @@
         rule.onNodeWithTag("background-2").assertDoesNotExist()
     }
 
+    @Test
+    fun immersiveList_scrollToRegainFocus_checkBringIntoView() {
+        val focusRequesterList = mutableListOf<FocusRequester>()
+        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
+        setupContent(focusRequesterList)
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Immersive List's first card
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("list-card-0").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+
+        // Scroll down to last element, making sure the immersive list is partially visible
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+
+        // Scroll back to the immersive list to check if it's brought into view on regaining focus
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+    }
+
+    private fun checkNodeCompletelyVisible(tag: String): Boolean {
+        rule.waitForIdle()
+
+        val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+        val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+        return itemRect.left >= rootRect.left &&
+            itemRect.right <= rootRect.right &&
+            itemRect.top >= rootRect.top &&
+            itemRect.bottom <= rootRect.bottom
+    }
+
+    private fun setupContent(focusRequesterList: List<FocusRequester>) {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            LazyColumn() {
+                items(3) {
+                    val modifier =
+                        if (it == 0) Modifier.focusRequester(focusRequester)
+                        else Modifier
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = modifier
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+                item { TestImmersiveList(focusRequesterList) }
+                items(2) {
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+    }
+
+    @OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
+    @Composable
+    private fun TestImmersiveList(focusRequesterList: List<FocusRequester>) {
+        val frList = remember { focusRequesterList }
+        ImmersiveList(
+            background = { index, _ ->
+                AnimatedContent(targetState = index) {
+                    Box(
+                        Modifier
+                            .testTag("background-$it")
+                            .fillMaxWidth()
+                            .height(400.dp)
+                            .border(2.dp, Color.Black, RectangleShape)
+                    ) {
+                        BasicText("background-$it")
+                    }
+                }
+            },
+            modifier = Modifier.testTag("immersive-list")
+        ) {
+            TvLazyRow {
+                items(frList.count()) { index ->
+                    var modifier = Modifier
+                        .testTag("list-card-$index")
+                        .size(50.dp)
+                    for (item in frList) {
+                        modifier = modifier.focusRequester(frList[index])
+                    }
+                    Box(modifier.focusableItem(index)) { BasicText("list-card-$index") }
+                }
+            }
+        }
+    }
+
     private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
         for (index in 0 until numberOfPresses)
             InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt b/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt
new file mode 100644
index 0000000..f1b11f5
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+/**
+ * CompositionLocal containing the preferred content color for a given position in the hierarchy.
+ * This typically represents the `on` color for a color in `ColorScheme`. For example, if the
+ * background color is `surface`, this color is typically set to
+ * `onSurface`.
+ *
+ * This color should be used for any typography / iconography, to ensure that the color of these
+ * adjusts when the background color changes. For example, on a dark background, text should be
+ * light, and on a light background, text should be dark.
+ *
+ * Defaults to [Color.Black] if no color has been explicitly set.
+ */
+val LocalContentColor = compositionLocalOf { Color.Black }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
index b0e19e2..ebac64e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
@@ -19,4 +19,5 @@
 @RequiresOptIn(
     "This tv-material API is experimental and likely to change or be removed in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTvMaterialApi
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
new file mode 100644
index 0000000..a1cefdb
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+
+/**
+ * Material Design tab.
+ *
+ * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
+ * screens, data sets, and other interactions.
+ *
+ * This should typically be used inside of a [TabRow], see the corresponding documentation for
+ * example usage.
+ *
+ * @param selected whether this tab is selected or not
+ * @param onSelect called when this tab is selected (when in focus). Doesn't trigger if the tab is
+ * already selected
+ * @param modifier the [Modifier] to be applied to this tab
+ * @param enabled controls the enabled state of this tab. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors these will be used by the tab when in different states (focused,
+ * selected, etc.)
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this tab. You can create and pass in your own `remember`ed instance to observe [Interaction]s
+ * and customize the appearance / behavior of this tab in different states.
+ * @param content content of the [Tab]
+ */
+@Composable
+fun Tab(
+  selected: Boolean,
+  onSelect: () -> Unit,
+  modifier: Modifier = Modifier,
+  enabled: Boolean = true,
+  colors: TabColors = TabDefaults.pillIndicatorTabColors(),
+  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+  content: @Composable RowScope.() -> Unit
+) {
+  val contentColor by
+    animateColorAsState(
+      getTabContentColor(
+        colors = colors,
+        anyTabFocused = LocalTabRowHasFocus.current,
+        selected = selected,
+        enabled = enabled,
+      )
+    )
+  CompositionLocalProvider(LocalContentColor provides contentColor) {
+    Row(
+      modifier =
+        modifier
+          .semantics {
+            this.selected = selected
+            this.role = Role.Tab
+          }
+          .onFocusChanged {
+            if (it.isFocused && !selected) {
+              onSelect()
+            }
+          }
+          .focusable(enabled, interactionSource),
+      horizontalArrangement = Arrangement.Center,
+      verticalAlignment = Alignment.CenterVertically,
+      content = content
+    )
+  }
+}
+
+/**
+ * Represents the colors used in a tab in different states.
+ *
+ * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
+ * Pill indicator.
+ * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
+ * using an Underlined indicator
+ */
+class TabColors
+internal constructor(
+  private val activeContentColor: Color,
+  private val selectedContentColor: Color,
+  private val focusedContentColor: Color,
+  private val disabledActiveContentColor: Color,
+  private val disabledSelectedContentColor: Color,
+) {
+  /**
+   * Represents the content color for this tab, depending on whether it is inactive and [enabled]
+   *
+   * [Tab] is inactive when the [TabRow] is not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun inactiveContentColor(enabled: Boolean): Color {
+    return if (enabled) activeContentColor.copy(alpha = 0.4f)
+    else disabledActiveContentColor.copy(alpha = 0.4f)
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is active and [enabled]
+   *
+   * [Tab] is active when some other [Tab] is focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun activeContentColor(enabled: Boolean): Color {
+    return if (enabled) activeContentColor else disabledActiveContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is selected and [enabled]
+   *
+   * [Tab] is selected when the current [Tab] is selected and not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun selectedContentColor(enabled: Boolean): Color {
+    return if (enabled) selectedContentColor else disabledSelectedContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is focused
+   *
+   * * [Tab] is focused when the current [Tab] is selected and focused
+   */
+  internal fun focusedContentColor(): Color {
+    return focusedContentColor
+  }
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (other == null || other !is TabColors) return false
+
+    if (activeContentColor != other.activeContentColor(true)) return false
+    if (selectedContentColor != other.selectedContentColor(true)) return false
+    if (focusedContentColor != other.focusedContentColor()) return false
+
+    if (disabledActiveContentColor != other.activeContentColor(false)) return false
+    if (disabledSelectedContentColor != other.selectedContentColor(false)) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = activeContentColor.hashCode()
+    result = 31 * result + selectedContentColor.hashCode()
+    result = 31 * result + focusedContentColor.hashCode()
+    result = 31 * result + disabledActiveContentColor.hashCode()
+    result = 31 * result + disabledSelectedContentColor.hashCode()
+    return result
+  }
+}
+
+object TabDefaults {
+  /**
+   * [Tab]'s content colors to in conjunction with underlined indicator
+   */
+  // TODO: get selected & focused values from theme
+  @Composable
+  fun underlinedIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    selectedContentColor: Color = Color(0xFFC9C2E8),
+    focusedContentColor: Color = Color(0xFFC9BFFF),
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+
+  /**
+   * [Tab]'s content colors to in conjunction with pill indicator
+   */
+  // TODO: get selected & focused values from theme
+  @Composable
+  fun pillIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    selectedContentColor: Color = Color(0xFFE5DEFF),
+    focusedContentColor: Color = Color(0xFF313033),
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+}
+
+/** Returns the [Tab]'s content color based on focused/selected state */
+private fun getTabContentColor(
+  colors: TabColors,
+  anyTabFocused: Boolean,
+  selected: Boolean,
+  enabled: Boolean,
+): Color =
+  when {
+    anyTabFocused && selected -> colors.focusedContentColor()
+    selected -> colors.selectedContentColor(enabled)
+    anyTabFocused -> colors.activeContentColor(enabled)
+    else -> colors.inactiveContentColor(enabled)
+  }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
new file mode 100644
index 0000000..9fb0765
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
@@ -0,0 +1,275 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.compose.ui.zIndex
+
+/**
+ * TV-Material Design Horizontal TabRow
+ *
+ * Display all tabs in a set simultaneously and if the tabs exceed the container size, it has
+ * scrolling to navigate to next tab. They are best for switching between related content quickly,
+ * such as between transportation methods in a map. To navigate between tabs, use d-pad left or
+ * d-pad right when focused.
+ *
+ * A TvTabRow contains a row of []s, and displays an indicator underneath the currently selected
+ * tab. A TvTabRow places its tabs offset from the starting edge, and allows scrolling to tabs that
+ * are placed off screen.
+ *
+ * @param selectedTabIndex the index of the currently selected tab
+ * @param modifier the [Modifier] to be applied to this tab row
+ * @param containerColor the color used for the background of this tab row
+ * @param contentColor the primary color used in the tabs
+ * @param separator use this composable to add a separator between the tabs
+ * @param indicator used to indicate which tab is currently selected and/or focused
+ * @param tabs a composable which will render all the tabs
+ */
+@Composable
+fun TabRow(
+  selectedTabIndex: Int,
+  modifier: Modifier = Modifier,
+  containerColor: Color = TabRowDefaults.ContainerColor,
+  contentColor: Color = TabRowDefaults.contentColor(),
+  separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
+  indicator: @Composable (tabPositions: List<DpRect>) -> Unit =
+    @Composable { tabPositions ->
+      TabRowDefaults.PillIndicator(currentTabPosition = tabPositions[selectedTabIndex])
+    },
+  tabs: @Composable () -> Unit
+) {
+  val scrollState = rememberScrollState()
+  var isAnyTabFocused by remember { mutableStateOf(false) }
+
+  CompositionLocalProvider(
+    LocalTabRowHasFocus provides isAnyTabFocused,
+    LocalContentColor provides contentColor
+  ) {
+    SubcomposeLayout(
+      modifier =
+        modifier
+          .background(containerColor)
+          .clipToBounds()
+          .horizontalScroll(scrollState)
+          .onFocusChanged { isAnyTabFocused = it.hasFocus }
+          .selectableGroup()
+    ) { constraints ->
+      // Tab measurables
+      val tabMeasurables = subcompose(TabRowSlots.Tabs, tabs)
+
+      // Tab placeables
+      val tabPlaceables =
+        tabMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val tabsCount = tabMeasurables.size
+      val separatorsCount = tabsCount - 1
+
+      // Separators
+      val separators = @Composable { repeat(separatorsCount) { separator() } }
+      val separatorMeasurables = subcompose(TabRowSlots.Separator, separators)
+      val separatorPlaceables =
+        separatorMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val separatorWidth = separatorPlaceables.first().width
+
+      val layoutWidth = tabPlaceables.sumOf { it.width } + separatorsCount * separatorWidth
+      val layoutHeight =
+        (tabMeasurables.maxOfOrNull { it.maxIntrinsicHeight(Constraints.Infinity) } ?: 0)
+          .coerceAtLeast(0)
+
+      // Position the children
+      layout(layoutWidth, layoutHeight) {
+
+        // Place the tabs
+        val tabPositions = mutableListOf<DpRect>()
+        var left = 0
+        tabPlaceables.forEachIndexed { index, tabPlaceable ->
+          // place the tab
+          tabPlaceable.placeRelative(left, 0)
+
+          tabPositions.add(
+            this@SubcomposeLayout.buildTabPosition(placeable = tabPlaceable, initialLeft = left)
+          )
+          left += tabPlaceable.width
+
+          // place the separator
+          if (tabPlaceables.lastIndex != index) {
+            separatorPlaceables[index].placeRelative(left, 0)
+          }
+
+          left += separatorWidth
+        }
+
+        // Place the indicator
+        subcompose(TabRowSlots.Indicator) { indicator(tabPositions) }
+          .forEach { it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) }
+      }
+    }
+  }
+}
+
+object TabRowDefaults {
+  /** Color of the background of a tab */
+  val ContainerColor = Color.Transparent
+
+  /** Space between tabs in the tab row */
+  @Composable
+  fun TabSeparator() {
+    Spacer(modifier = Modifier.width(20.dp))
+  }
+
+  /** Default accent color for the TabRow */
+  // TODO: Use value from a theme
+  @Composable fun contentColor(): Color = Color(0xFFC9C5D0)
+
+  /**
+   * Adds a pill indicator behind the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun PillIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFE5E1E6),
+    inactiveColor: Color = Color(0xFF484362).copy(alpha = 0.4f)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val width by animateDpAsState(targetValue = currentTabPosition.width)
+    val height = currentTabPosition.height
+    val leftOffset by animateDpAsState(targetValue = currentTabPosition.left)
+    val topOffset = currentTabPosition.top
+
+    val pillColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset, y = topOffset)
+        .width(width)
+        .height(height)
+        .background(color = pillColor, shape = RoundedCornerShape(50))
+        .zIndex(-1f)
+    )
+  }
+
+  /**
+   * Adds an underlined indicator below the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun UnderlinedIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFC9BFFF),
+    inactiveColor: Color = Color(0xFFC9C2E8)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val unfocusedUnderlineWidth = 10.dp
+    val indicatorHeight = 2.dp
+    val width by
+      animateDpAsState(
+        targetValue = if (anyTabFocused) currentTabPosition.width else unfocusedUnderlineWidth
+      )
+    val leftOffset by
+      animateDpAsState(
+        targetValue =
+          if (anyTabFocused) {
+            currentTabPosition.left
+          } else {
+            val tabCenter = currentTabPosition.left + currentTabPosition.width / 2
+            tabCenter - unfocusedUnderlineWidth / 2
+          }
+      )
+
+    val underlineColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset)
+        .width(width)
+        .height(indicatorHeight)
+        .background(color = underlineColor)
+    )
+  }
+}
+
+/** A provider to store whether any [Tab] is focused inside the [TabRow] */
+internal val LocalTabRowHasFocus = compositionLocalOf { false }
+
+/** Slots for [TabRow]'s content */
+private enum class TabRowSlots {
+  Tabs,
+  Indicator,
+  Separator
+}
+
+/** Builds TabPosition based on placeable */
+private fun Density.buildTabPosition(
+  placeable: Placeable,
+  initialLeft: Int = 0,
+  initialTop: Int = 0,
+): DpRect =
+  DpRect(
+    left = initialLeft.toDp(),
+    right = (initialLeft + placeable.width).toDp(),
+    top = initialTop.toDp(),
+    bottom = (initialTop + placeable.height).toDp(),
+  )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index cc77d09..2b37161 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -345,11 +345,11 @@
         if (slideCount <= 0) {
             Box(modifier = modifier)
         } else {
-            val defaultSize = 8.dp
-            val inactiveColor = Color.LightGray
-            val activeColor = Color.White
-            val shape = CircleShape
-            val indicatorModifier = Modifier.size(defaultSize)
+            val defaultSize = remember { 8.dp }
+            val inactiveColor = remember { Color.LightGray }
+            val activeColor = remember { Color.White }
+            val shape = remember { CircleShape }
+            val indicatorModifier = remember { Modifier.size(defaultSize) }
 
             Box(modifier = modifier) {
                 Row(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 0a58e20..9fa9d6e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -22,13 +22,17 @@
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -41,6 +45,7 @@
 import androidx.tv.material.ExperimentalTvMaterialApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
 
 /**
  * This composable is intended for use in Carousel.
@@ -57,7 +62,7 @@
  * @param overlay composable defining the content overlaid on the background.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun CarouselItem(
@@ -72,22 +77,35 @@
     val overlayVisible = remember { MutableTransitionState(initialState = false) }
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
+    val bringIntoViewRequester = remember { BringIntoViewRequester() }
+    val coroutineScope = rememberCoroutineScope()
 
     LaunchedEffect(overlayVisible) {
         snapshotFlow { overlayVisible.isIdle && overlayVisible.currentState }.first { it }
         // slide has loaded completely.
         if (focusState?.isFocused == true) {
-	    focusManager.moveFocus(FocusDirection.Enter)
+            // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
+            // item is within an animation, bringIntoView scrolls excessively and loses focus.
+            // b/241591211
+            // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
+            // completed animating into position.
+            bringIntoViewRequester.bringIntoView()
+            focusManager.moveFocus(FocusDirection.Enter)
         }
     }
 
     Box(modifier = modifier
-            .onFocusChanged {
-                focusState = it
-                if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+        .bringIntoViewRequester(bringIntoViewRequester)
+        .onFocusChanged {
+            focusState = it
+            if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+                coroutineScope.launch {
+                    bringIntoViewRequester.bringIntoView()
                     focusManager.moveFocus(FocusDirection.Enter)
                 }
-             }.focusable()) {
+            }
+        }
+        .focusable()) {
         background()
 
         LaunchedEffect(overlayVisible) {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
index 091c16e..592bbf5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
@@ -26,14 +26,18 @@
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.with
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,6 +46,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.tv.material.ExperimentalTvMaterialApi
+import kotlinx.coroutines.launch
 
 /**
  * Immersive List consists of a list with multiple items and a background that displays content
@@ -57,7 +62,7 @@
  * @param list composable defining the list of items that has to be rendered.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun ImmersiveList(
@@ -69,8 +74,19 @@
 ) {
     var currentItemIndex by remember { mutableStateOf(0) }
     var listHasFocus by remember { mutableStateOf(false) }
+    val bringIntoViewRequester = remember { BringIntoViewRequester() }
+    val coroutineScope = rememberCoroutineScope()
 
-    Box(modifier) {
+    Box(modifier
+            .bringIntoViewRequester(bringIntoViewRequester)
+            .onFocusChanged {
+                if (it.isFocused) {
+                    coroutineScope.launch {
+                        bringIntoViewRequester.bringIntoView()
+                    }
+                }
+            }
+    ) {
         ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
 
         val focusManager = LocalFocusManager.current
diff --git a/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt b/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
index 70aa8ed..a328f50 100644
--- a/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
+++ b/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
@@ -58,15 +58,12 @@
 
     companion object {
         private const val numPages = 3
-        private var mSystemWindowInsetsConsumedField: Field? = null
-
-        init {
+        private val mSystemWindowInsetsConsumedField: Field? by lazy {
             // Only need reflection on API < 29 to create an unconsumed WindowInsets.
             // On API 29+, a new builder is used that will do that for us.
             if (Build.VERSION.SDK_INT < 29) {
-                mSystemWindowInsetsConsumedField = field("mSystemWindowInsetsConsumed")
-                mSystemWindowInsetsConsumedField!!.isAccessible = true
-            }
+                field("mSystemWindowInsetsConsumed").also { it.isAccessible = true }
+            } else null
         }
 
         @Suppress("SameParameterValue")
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index f055c6f..0b8d171 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -13,6 +13,7 @@
     api("androidx.recyclerview:recyclerview:1.1.0")
     api("androidx.core:core:1.6.0")
     api("androidx.versionedparcelable:versionedparcelable:1.1.1")
+    api('androidx.dynamicanimation:dynamicanimation:1.0.0')
 
     androidTestImplementation(project(":test:screenshot:screenshot"))
     androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
index 8638f8b..3a12522 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
@@ -24,8 +24,6 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -41,41 +39,26 @@
 @UiThread
 class SwipeDismissController extends DismissController {
     private static final String TAG = "SwipeDismissController";
-
     public static final float DEFAULT_DISMISS_DRAG_WIDTH_RATIO = .33f;
+    private float mDismissMinDragWidthRatio = DEFAULT_DISMISS_DRAG_WIDTH_RATIO;
     // A value between 0.0 and 1.0 determining the percentage of the screen on the left-hand-side
     // where edge swipe gestures are permitted to begin.
     private static final float EDGE_SWIPE_THRESHOLD = 0.1f;
-    private static final float TRANSLATION_MIN_ALPHA = 0.5f;
-    private static final float DEFAULT_INTERPOLATION_FACTOR = 1.5f;
-
+    private static final int VELOCITY_UNIT = 1000;
     // Cached ViewConfiguration and system-wide constant value
-    private int mSlop;
-    private int mMinFlingVelocity;
-    private float mGestureThresholdPx;
-
-    // Transient properties
+    private final int mSlop;
+    private final int mMinFlingVelocity;
+    private final float mGestureThresholdPx;
+    private final SwipeDismissTransitionHelper mSwipeDismissTransitionHelper;
     private int mActiveTouchId;
     private float mDownX;
     private float mDownY;
+    private float mLastX;
     private boolean mSwiping;
-    // This variable holds information about whether the initial move of a longer swipe
-    // (consisting of multiple move events) has conformed to the definition of a horizontal
-    // swipe-to-dismiss. A swipe gesture is only ever allowed to be recognized if this variable is
-    // set to true. Otherwise, the motion events will be allowed to propagate to the children.
-    private boolean mCanStartSwipe = true;
     private boolean mDismissed;
     private boolean mDiscardIntercept;
-    private VelocityTracker mVelocityTracker;
-    private float mTranslationX;
-    private float mLastX;
-    private float mDismissMinDragWidthRatio = DEFAULT_DISMISS_DRAG_WIDTH_RATIO;
-    boolean mStarted;
-    final int mAnimationTime;
 
-    final DecelerateInterpolator mCancelInterpolator;
-    final AccelerateInterpolator mDismissInterpolator;
-    final DecelerateInterpolator mCompleteDismissGestureInterpolator;
+    private boolean mBlockGesture = false;
 
     SwipeDismissController(Context context, DismissibleFrameLayout layout) {
         super(context, layout);
@@ -85,12 +68,8 @@
         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
         mGestureThresholdPx =
                 Resources.getSystem().getDisplayMetrics().widthPixels * EDGE_SWIPE_THRESHOLD;
-        mAnimationTime = context.getResources().getInteger(
-                android.R.integer.config_shortAnimTime);
-        mCancelInterpolator = new DecelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
-        mDismissInterpolator = new AccelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
-        mCompleteDismissGestureInterpolator = new DecelerateInterpolator(
-                DEFAULT_INTERPOLATION_FACTOR);
+
+        mSwipeDismissTransitionHelper = new SwipeDismissTransitionHelper(context, layout);
     }
 
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -108,8 +87,16 @@
     }
 
     boolean onInterceptTouchEvent(MotionEvent ev) {
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+        checkGesture(ev);
+        if (mBlockGesture) {
+            return true;
+        }
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        float offsetX = ev.getRawX() - ev.getX();
+        float offsetY = 0.0f;
+        ev.offsetLocation(offsetX, offsetY);
 
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
@@ -117,8 +104,8 @@
                 mDownX = ev.getRawX();
                 mDownY = ev.getRawY();
                 mActiveTouchId = ev.getPointerId(0);
-                mVelocityTracker = VelocityTracker.obtain();
-                mVelocityTracker.addMovement(ev);
+                mSwipeDismissTransitionHelper.obtainVelocityTracker();
+                mSwipeDismissTransitionHelper.getVelocityTracker().addMovement(ev);
                 break;
 
             case MotionEvent.ACTION_POINTER_DOWN:
@@ -141,7 +128,8 @@
                 break;
 
             case MotionEvent.ACTION_MOVE:
-                if (mVelocityTracker == null || mDiscardIntercept) {
+                if (mSwipeDismissTransitionHelper.getVelocityTracker() == null
+                        || mDiscardIntercept) {
                     break;
                 }
 
@@ -155,15 +143,15 @@
                 float x = ev.getX(pointerIndex);
                 float y = ev.getY(pointerIndex);
 
-                if (dx != 0 && mDownX >= mGestureThresholdPx
-                        && canScroll(mLayout, false, dx, x, y)) {
+                if (dx != 0 && mDownX >= mGestureThresholdPx && canScroll(mLayout, false, dx, x,
+                        y)) {
                     mDiscardIntercept = true;
                     break;
                 }
                 updateSwiping(ev);
                 break;
         }
-
+        ev.offsetLocation(-offsetX, -offsetY);
         return (!mDiscardIntercept && mSwiping);
     }
 
@@ -187,114 +175,61 @@
     }
 
     public boolean onTouchEvent(@NonNull MotionEvent ev) {
-        if (mVelocityTracker == null) {
+        checkGesture(ev);
+        if (mBlockGesture) {
+            return true;
+        }
+
+        if (mSwipeDismissTransitionHelper.getVelocityTracker() == null) {
             return false;
         }
 
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        float offsetX = ev.getRawX() - ev.getX();
+        float offsetY = 0.0f;
+        ev.offsetLocation(offsetX, offsetY);
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_UP:
                 updateDismiss(ev);
+                // Fall through, don't update gesture tracker with the event for ACTION_CANCEL
+            case MotionEvent.ACTION_CANCEL:
                 if (mDismissed) {
-                    dismiss();
-                } else if (mSwiping) {
-                    cancel();
+                    mSwipeDismissTransitionHelper.animateDismissal(mDismissListener);
+                } else if (mSwiping
+                        // Only trigger animation if we had a MOVE event that would shift the
+                        // underlying view, otherwise the animation would be janky.
+                        && mLastX != Integer.MIN_VALUE) {
+                    mSwipeDismissTransitionHelper.animateRecovery(mDismissListener);
                 }
                 resetSwipeDetectMembers();
                 break;
-
-            case MotionEvent.ACTION_CANCEL:
-                cancel();
-                resetSwipeDetectMembers();
-                break;
-
             case MotionEvent.ACTION_MOVE:
-                mVelocityTracker.addMovement(ev);
+                mSwipeDismissTransitionHelper.getVelocityTracker().addMovement(ev);
                 mLastX = ev.getRawX();
                 updateSwiping(ev);
                 if (mSwiping) {
-                    setProgress(ev.getRawX() - mDownX);
+                    mSwipeDismissTransitionHelper.onSwipeProgressChanged(ev.getRawX() - mDownX, ev);
                     break;
                 }
         }
+        ev.offsetLocation(-offsetX, -offsetY);
         return true;
     }
 
-    private void setProgress(float deltaX) {
-        mTranslationX = deltaX;
-        mLayout.setTranslationX(deltaX);
-        mLayout.setAlpha(1 - (deltaX / mLayout.getWidth() * TRANSLATION_MIN_ALPHA));
-        mStarted = true;
-
-        if (mDismissListener != null && deltaX >= 0) {
-            mDismissListener.onDismissStarted();
-        }
-    }
-
-    void dismiss() {
-        mLayout.animate()
-                .translationX(mLayout.getWidth())
-                .alpha(0)
-                .setDuration(mAnimationTime)
-                .setInterpolator(
-                        mStarted ? mCompleteDismissGestureInterpolator
-                                : mDismissInterpolator)
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mDismissListener != null) {
-                                    mDismissListener.onDismissed();
-                                }
-                                resetTranslationAndAlpha();
-                            }
-                        });
-    }
-
-    void cancel() {
-        mStarted = false;
-        mLayout.animate()
-                .translationX(0)
-                .alpha(1)
-                .setDuration(mAnimationTime)
-                .setInterpolator(mCancelInterpolator)
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mDismissListener != null) {
-                                    mDismissListener.onDismissCanceled();
-                                }
-                                resetTranslationAndAlpha();
-                            }
-                        });
-    }
-
-    /**
-     * Resets this view to the original state. This method cancels any pending animations on this
-     * view and resets the alpha as well as x translation values.
-     */
-    void resetTranslationAndAlpha() {
-        mLayout.animate().cancel();
-        mLayout.setTranslationX(0);
-        mLayout.setAlpha(1);
-        mStarted = false;
-    }
-
     /** Resets internal members when canceling or finishing a given gesture. */
     private void resetSwipeDetectMembers() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
+        if (mSwipeDismissTransitionHelper.getVelocityTracker() != null) {
+            mSwipeDismissTransitionHelper.getVelocityTracker().recycle();
         }
-        mVelocityTracker = null;
-        mTranslationX = 0;
+        mSwipeDismissTransitionHelper.resetVelocityTracker();
         mDownX = 0;
         mDownY = 0;
         mSwiping = false;
+        mLastX = Integer.MIN_VALUE;
         mDismissed = false;
         mDiscardIntercept = false;
-        mCanStartSwipe = true;
     }
 
     private void updateSwiping(MotionEvent ev) {
@@ -302,33 +237,40 @@
             float deltaX = ev.getRawX() - mDownX;
             float deltaY = ev.getRawY() - mDownY;
             if (isPotentialSwipe(deltaX, deltaY)) {
-                // There are three conditions on which we want want to start swiping:
-                // 1. The swipe is from left to right AND
-                // 2. It is horizontal AND
-                // 3. We actually can start swiping
-                mSwiping = mCanStartSwipe && Math.abs(deltaY) < Math.abs(deltaX) && deltaX > 0;
-                mCanStartSwipe = mSwiping;
+                mSwiping = deltaX > mSlop * 2
+                        && Math.abs(deltaY) < Math.abs(deltaX);
+            } else {
+                mSwiping = false;
             }
         }
     }
 
     private void updateDismiss(@NonNull MotionEvent ev) {
         float deltaX = ev.getRawX() - mDownX;
-        mVelocityTracker.addMovement(ev);
-        mVelocityTracker.computeCurrentVelocity(1000);
+        // Don't add the motion event as an UP event would clear the velocity tracker
+        VelocityTracker velocityTracker = mSwipeDismissTransitionHelper.getVelocityTracker();
+        velocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        float xVelocity = velocityTracker.getXVelocity();
+        float yVelocity = velocityTracker.getYVelocity();
+        if (mLastX == Integer.MIN_VALUE) {
+            // If there's no changes to mLastX, we have only one point of data, and therefore no
+            // velocity. Estimate velocity from just the up and down event in that case.
+            xVelocity = deltaX / ((ev.getEventTime() - ev.getDownTime()) / 1000f);
+        }
+
         if (!mDismissed) {
             if ((deltaX > (mLayout.getWidth() * mDismissMinDragWidthRatio)
                     && ev.getRawX() >= mLastX)
-                    || (mVelocityTracker.getXVelocity() >= mMinFlingVelocity
-                    && mVelocityTracker.getXVelocity() > Math.abs(
-                    mVelocityTracker.getYVelocity()))) {
+                    || (xVelocity >= mMinFlingVelocity
+                    && xVelocity > Math.abs(
+                    yVelocity)))  {
                 mDismissed = true;
             }
         }
         // Check if the user tried to undo this.
         if (mDismissed && mSwiping) {
             // Check if the user's finger is actually flinging back to left
-            if (mVelocityTracker.getXVelocity() < -mMinFlingVelocity) {
+            if (xVelocity < -mMinFlingVelocity) {
                 mDismissed = false;
             }
         }
@@ -367,4 +309,10 @@
 
         return checkV && v.canScrollHorizontally((int) -dx);
     }
-}
+
+    private void checkGesture(MotionEvent ev) {
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mBlockGesture = mSwipeDismissTransitionHelper.isAnimating();
+        }
+    }
+}
\ No newline at end of file
diff --git a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java
new file mode 100644
index 0000000..75ff715
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java
@@ -0,0 +1,340 @@
+/*
+ * 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.wear.widget;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatValueHolder;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * A helper class to handle transition of swiping to dismiss and dismiss animation.
+ */
+class SwipeDismissTransitionHelper {
+
+    private static final String TAG = "SwipeDismissTransitionHelper";
+    private static final float SCALE_MIN = 0.7f;
+    private static final float SCALE_MAX = 1.0f;
+    public static final float SCRIM_BACKGROUND_MAX = 0.5f;
+    private static final float DIM_FOREGROUND_PROGRESS_FACTOR = 2.0f;
+    private static final float DIM_FOREGROUND_MIN = 0.3f;
+    private static final int VELOCITY_UNIT = 1000;
+    // Spring properties
+    private static final float SPRING_STIFFNESS = 600f;
+    private static final float SPRING_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY;
+    private static final float SPRING_MIN_VISIBLE_CHANGE = 0.5f;
+    private static final int SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX = 5;
+    private final DismissibleFrameLayout mLayout;
+
+    private final int mScreenWidth;
+    private final SparseArray<ColorFilter> mDimmingColorFilterCache = new SparseArray<>();
+    private final View mScrimBackground;
+    private final boolean mIsScreenRound;
+    private final Paint mCompositingPaint = new Paint();
+
+    private VelocityTracker mVelocityTracker;
+    private boolean mStarted;
+    private int mOriginalViewWidth;
+    private float mTranslationX;
+    private float mScale;
+    private float mProgress;
+    private float mDimming;
+    private SpringAnimation mDismissalSpring;
+    private SpringAnimation mRecoverySpring;
+
+    SwipeDismissTransitionHelper(@NonNull Context context,
+            @NonNull DismissibleFrameLayout layout) {
+        mLayout = layout;
+        mIsScreenRound = layout.getResources().getConfiguration().isScreenRound();
+        mScreenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
+        mScrimBackground = new View(context);
+        clipOutline(mScrimBackground, mIsScreenRound);
+        mScrimBackground.setLayoutParams(
+                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        mScrimBackground.setBackgroundColor(Color.BLACK);
+    }
+
+    private static void clipOutline(@NonNull View view, boolean useRoundShape) {
+        view.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                if (useRoundShape) {
+                    outline.setOval(0, 0, view.getWidth(), view.getHeight());
+                } else {
+                    outline.setRect(0, 0, view.getWidth(), view.getHeight());
+                }
+                outline.setAlpha(0);
+            }
+        });
+        view.setClipToOutline(true);
+    }
+
+
+    private static float lerp(float min, float max, float value) {
+        return min + (max - min) * value;
+    }
+
+    private static float clamp(float min, float max, float value) {
+        return max(min, min(max, value));
+    }
+
+    private static float lerpInv(float min, float max, float value) {
+        return min != max ? ((value - min) / (max - min)) : 0.0f;
+    }
+
+    private ColorFilter createDimmingColorFilter(float level) {
+        level = clamp(0, 1, level);
+        int alpha = (int) (0xFF * level);
+        int color = Color.argb(alpha, 0, 0, 0);
+        ColorFilter colorFilter = mDimmingColorFilterCache.get(alpha);
+        if (colorFilter != null) {
+            return colorFilter;
+        }
+        colorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+        mDimmingColorFilterCache.put(alpha, colorFilter);
+        return colorFilter;
+    }
+
+    private SpringAnimation createSpringAnimation(float startValue,
+            float finalValue,
+            float startVelocity,
+            DynamicAnimation.OnAnimationUpdateListener onUpdateListener,
+            DynamicAnimation.OnAnimationEndListener onEndListener) {
+        SpringAnimation animation = new SpringAnimation(new FloatValueHolder());
+        animation.setStartValue(startValue);
+        animation.setMinimumVisibleChange(SPRING_MIN_VISIBLE_CHANGE);
+        SpringForce spring = new SpringForce();
+        spring.setFinalPosition(finalValue);
+        spring.setDampingRatio(SPRING_DAMPING_RATIO);
+        spring.setStiffness(SPRING_STIFFNESS);
+        animation.setMinValue(0.0f);
+        animation.setMaxValue(mScreenWidth);
+        animation.setStartVelocity(startVelocity);
+        animation.setSpring(spring);
+        animation.addUpdateListener(onUpdateListener);
+        animation.addEndListener(onEndListener);
+        animation.start();
+        return animation;
+    }
+
+    /**
+     * Updates the swipe progress
+     *
+     * @param deltaX The X delta of gesture
+     * @param ev     The motion event
+     */
+    void onSwipeProgressChanged(float deltaX, @NonNull MotionEvent ev) {
+        if (!mStarted) {
+            initializeTransition();
+        }
+
+        mVelocityTracker.addMovement(ev);
+        mOriginalViewWidth = mLayout.getWidth();
+        // For swiping, mProgress is directly manipulated
+        // mProgress = 0 (no swipe) - 0.5 (swiped to mid screen) - 1 (swipe to right of screen)
+        mProgress = deltaX / mOriginalViewWidth;
+        // Solve for other variables
+        // Scale = lerp 100% -> 70% when swiping from left edge to right edge
+        mScale = lerp(SCALE_MAX, SCALE_MIN, mProgress);
+        // Translation: make sure the right edge of mOriginalView touches right edge of screen
+        mTranslationX = max(0f, 1 - mScale) * mLayout.getWidth() / 2.0f;
+        mDimming = Math.min(DIM_FOREGROUND_MIN, mProgress / DIM_FOREGROUND_PROGRESS_FACTOR);
+
+        updateView();
+    }
+
+    private void onDismissalRecoveryAnimationProgressChanged(float translationX) {
+        mOriginalViewWidth = mLayout.getWidth();
+        mTranslationX = translationX;
+
+        mScale = 1 - mTranslationX * 2 / mOriginalViewWidth;
+        // Clamp mScale so that we can solve for mProgress
+        mScale = Math.max(SCALE_MIN, Math.min(mScale, SCALE_MAX));
+        float nextProgress = lerpInv(SCALE_MAX, SCALE_MIN, mScale);
+        if (nextProgress > mProgress) {
+            mProgress = nextProgress;
+        }
+        mDimming = Math.min(DIM_FOREGROUND_MIN, mProgress / DIM_FOREGROUND_PROGRESS_FACTOR);
+        updateView();
+    }
+
+    private void updateView() {
+        mLayout.setScaleX(mScale);
+        mLayout.setScaleY(mScale);
+        mLayout.setTranslationX(mTranslationX);
+        updateDim();
+        updateScrim();
+    }
+
+    private void updateDim() {
+        mCompositingPaint.setColorFilter(createDimmingColorFilter(mDimming));
+        mLayout.setLayerPaint(mCompositingPaint);
+    }
+
+    private void updateScrim() {
+        float alpha =  SCRIM_BACKGROUND_MAX * (1 - mProgress);
+        mScrimBackground.setAlpha(alpha);
+    }
+
+    private void initializeTransition() {
+        mStarted = true;
+        ViewGroup originalParentView = getOriginalParentView();
+        ViewParent scrimBackgroundParent = mScrimBackground.getParent();
+
+        if (originalParentView == null) return;
+
+        // Check if scrim background is already attached to the parent view.
+        if (scrimBackgroundParent != originalParentView) {
+            originalParentView.addView(mScrimBackground);
+            mLayout.bringToFront();
+        }
+
+        mCompositingPaint.setColorFilter(null);
+        mLayout.setLayerType(View.LAYER_TYPE_HARDWARE, mCompositingPaint);
+        clipOutline(mLayout, mIsScreenRound);
+    }
+
+    private void resetTranslationAndAlpha() {
+        // resetting variables
+        mStarted = false;
+        mTranslationX = 0;
+        mProgress = 0;
+        mScale = 1;
+        // resetting layout params
+        mLayout.setTranslationX(0);
+        mLayout.setScaleX(1);
+        mLayout.setScaleY(1);
+        mLayout.setAlpha(1);
+        mScrimBackground.setAlpha(0);
+
+        mCompositingPaint.setColorFilter(null);
+        mLayout.setLayerType(View.LAYER_TYPE_NONE, null);
+        mLayout.setClipToOutline(false);
+    }
+
+    /**
+     * @return If dismiss or recovery animation is running.
+     */
+    boolean isAnimating() {
+        return (mDismissalSpring != null && mDismissalSpring.isRunning()) || (
+                mRecoverySpring != null && mRecoverySpring.isRunning());
+    }
+
+    /**
+     * Triggers the recovery animation.
+     */
+    void animateRecovery(@Nullable DismissController.OnDismissListener dismissListener) {
+        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        mRecoverySpring = createSpringAnimation(mTranslationX, 0, mVelocityTracker.getXVelocity(),
+                (animation, value, velocity) -> {
+                    float distanceRemaining = Math.max(0, (value - 0));
+                    if (distanceRemaining <= SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX
+                            && mRecoverySpring != null) {
+                        // Skip last 2% of animation.
+                        mRecoverySpring.skipToEnd();
+                    }
+                    onDismissalRecoveryAnimationProgressChanged(value);
+                }, (animation, canceled, value, velocity) -> {
+
+                    resetTranslationAndAlpha();
+                    if (dismissListener != null) {
+                        dismissListener.onDismissCanceled();
+                    }
+                });
+    }
+
+    /**
+     * Triggers the dismiss animation.
+     */
+    void animateDismissal(@Nullable DismissController.OnDismissListener dismissListener) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        // Dismissal has started
+        if (dismissListener != null) {
+            dismissListener.onDismissStarted();
+        }
+
+        mDismissalSpring = createSpringAnimation(mTranslationX, mScreenWidth,
+                mVelocityTracker.getXVelocity(), (animation, value, velocity) -> {
+                    float distanceRemaining = Math.max(0, (mScreenWidth - value));
+                    if (distanceRemaining <= SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX
+                            && mDismissalSpring != null) {
+                        // Skip last 2% of animation.
+                        mDismissalSpring.skipToEnd();
+                    }
+                    onDismissalRecoveryAnimationProgressChanged(value);
+                }, (animation, canceled, value, velocity) -> {
+                    resetTranslationAndAlpha();
+                    if (dismissListener != null) {
+                        dismissListener.onDismissed();
+                    }
+                });
+    }
+
+    private @Nullable ViewGroup getOriginalParentView() {
+        if (mLayout.getParent() instanceof ViewGroup) {
+            return (ViewGroup) mLayout.getParent();
+        }
+        return null;
+    }
+
+    /**
+     * @return The velocity tracker.
+     */
+    @Nullable
+    VelocityTracker getVelocityTracker() {
+        return mVelocityTracker;
+    }
+
+    /**
+     * Obtain velocity tracker.
+     */
+    void obtainVelocityTracker() {
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    /**
+     * Reset velocity tracker to null.
+     */
+    void resetVelocityTracker() {
+        mVelocityTracker = null;
+    }
+}