Merge "Update androidx-core in the Compose ui module to latest version." 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/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
index 7ffb7e9..bc46326 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -72,7 +72,7 @@
     private final int mRewriteSearchSpecLatencyMillis;
     /** Time used to rewrite the search results. */
     private final int mRewriteSearchResultLatencyMillis;
-    /** Time passed while waiting to acquire the lock during Java function calls. **/
+    /** Time passed while waiting to acquire the lock during Java function calls. */
     private final int mJavaLockAcquisitionLatencyMillis;
     /**
      * Time spent on ACL checking. This is the time spent filtering namespaces based on package
@@ -200,7 +200,7 @@
         return mRewriteSearchResultLatencyMillis;
     }
 
-    /** Returns time passed while waiting to acquire the lock during Java function calls **/
+    /** Returns time passed while waiting to acquire the lock during Java function calls */
     public int getJavaLockAcquisitionLatencyMillis() {
         return mJavaLockAcquisitionLatencyMillis;
     }
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index c9e4a8b..c82c623 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -22,6 +22,9 @@
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled true
+    }
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
@@ -48,6 +51,7 @@
     // This dependency is unused by the test implementation, but it's here to validate that
     // icing's jarjar'ing of the Protobuf_lite doesn't conflict with external dependencies.
     androidTestImplementation(libs.protobufLite)
+    androidTestImplementation(libs.multidex)
 }
 
 androidx {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index ddb7093..00ad136 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
 import android.content.Context;
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-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index 3e74e58..1b8fa29 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -97,7 +97,19 @@
                 val path = "$UNBUNDLED_PERFETTO_ROOT_DIR/config.pb"
                 // Move the config to a directory that unbundled perfetto has permissions for.
                 Shell.executeScriptSilent("rm -f $path")
-                Shell.executeScriptSilent("mv $configFilePath $path")
+                if (Build.VERSION.SDK_INT >= 24) {
+                    Shell.executeScriptSilent("mv $configFilePath $path")
+                } else {
+                    // Observed stderr output (though command still completes successfully) on:
+                    // google/shamu/shamu:6.0.1/MOB31T/3671974:userdebug/dev-keys
+                    // Doesn't repro on all API 23 devices :|
+                    Shell.executeScriptCaptureStdoutStderr("mv $configFilePath $path").also {
+                        check(
+                            it.stdout.isBlank() &&
+                                (it.stderr.isBlank() || it.stderr.startsWith("mv: chown"))
+                        )
+                    }
+                }
                 path
             } else {
                 configFilePath
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 6ef1b7f..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
@@ -93,7 +93,6 @@
             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(
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 39f283b..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,18 +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"
-        }
-        // Delete the generated Info.plist file given our source folders should be clean.
+        // Copy generated `App.plist` and `Benchmark.plist` files.
         // Context: b/258545725
-        val deleted = sourceFile.deleteRecursively()
-        require(deleted) {
-            "Unable to delete $sourceFile"
+        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 66f15d8..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,6 +49,8 @@
     @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
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
index 4820dcf..bfa0fc6 100644
--- 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
@@ -30,19 +30,22 @@
     private val destination: String
 ) {
 
-    private var device: String? = null
+    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 {
-            device = boot(destination, execOperations)
-            block(device!!)
+            val instance = boot(destination, execOperations)
+            destinationDesc = instance.destinationDesc
+            deviceId = instance.deviceId
+            block(destinationDesc!!)
         } finally {
-            val id = device
+            val id = deviceId
             if (id != null) {
-                shutDownAndDelete(execOperations)
+                shutDownAndDelete(execOperations, id)
             }
         }
     }
@@ -54,16 +57,24 @@
         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
-        ): String {
+        ): 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 -> destination
+                else -> SimulatorInstance(destinationDesc = destination)
             }
         }
 
@@ -89,7 +100,7 @@
             destination: String,
             parsed: Map<String, String>,
             execOperations: ExecOperations
-        ): String {
+        ): SimulatorInstance {
             val deviceName = deviceName(parsed)
             val supported = discoverSimulatorRuntimeVersion(execOperations)
             // While this is not strictly correct, these versions should be pretty close.
@@ -113,17 +124,20 @@
             executeCommand(
                 execOperations, listOf("xcrun", "simctl", "boot", deviceId)
             )
-            // Return a new descriptor
-            return "id=$deviceId"
+            // Return a simulator instance with the new descriptor + device id
+            return SimulatorInstance(destinationDesc = "id=$deviceId", deviceId = deviceId)
         }
 
-        internal fun shutDownAndDelete(execOperations: ExecOperations) {
-            // Cleans up all running simulators.
+        internal fun shutDownAndDelete(
+            execOperations: ExecOperations,
+            deviceId: String
+        ) {
+            // Cleans up the instance of the simulator that was booted up.
             executeCommand(
-                execOperations, listOf("xcrun", "simctl", "shutdown", "all")
+                execOperations, listOf("xcrun", "simctl", "shutdown", deviceId)
             )
             executeCommand(
-                execOperations, listOf("xcrun", "simctl", "delete", "all")
+                execOperations, listOf("xcrun", "simctl", "delete", deviceId)
             )
         }
 
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 42177bd..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:
diff --git a/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml b/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
index 79abc41..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:
diff --git a/benchmark/benchmark-junit4/build.gradle b/benchmark/benchmark-junit4/build.gradle
index 3ee2141..ee2aaf0 100644
--- a/benchmark/benchmark-junit4/build.gradle
+++ b/benchmark/benchmark-junit4/build.gradle
@@ -37,7 +37,7 @@
 
     implementation("androidx.test:rules:1.4.0")
     implementation("androidx.test:runner:1.4.0")
-    implementation("androidx.tracing:tracing-ktx:1.0.0")
+    implementation("androidx.tracing:tracing-ktx:1.1.0")
     api("androidx.annotation:annotation:1.1.0")
 
     androidTestImplementation(project(":internal-testutils-ktx"))
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/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
new file mode 100644
index 0000000..de9746e
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.macro
+
+@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 4edcfaa..9c38c66 100755
--- a/busytown/androidx-native-mac-host-tests.sh
+++ b/busytown/androidx-native-mac-host-tests.sh
@@ -10,6 +10,9 @@
 
 cd "$(dirname $0)"
 
+# Setup simulators
+impl/androidx-native-mac-simulator-setup.sh
+
 impl/build.sh darwinBenchmarkResults allTests \
     --no-configuration-cache \
     -Pandroidx.ignoreTestFailures \
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-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
new file mode 100644
index 0000000..7bbc024
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.display.DisplayManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assume
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@Suppress("DEPRECATION") // getRealSize
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class DisplayInfoManagerTest {
+    private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+
+    @Test
+    fun defaultDisplayIsDeviceDisplay_whenOneDisplay() {
+        // Arrange
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+
+        Assume.assumeTrue(displayManager.displays.size == 1)
+
+        val currentDisplaySize = Point()
+        displayManager.displays[0].getRealSize(currentDisplaySize)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(currentDisplaySize, size)
+    }
+
+    @Test
+    fun previewSizeAreaIsWithinMaxPreviewArea() {
+        // Act & Assert
+        val previewSize = displayInfoManager.previewSize
+        assertTrue("$previewSize has larger area than 1920 * 1080",
+            previewSize.width * previewSize.height <= 1920 * 1080)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index fc355fe..9b9c238 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -42,7 +42,8 @@
 import androidx.camera.core.impl.SurfaceSizeDefinition
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.utils.AspectRatioUtil
-import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.internal.utils.SizeUtil
@@ -85,6 +86,8 @@
     internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val displayManager: DisplayManager =
         (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
+    private val activeArraySize =
+        cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]
 
     init {
         checkCapabilities()
@@ -583,10 +586,90 @@
     }
 
     /**
-     * Obtains the target aspect ratio from ImageOutputConfig
+     * Returns the aspect ratio group key of the target size when grouping the input resolution
+     * candidate list.
+     *
+     * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
+     * also need to consider the mod 16 factor to find which aspect ratio of group the target size
+     * might be put in. So that sizes of the group will be selected to use in the highest priority.
      */
-    private fun getTargetAspectRatio(imageOutputConfig: ImageOutputConfig): Rational? {
-        val targetSize = getTargetSize(imageOutputConfig)
+    private fun getAspectRatioGroupKeyOfTargetSize(
+        targetSize: Size?,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
+        if (targetSize == null) {
+            return null
+        }
+
+        val aspectRatios = getResolutionListGroupingAspectRatioKeys(
+            resolutionCandidateList
+        )
+        aspectRatios.forEach {
+            if (hasMatchingAspectRatio(targetSize, it)) {
+                return it
+            }
+        }
+        return Rational(targetSize.width, targetSize.height)
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    private fun getResolutionListGroupingAspectRatioKeys(
+        resolutionCandidateList: List<Size>
+    ): List<Rational> {
+        val aspectRatios: MutableList<Rational> = mutableListOf()
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_4_3)
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_16_9)
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        resolutionCandidateList.forEach { size ->
+            val newRatio = Rational(size.width, size.height)
+            var aspectRatioFound = aspectRatios.contains(newRatio)
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                var hasMatchingAspectRatio = false
+                aspectRatios.forEach loop@{ aspectRatio ->
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true
+                        return@loop
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio)
+                }
+            }
+        }
+        return aspectRatios
+    }
+
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by TargetAspectRatio quirk (not implemented yet).
+     * 2. The use case's original aspect ratio if TargetAspectRatio quirk returns RATIO_ORIGINAL
+     * and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if TargetAspectRatio quirk returns
+     * RATIO_ORIGINAL and the use case has no target aspect ratio but has target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private fun getTargetAspectRatio(
+        imageOutputConfig: ImageOutputConfig,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
         var outputRatio: Rational? = null
         // TODO(b/245622117) Get the corrected aspect ratio from quirks instead of always using
         //  TargetAspectRatio.RATIO_ORIGINAL
@@ -603,11 +686,17 @@
                     "Undefined target aspect ratio: $aspectRatio"
                 )
             }
-        } else if (targetSize != null) {
-            // Target size is calculated from the target resolution. If target size is not
-            // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-            // will be selected in priority.
-            outputRatio = Rational(targetSize.width, targetSize.height)
+        } else {
+            // The legacy resolution API will use the aspect ratio of the target size to
+            // be the fallback target aspect ratio value when the use case has no target
+            // aspect ratio setting.
+            val targetSize = getTargetSize(imageOutputConfig)
+            if (targetSize != null) {
+                outputRatio = getAspectRatioGroupKeyOfTargetSize(
+                    targetSize,
+                    resolutionCandidateList
+                )
+            }
         }
         return outputRatio
     }
@@ -656,33 +745,22 @@
      * Groups sizes together according to their aspect ratios.
      */
     private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
-        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = java.util.HashMap()
+        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = mutableMapOf()
 
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_4_3] = ArrayList()
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_16_9] = ArrayList()
-        for (outputSize in sizes) {
-            var matchedKey: Rational? = null
-            for (key in aspectRatioSizeListMap.keys) {
+        val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+        aspectRatioKeys.forEach {
+            aspectRatioSizeListMap[it] = mutableListOf()
+        }
+
+        sizes.forEach { size ->
+            aspectRatioSizeListMap.keys.forEach { aspectRatio ->
                 // Put the size into all groups that is matched in mod16 condition since a size
                 // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key
-                    val sizeList = aspectRatioSizeListMap[matchedKey]!!
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize)
-                    }
+                if (hasMatchingAspectRatio(size, aspectRatio)) {
+                    aspectRatioSizeListMap[aspectRatio]?.add(size)
                 }
             }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap[Rational(outputSize.width, outputSize.height)] =
-                    ArrayList(setOf(outputSize))
-            }
         }
         return aspectRatioSizeListMap
     }
@@ -714,8 +792,8 @@
         // result.
         Arrays.sort(outputSizes, CompareSizesByArea(true))
         var targetSize: Size? = getTargetSize(imageOutputConfig)
-        var minSize = SizeUtil.RESOLUTION_VGA
-        val defaultSizeArea = SizeUtil.getArea(SizeUtil.RESOLUTION_VGA)
+        var minSize = RESOLUTION_VGA
+        val defaultSizeArea = SizeUtil.getArea(RESOLUTION_VGA)
         val maxSizeArea = SizeUtil.getArea(maxSize)
         // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
         // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
@@ -742,7 +820,8 @@
                     imageFormat
             )
         }
-        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig)
+
+        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates)
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize ?: imageOutputConfig.getDefaultResolution(null)
@@ -773,9 +852,17 @@
 
             // Sort the aspect ratio key set by the target aspect ratio.
             val aspectRatios: List<Rational?> = ArrayList(aspectRatioSizeListMap.keys)
+            val fullFovRatio = if (activeArraySize != null) {
+                Rational(activeArraySize.width(), activeArraySize.height())
+            } else {
+                null
+            }
             Collections.sort(
                 aspectRatios,
-                CompareAspectRatiosByDistanceToTargetRatio(aspectRatio)
+                CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                    aspectRatio,
+                    fullFovRatio
+                )
             )
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index 1babffd..c7745f6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -19,6 +19,9 @@
 import android.content.Context
 import android.graphics.Point
 import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.os.Handler
+import android.os.Looper
 import android.util.Size
 import android.view.Display
 import androidx.annotation.RequiresApi
@@ -31,45 +34,86 @@
 class DisplayInfoManager @Inject constructor(context: Context) {
     private val MAX_PREVIEW_SIZE = Size(1920, 1080)
 
+    companion object {
+        private var lazyMaxDisplay: Display? = null
+        private var lazyPreviewSize: Size? = null
+
+        internal fun invalidateLazyFields() {
+            lazyMaxDisplay = null
+            lazyPreviewSize = null
+        }
+
+        internal val displayListener by lazy {
+            object : DisplayListener {
+                override fun onDisplayAdded(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayRemoved(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayChanged(displayId: Int) {
+                    invalidateLazyFields()
+                }
+            }
+        }
+    }
+
     private val displayManager: DisplayManager by lazy {
-        context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager).also {
+            it.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
+        }
     }
 
-    // TODO(b/198257203): Fetch latest display information for devices where display size is not
-    //  guaranteed to be fixed. (e.g. foldable devices or devices with multiple displays)
+    val defaultDisplay: Display
+        get() = getMaxSizeDisplay()
 
-    val defaultDisplay: Display by lazy {
-        getMaxSizeDisplay()
-    }
-
-    val previewSize: Size by lazy {
-        calculatePreviewSize()
-    }
+    val previewSize: Size
+        get() = calculatePreviewSize()
 
     private fun getMaxSizeDisplay(): Display {
+        lazyMaxDisplay?.let { return it }
+
         val displays = displayManager.displays
+
+        var maxDisplayWhenStateNotOff: Display? = null
+        var maxDisplaySizeWhenStateNotOff = -1
+
         var maxDisplay: Display? = null
         var maxDisplaySize = -1
 
-        // TODO(b/211945950, b/255170076): Handle STATE_OFF displays.
-
         for (display: Display in displays) {
             val displaySize = Point()
             // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
             //  is deprecated since API level 31.
             display.getRealSize(displaySize)
+
             if (displaySize.x * displaySize.y > maxDisplaySize) {
                 maxDisplaySize = displaySize.x * displaySize.y
                 maxDisplay = display
             }
+            if (display.state != Display.STATE_OFF) {
+                if (displaySize.x * displaySize.y > maxDisplaySizeWhenStateNotOff) {
+                    maxDisplaySizeWhenStateNotOff = displaySize.x * displaySize.y
+                    maxDisplayWhenStateNotOff = display
+                }
+            }
         }
-        return checkNotNull(maxDisplay) { "No displays found from ${displayManager.displays}!" }
+
+        lazyMaxDisplay = maxDisplayWhenStateNotOff ?: maxDisplay
+
+        return checkNotNull(lazyMaxDisplay) {
+            "No displays found from ${displayManager.displays}!"
+        }
     }
 
     /**
      * Calculates the device's screen resolution, or MAX_PREVIEW_SIZE, whichever is smaller.
      */
     private fun calculatePreviewSize(): Size {
+        lazyPreviewSize?.let { return it }
+
         val displaySize = Point()
         val display: Display = defaultDisplay
         // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
@@ -87,6 +131,7 @@
             displayViewSize = MAX_PREVIEW_SIZE
         }
         // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
-        return displayViewSize
+
+        return displayViewSize.also { lazyPreviewSize = displayViewSize }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 684eb88..47dc650 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -219,6 +219,8 @@
 
         override fun setZslDisabled(disabled: Boolean) = this
 
+        override fun setHighResolutionDisabled(disabled: Boolean) = this
+
         override fun build(): MeteringRepeating {
             return MeteringRepeating(cameraProperties, useCaseConfig, displayInfoManager)
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 84788a9..2249cca 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -697,10 +697,10 @@
             mockCamcorderProfileAdapter
         )
 
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val targetResolution = Size(1200, 720)
+        val targetResolution = Size(1280, 640)
         val imageCapture = ImageCapture.Builder().setTargetResolution(
             targetResolution
         ).setTargetRotation(Surface.ROTATION_90).build()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index f550d39..1d12d9b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -20,22 +20,57 @@
 import android.graphics.Point
 import android.hardware.display.DisplayManager
 import android.util.Size
+import android.view.Display
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowDisplay
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 
 @Suppress("DEPRECATION") // getRealSize
-@RunWith(RobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 class DisplayInfoManagerTest {
     private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
 
-    private fun addDisplay(width: Int, height: Int) {
-        ShadowDisplayManager.addDisplay(String.format("w%ddp-h%ddp", width, height))
+    private fun addDisplay(width: Int, height: Int, state: Int = Display.STATE_ON): Int {
+        val displayStr = String.format("w%ddp-h%ddp", width, height)
+        val displayId = ShadowDisplayManager.addDisplay(displayStr)
+
+        if (state != Display.STATE_ON) {
+            val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+                .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+            (Shadow.extract(displayManager.getDisplay(displayId)) as ShadowDisplay).setState(state)
+        }
+
+        return displayId
+    }
+
+    companion object {
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
     }
 
     @Test
@@ -68,10 +103,89 @@
         assertEquals(Point(2000, 3000), size)
     }
 
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenPreviousMaxDisplayRemoved() {
+        // Arrange
+        val id = addDisplay(2000, 3000)
+        addDisplay(480, 640)
+        removeDisplay(id)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenNewMaxDisplayAddedAfterGettingPrevious() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.defaultDisplay
+        addDisplay(2000, 3000)
+
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithSomeOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640)
+        addDisplay(240, 320)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithNoOnState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_UNKNOWN)
+        addDisplay(240, 320, Display.STATE_UNKNOWN)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInOffState_whenMultipleDisplayWithAllOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_OFF)
+        addDisplay(200, 300, Display.STATE_OFF)
+        removeDisplay(0)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
     @Test(expected = IllegalStateException::class)
     fun throwsCorrectExceptionForDefaultDisplay_whenNoDisplay() {
         // Arrange
-        ShadowDisplayManager.removeDisplay(0)
+        removeDisplay(0)
 
         // Act
         val size = Point()
@@ -95,4 +209,17 @@
         // Act & Assert
         assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
     }
+
+    @Test
+    fun previewSizeIsUpdated_whenNewDisplayAddedAfterPreviousUse() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.previewSize
+        addDisplay(2000, 3000)
+
+        // Assert
+        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index f40a4cf..cc035a9 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -16,19 +16,24 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
+import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.display.DisplayManager
 import android.os.Build
 import android.util.Size
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 import org.robolectric.shadows.StreamConfigurationMapBuilder
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
@@ -80,6 +85,12 @@
                 )
             )
         }
+
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
     }
 
     private lateinit var meteringRepeating: MeteringRepeating
@@ -103,6 +114,18 @@
         ).build()
     }
 
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
+    }
+
     @Test
     fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 6ebf92f..f9affe9 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -52,6 +52,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.InitializationException;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -651,13 +652,17 @@
     private UseCase createUseCase(int template, boolean isZslDisabled) {
         FakeUseCaseConfig.Builder configBuilder =
                 new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
-                        new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
+                                new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
                         .setZslDisabled(isZslDisabled);
         new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
+        return createUseCase(configBuilder.getUseCaseConfig(), template);
+    }
+
+    private UseCase createUseCase(@NonNull FakeUseCaseConfig config, int template) {
         CameraSelector selector =
                 new CameraSelector.Builder().requireLensFacing(
                         CameraSelector.LENS_FACING_BACK).build();
-        TestUseCase testUseCase = new TestUseCase(template, configBuilder.getUseCaseConfig(),
+        TestUseCase testUseCase = new TestUseCase(template, config,
                 selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
         testUseCase.updateSuggestedResolution(new Size(640, 480));
         mFakeUseCases.add(testUseCase);
@@ -934,6 +939,38 @@
                 .isFalse();
     }
 
+    @SdkSuppress(minSdkVersion = 23)
+    @Test
+    public void zslDisabled_whenHighResolutionIsEnabled() throws InterruptedException {
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
+                /* isZslDisabled = */false);
+
+        // Creates a test use case with high resolution enabled.
+        ResolutionSelector highResolutionSelector =
+                new ResolutionSelector.Builder().setHighResolutionEnabled(true).build();
+        FakeUseCaseConfig.Builder configBuilder =
+                new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
+                        new Camera2SessionOptionUnpacker()).setTargetName(
+                        "UseCase").setResolutionSelector(highResolutionSelector);
+        new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
+        UseCase highResolutionUseCase = createUseCase(configBuilder.getUseCaseConfig(),
+                CameraDevice.TEMPLATE_PREVIEW);
+
+        // Checks zsl is disabled after UseCase#onAttach() is called to merge/update config.
+        assertThat(highResolutionUseCase.getCurrentConfig().isZslDisabled(false)).isTrue();
+
+        if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
+            return;
+        }
+
+        mCamera2CameraImpl.attachUseCases(Arrays.asList(zsl, highResolutionUseCase));
+        mCamera2CameraImpl.onUseCaseActive(zsl);
+        mCamera2CameraImpl.onUseCaseActive(highResolutionUseCase);
+        HandlerUtil.waitForLooperToIdle(sCameraHandler);
+        assertThat(mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
+                .isTrue();
+    }
+
     private DeferrableSurface getUseCaseSurface(UseCase useCase) {
         return useCase.getSessionConfig().getSurfaces().get(0);
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
new file mode 100644
index 0000000..02e0614
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
@@ -0,0 +1,737 @@
+/*
+ * 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.camera2.internal;
+
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
+import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
+import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.SizeCoordinate;
+import androidx.camera.core.impl.SurfaceConfig;
+import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CameraOrientationUtil;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The supported output sizes collector to help collect the available resolution candidate list
+ * according to the use case config and the following settings in {@link ResolutionSelector}:
+ *
+ * 1. Preferred aspect ratio
+ * 2. Preferred resolution
+ * 3. Max resolution
+ * 4. Is high resolution enabled
+ *
+ * The problematic resolutions retrieved from {@link ExcludedSupportedSizesContainer} will also
+ * be execulded.
+ */
+@RequiresApi(21)
+final class SupportedOutputSizesCollector {
+    private static final String TAG = "SupportedOutputSizesCollector";
+    private final String mCameraId;
+    @NonNull
+    private final CameraCharacteristicsCompat mCharacteristics;
+    @NonNull
+    private final DisplayInfoManager mDisplayInfoManager;
+    private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
+    private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
+    private final Map<Integer, Size[]> mHighResolutionOutputSizesCache = new HashMap<>();
+    private final Map<Integer, Size> mMaxSizeCache = new HashMap<>();
+    private final ExcludedSupportedSizesContainer mExcludedSupportedSizesContainer;
+    private final Map<Integer, List<Size>> mExcludedSizeListCache = new HashMap<>();
+    private final boolean mIsSensorLandscapeResolution;
+    private final boolean mIsBurstCaptureSupported;
+    private final Size mActiveArraySize;
+    private final int mSensorOrientation;
+    private final int mLensFacing;
+
+    SupportedOutputSizesCollector(@NonNull String cameraId,
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics,
+            @NonNull DisplayInfoManager displayInfoManager) {
+        mCameraId = cameraId;
+        mCharacteristics = cameraCharacteristics;
+        mDisplayInfoManager = displayInfoManager;
+
+        mExcludedSupportedSizesContainer = new ExcludedSupportedSizesContainer(cameraId);
+
+        mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
+        mIsBurstCaptureSupported = isBurstCaptureSupported();
+
+        Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
+
+        mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+        mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
+    }
+
+    /**
+     * Collects and sorts the resolution candidate list by the following steps:
+     *
+     * 1. Collects the candidate list by the high resolution enable setting.
+     * 2. Filters out the candidate list according to the min size bound, max resolution or
+     * excluded resolution quirk.
+     * 3. Sorts the candidate list according to the rules of legacy resolution API or new
+     * Resolution API.
+     * 4. Forces select specific resolutions according to ResolutionCorrector workaround.
+     */
+    @NonNull
+    List<Size> getSupportedOutputSizes(@NonNull ResolutionSelector resolutionSelector,
+            int imageFormat, @Nullable Size miniBoundingSize, boolean isHighResolutionDisabled,
+            @Nullable Size[] customizedSupportSizes) {
+        // 1. Collects the candidate list by the high resolution enable setting.
+        List<Size> resolutionCandidateList = collectResolutionCandidateList(resolutionSelector,
+                imageFormat, isHighResolutionDisabled, customizedSupportSizes);
+
+        // 2. Filters out the candidate list according to the min size bound, max resolution or
+        // excluded resolution quirk.
+        resolutionCandidateList = filterOutResolutionCandidateListBySettings(
+                resolutionCandidateList, resolutionSelector, imageFormat);
+
+        // 3. Sorts the candidate list according to the rules of new Resolution API.
+        resolutionCandidateList = sortResolutionCandidateListByResolutionSelector(
+                resolutionCandidateList, resolutionSelector,
+                mDisplayInfoManager.getMaxSizeDisplay().getRotation(), miniBoundingSize);
+
+        // 4. Forces select specific resolutions according to ResolutionCorrector workaround.
+        resolutionCandidateList = mResolutionCorrector.insertOrPrioritize(
+                SurfaceConfig.getConfigType(imageFormat), resolutionCandidateList);
+
+        return resolutionCandidateList;
+    }
+
+    /**
+     * Collects the resolution candidate list.
+     *
+     * 1. Customized supported resolutions list will be returned when it exists
+     * 2. Otherwise, the sizes retrieved from {@link StreamConfigurationMap#getOutputSizes(int)}
+     * will be the base of the resolution candidate list.
+     * 3. High resolution sizes retrieved from
+     * {@link StreamConfigurationMap#getHighResolutionOutputSizes(int)} will be included when
+     * {@link ResolutionSelector#isHighResolutionEnabled()} returns true.
+     *
+     * The returned list will be sorted in descending order and duplicate items will be removed.
+     */
+    @NonNull
+    private List<Size> collectResolutionCandidateList(
+            @NonNull ResolutionSelector resolutionSelector, int imageFormat,
+            boolean isHighResolutionDisabled, @Nullable Size[] customizedSupportedSizes) {
+        Size[] outputSizes = customizedSupportedSizes;
+
+        if (outputSizes == null) {
+            boolean highResolutionEnabled =
+                    !isHighResolutionDisabled && resolutionSelector.isHighResolutionEnabled();
+            outputSizes = getAllOutputSizesByFormat(imageFormat, highResolutionEnabled);
+        }
+
+        // Sort the output sizes. The Comparator result must be reversed to have a descending order
+        // result.
+        Arrays.sort(outputSizes, new CompareSizesByArea(true));
+
+        // Removes the duplicate items
+        List<Size> resultList = new ArrayList<>();
+        for (Size size: outputSizes) {
+            if (!resultList.contains(size)) {
+                resultList.add(size);
+            }
+        }
+
+        if (resultList.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Resolution candidate list is empty when collecting by the settings!");
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Filters out the resolution candidate list by the max resolution setting.
+     *
+     * The input size list should have been sorted in descending order.
+     */
+    private List<Size> filterOutResolutionCandidateListBySettings(
+            @NonNull List<Size> resolutionCandidateList,
+            @NonNull ResolutionSelector resolutionSelector, int imageFormat) {
+        // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
+        // selection logic should depend on ResolutionSelector's settings.
+        Size maxResolution = resolutionSelector.getMaxResolution();
+
+        // Filter out the resolution candidate list by the max resolution. Sizes that any edge
+        // exceeds the max resolution will be filtered out.
+        List<Size> resultList;
+
+        if (maxResolution == null) {
+            resultList = new ArrayList<>(resolutionCandidateList);
+        } else {
+            resultList = new ArrayList<>();
+            for (Size outputSize : resolutionCandidateList) {
+                if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
+                    resultList.add(outputSize);
+                }
+            }
+        }
+
+        resultList = excludeProblematicSizes(resultList, imageFormat);
+
+        if (resultList.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Resolution candidate list is empty after filtering out by the settings!");
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
+     *
+     * The list will be sorted by the following order:
+     * 1. size of preferred resolution
+     * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
+     * preferred resolution.
+     * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
+     * size in descending order of resolution area size.
+     * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
+     * area size.
+     */
+    @NonNull
+    private List<Size> sortResolutionCandidateListByResolutionSelector(
+            @NonNull List<Size> resolutionCandidateList,
+            @NonNull ResolutionSelector resolutionSelector,
+            @ImageOutputConfig.RotationValue int targetRotation,
+            @Nullable Size miniBoundingSize) {
+        Rational aspectRatio = getTargetAspectRatioByResolutionSelector(resolutionSelector);
+        Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
+                + " value.");
+
+        Size targetSize = getTargetSizeByResolutionSelector(resolutionSelector, targetRotation,
+                mSensorOrientation, mLensFacing);
+        List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
+                resolutionCandidateList, aspectRatio, miniBoundingSize);
+
+        // Moves the target size to the first position if it exists in the resolution candidate
+        // list and there is no quirk that needs to select specific aspect ratio sizes in priority.
+        if (resultList.contains(targetSize) && canResolutionBeMovedToHead(targetSize)) {
+            resultList.remove(targetSize);
+            resultList.add(0, targetSize);
+        }
+
+        return resultList;
+    }
+
+    @NonNull
+    private Size[] getAllOutputSizesByFormat(int imageFormat, boolean highResolutionEnabled) {
+        Size[] outputs = mOutputSizesCache.get(imageFormat);
+        if (outputs == null) {
+            outputs = doGetOutputSizesByFormat(imageFormat);
+            mOutputSizesCache.put(imageFormat, outputs);
+        }
+
+        Size[] highResolutionOutputs = null;
+
+        // A device that does not support the BURST_CAPTURE capability,
+        // StreamConfigurationMap#getHighResolutionOutputSizes() will return null.
+        if (highResolutionEnabled && mIsBurstCaptureSupported) {
+            highResolutionOutputs = mHighResolutionOutputSizesCache.get(imageFormat);
+
+            // High resolution output sizes list may be empty. If it is empty and cached in the
+            // map, don't need to query it again.
+            if (highResolutionOutputs == null && !mHighResolutionOutputSizesCache.containsKey(
+                    imageFormat)) {
+                highResolutionOutputs = doGetHighResolutionOutputSizesByFormat(imageFormat);
+                mHighResolutionOutputSizesCache.put(imageFormat, highResolutionOutputs);
+            }
+        }
+
+        // Combines output sizes if high resolution sizes list is not empty.
+        if (highResolutionOutputs != null) {
+            Size[] allOutputs = Arrays.copyOf(highResolutionOutputs,
+                    highResolutionOutputs.length + outputs.length);
+            System.arraycopy(outputs, 0, allOutputs, highResolutionOutputs.length, outputs.length);
+            outputs = allOutputs;
+        }
+
+        return outputs;
+    }
+
+    @NonNull
+    private Size[] doGetOutputSizesByFormat(int imageFormat) {
+        Size[] outputSizes;
+
+        StreamConfigurationMap map =
+                mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+        if (map == null) {
+            throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
+        }
+
+        if (Build.VERSION.SDK_INT < 23
+                && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+            // This is a little tricky that 0x22 that is internal defined in
+            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+            // retrieve the output sizes information.
+            outputSizes = map.getOutputSizes(SurfaceTexture.class);
+        } else {
+            outputSizes = map.getOutputSizes(imageFormat);
+        }
+
+        if (outputSizes == null) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the format: " + imageFormat);
+        }
+
+        return outputSizes;
+    }
+
+    @Nullable
+    private Size[] doGetHighResolutionOutputSizesByFormat(int imageFormat) {
+        if (Build.VERSION.SDK_INT < 23) {
+            return null;
+        }
+
+        Size[] outputSizes;
+
+        StreamConfigurationMap map =
+                mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+        if (map == null) {
+            throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
+        }
+
+        outputSizes = Api23Impl.getHighResolutionOutputSizes(map, imageFormat);
+
+        return outputSizes;
+    }
+
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by {@link TargetAspectRatio} if it is
+     * {@link TargetAspectRatio#RATIO_4_3}, {@link TargetAspectRatio#RATIO_16_9} or
+     * {@link TargetAspectRatio#RATIO_MAX_JPEG}.
+     * 2. The use case's original aspect ratio if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has target aspect ratio setting.
+     *
+     * @param resolutionSelector the resolution selector of the use case.
+     */
+    @Nullable
+    private Rational getTargetAspectRatioByResolutionSelector(
+            @NonNull ResolutionSelector resolutionSelector) {
+        Rational outputRatio = getTargetAspectRatioFromQuirk();
+
+        if (outputRatio == null) {
+            @AspectRatio.Ratio int aspectRatio = resolutionSelector.getPreferredAspectRatio();
+            switch (aspectRatio) {
+                case AspectRatio.RATIO_4_3:
+                    outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3
+                            : ASPECT_RATIO_3_4;
+                    break;
+                case AspectRatio.RATIO_16_9:
+                    outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9
+                            : ASPECT_RATIO_9_16;
+                    break;
+                default:
+                    Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
+            }
+        }
+        return outputRatio;
+    }
+
+    /**
+     * Returns the restricted target aspect ratio value from quirk. The returned value can be
+     * null which means that no quirk to restrict the use case to use a specific target aspect
+     * ratio value.
+     */
+    @Nullable
+    private Rational getTargetAspectRatioFromQuirk() {
+        Rational outputRatio = null;
+
+        // Gets the corrected aspect ratio due to device constraints or null if no correction is
+        // needed.
+        @TargetAspectRatio.Ratio int targetAspectRatio =
+                new TargetAspectRatio().get(mCameraId, mCharacteristics);
+        switch (targetAspectRatio) {
+            case TargetAspectRatio.RATIO_4_3:
+                outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3 : ASPECT_RATIO_3_4;
+                break;
+            case TargetAspectRatio.RATIO_16_9:
+                outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9 : ASPECT_RATIO_9_16;
+                break;
+            case TargetAspectRatio.RATIO_MAX_JPEG:
+                Size maxJpegSize = fetchMaxNormalOutputSize(ImageFormat.JPEG);
+                outputRatio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
+                break;
+            case TargetAspectRatio.RATIO_ORIGINAL:
+                break;
+        }
+
+        return outputRatio;
+    }
+
+    @Nullable
+    static Size getTargetSizeByResolutionSelector(@NonNull ResolutionSelector resolutionSelector,
+            int targetRotation, int sensorOrientation, int lensFacing) {
+        Size targetSize = resolutionSelector.getPreferredResolution();
+
+        // Calibrate targetSize by the target rotation value if it is set by the Android View
+        // coordinate orientation.
+        if (resolutionSelector.getSizeCoordinate() == SizeCoordinate.ANDROID_VIEW) {
+            targetSize = flipSizeByRotation(targetSize, targetRotation, lensFacing,
+                    sensorOrientation);
+        }
+        return targetSize;
+    }
+
+    private static boolean isRotationNeeded(int targetRotation, int lensFacing,
+            int sensorOrientation) {
+        int relativeRotationDegrees =
+                CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
+
+        // Currently this assumes that a back-facing camera is always opposite to the screen.
+        // This may not be the case for all devices, so in the future we may need to handle that
+        // scenario.
+        boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
+
+        int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
+                relativeRotationDegrees,
+                sensorOrientation,
+                isOppositeFacingScreen);
+        return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
+    }
+
+    @NonNull
+    private List<Size> excludeProblematicSizes(@NonNull List<Size> resolutionCandidateList,
+            int imageFormat) {
+        List<Size> excludedSizes = fetchExcludedSizes(imageFormat);
+        resolutionCandidateList.removeAll(excludedSizes);
+        return resolutionCandidateList;
+    }
+
+    @NonNull
+    private List<Size> fetchExcludedSizes(int imageFormat) {
+        List<Size> excludedSizes = mExcludedSizeListCache.get(imageFormat);
+
+        if (excludedSizes == null) {
+            excludedSizes = mExcludedSupportedSizesContainer.get(imageFormat);
+            mExcludedSizeListCache.put(imageFormat, excludedSizes);
+        }
+
+        return excludedSizes;
+    }
+
+    /**
+     * Sorts the resolution candidate list according to the target aspect ratio and size settings.
+     *
+     * 1. The resolution candidate list will be grouped by aspect ratio.
+     * 2. Each group only keeps one size which is not smaller than the target size.
+     * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
+     * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
+     * 4. Concatenate all sizes as the result list
+     */
+    @NonNull
+    private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
+            @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
+            @Nullable Size miniBoundingSize) {
+        // Rearrange the supported size to put the ones with the same aspect ratio in the front
+        // of the list and put others in the end from large to small. Some low end devices may
+        // not able to get an supported resolution that match the preferred aspect ratio.
+
+        // Group output sizes by aspect ratio.
+        Map<Rational, List<Size>> aspectRatioSizeListMap =
+                groupSizesByAspectRatio(resolutionCandidateList);
+
+        // If the target resolution is set, use it to remove unnecessary larger sizes.
+        if (miniBoundingSize != null) {
+            // Remove unnecessary larger sizes from each aspect ratio size list
+            for (Rational key : aspectRatioSizeListMap.keySet()) {
+                removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
+                        miniBoundingSize);
+            }
+        }
+
+        // Sort the aspect ratio key set by the target aspect ratio.
+        List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+        Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+                mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
+        Collections.sort(aspectRatios,
+                new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                        aspectRatio, fullFovRatio));
+
+        List<Size> resultList = new ArrayList<>();
+
+        // Put available sizes into final result list by aspect ratio distance to target ratio.
+        for (Rational rational : aspectRatios) {
+            for (Size size : aspectRatioSizeListMap.get(rational)) {
+                // A size may exist in multiple groups in mod16 condition. Keep only one in
+                // the final list.
+                if (!resultList.contains(size)) {
+                    resultList.add(size);
+                }
+            }
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Returns {@code true} if the input resolution can be moved to the head of resolution
+     * candidate list.
+     *
+     * The resolution possibly can't be moved to head due to some quirks that sizes of
+     * specific aspect ratio must be used to avoid problems.
+     */
+    private boolean canResolutionBeMovedToHead(@NonNull Size resolution) {
+        @TargetAspectRatio.Ratio int targetAspectRatio =
+                new TargetAspectRatio().get(mCameraId, mCharacteristics);
+
+        switch (targetAspectRatio) {
+            case TargetAspectRatio.RATIO_4_3:
+                return hasMatchingAspectRatio(resolution, ASPECT_RATIO_4_3);
+            case TargetAspectRatio.RATIO_16_9:
+                return hasMatchingAspectRatio(resolution, ASPECT_RATIO_16_9);
+            case TargetAspectRatio.RATIO_MAX_JPEG:
+                Size maxJpegSize = fetchMaxNormalOutputSize(ImageFormat.JPEG);
+                Rational maxJpegRatio = new Rational(maxJpegSize.getWidth(),
+                        maxJpegSize.getHeight());
+                return hasMatchingAspectRatio(resolution, maxJpegRatio);
+        }
+
+        return true;
+    }
+
+    private Size fetchMaxNormalOutputSize(int imageFormat) {
+        Size size = mMaxSizeCache.get(imageFormat);
+        if (size != null) {
+            return size;
+        }
+        Size maxSize = getMaxNormalOutputSizeByFormat(imageFormat);
+        mMaxSizeCache.put(imageFormat, maxSize);
+        return maxSize;
+    }
+
+    /**
+     * Gets max normal supported output size for specific image format.
+     *
+     * <p>Normal supported output sizes mean the sizes retrieved by the
+     * {@link StreamConfigurationMap#getOutputSizes(int)}. The high resolution sizes retrieved by
+     * the {@link StreamConfigurationMap#getHighResolutionOutputSizes(int)} are not included.
+     *
+     * @param imageFormat the image format info
+     * @return the max normal supported output size for the image format
+     */
+    private Size getMaxNormalOutputSizeByFormat(int imageFormat) {
+        Size[] outputSizes = getAllOutputSizesByFormat(imageFormat, false);
+
+        return SizeUtil.getMaxSize(Arrays.asList(outputSizes));
+    }
+
+    private boolean isBurstCaptureSupported() {
+        int[] availableCapabilities =
+                mCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+
+        if (availableCapabilities != null) {
+            for (int capability : availableCapabilities) {
+                if (capability
+                        == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////
+    // The following functions can be reused by the legacy resolution selection logic. The can be
+    // changed as private function after the legacy resolution API is completely removed.
+    //////////////////////////////////////////////////////////////////////////////////////////
+
+    // Use target rotation to calibrate the size.
+    @Nullable
+    static Size flipSizeByRotation(@Nullable Size size, int targetRotation, int lensFacing,
+            int sensorOrientation) {
+        Size outputSize = size;
+        // Calibrates the size with the display and sensor rotation degrees values.
+        if (size != null && isRotationNeeded(targetRotation, lensFacing, sensorOrientation)) {
+            outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
+        }
+        return outputSize;
+    }
+
+    static Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
+        Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
+
+        List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
+
+        for (Rational aspectRatio: aspectRatioKeys) {
+            aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
+        }
+
+        for (Size outputSize : sizes) {
+            for (Rational key : aspectRatioSizeListMap.keySet()) {
+                // Put the size into all groups that is matched in mod16 condition since a size
+                // may match multiple aspect ratio in mod16 algorithm.
+                if (hasMatchingAspectRatio(outputSize, key)) {
+                    aspectRatioSizeListMap.get(key).add(outputSize);
+                }
+            }
+        }
+
+        return aspectRatioSizeListMap;
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    @NonNull
+    static List<Rational> getResolutionListGroupingAspectRatioKeys(
+            @NonNull List<Size> resolutionCandidateList) {
+        List<Rational> aspectRatios = new ArrayList<>();
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(ASPECT_RATIO_4_3);
+        aspectRatios.add(ASPECT_RATIO_16_9);
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        for (Size size : resolutionCandidateList) {
+            Rational newRatio = new Rational(size.getWidth(), size.getHeight());
+            boolean aspectRatioFound = aspectRatios.contains(newRatio);
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                boolean hasMatchingAspectRatio = false;
+                for (Rational aspectRatio : aspectRatios) {
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true;
+                        break;
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio);
+                }
+            }
+        }
+
+        return aspectRatios;
+    }
+
+    /**
+     * Removes unnecessary sizes by target size.
+     *
+     * <p>If the target resolution is set, a size that is equal to or closest to the target
+     * resolution will be selected. If the list includes more than one size equal to or larger
+     * than the target resolution, only one closest size needs to be kept. The other larger sizes
+     * can be removed so that they won't be selected to use.
+     *
+     * @param supportedSizesList The list should have been sorted in descending order.
+     * @param miniBoundingSize The target size used to remove unnecessary sizes.
+     */
+    static void removeSupportedSizesByMiniBoundingSize(List<Size> supportedSizesList,
+            Size miniBoundingSize) {
+        if (supportedSizesList == null || supportedSizesList.isEmpty()) {
+            return;
+        }
+
+        int indexBigEnough = -1;
+        List<Size> removeSizes = new ArrayList<>();
+
+        // Get the index of the item that is equal to or closest to the target size.
+        for (int i = 0; i < supportedSizesList.size(); i++) {
+            Size outputSize = supportedSizesList.get(i);
+            if (outputSize.getWidth() >= miniBoundingSize.getWidth()
+                    && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
+                // New big enough item closer to the target size is found. Adding the previous
+                // one into the sizes list that will be removed.
+                if (indexBigEnough >= 0) {
+                    removeSizes.add(supportedSizesList.get(indexBigEnough));
+                }
+
+                indexBigEnough = i;
+            } else {
+                break;
+            }
+        }
+
+        // Remove the unnecessary items that are larger than the item closest to the target size.
+        supportedSizesList.removeAll(removeSizes);
+    }
+
+    static boolean isSensorLandscapeResolution(
+            @NonNull CameraCharacteristicsCompat characteristicsCompat) {
+        Size pixelArraySize =
+                characteristicsCompat.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
+
+        // Make the default value is true since usually the sensor resolution is landscape.
+        return pixelArraySize == null || pixelArraySize.getWidth() >= pixelArraySize.getHeight();
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////
+    // The above functions can be reused by the legacy resolution selection logic. The can be
+    // changed as private function after the legacy resolution API is completely removed.
+    //////////////////////////////////////////////////////////////////////////////////////////
+
+    @RequiresApi(23)
+    private static class Api23Impl {
+        private Api23Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static Size[] getHighResolutionOutputSizes(StreamConfigurationMap streamConfigurationMap,
+                int format) {
+            return streamConfigurationMap.getHighResolutionOutputSizes(format);
+        }
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 721b2ddf..25bf0f8 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -16,10 +16,17 @@
 
 package androidx.camera.camera2.internal;
 
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.flipSizeByRotation;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getResolutionListGroupingAspectRatioKeys;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getTargetSizeByResolutionSelector;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.groupSizesByAspectRatio;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.isSensorLandscapeResolution;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.removeSupportedSizesByMiniBoundingSize;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
@@ -28,6 +35,7 @@
 
 import android.content.Context;
 import android.graphics.ImageFormat;
+import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
@@ -53,6 +61,7 @@
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
 import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.ImageOutputConfig;
@@ -61,7 +70,6 @@
 import androidx.camera.core.impl.SurfaceSizeDefinition;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.utils.AspectRatioUtil;
-import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
 import androidx.core.util.Preconditions;
 
@@ -103,6 +111,10 @@
     @NonNull
     private final DisplayInfoManager mDisplayInfoManager;
     private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
+    private final Size mActiveArraySize;
+    private final int mSensorOrientation;
+    private final int mLensFacing;
+    private final SupportedOutputSizesCollector mSupportedOutputSizesCollector;
 
     SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
             @NonNull CameraManagerCompat cameraManagerCompat,
@@ -121,7 +133,7 @@
                     CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
             mHardwareLevel = keyValue != null ? keyValue
                     : CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
-            mIsSensorLandscapeResolution = isSensorLandscapeResolution();
+            mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
@@ -140,9 +152,18 @@
             }
         }
 
+        Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
+
+        mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+        mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
+
         generateSupportedCombinationList();
         generateSurfaceSizeDefinition();
         checkCustomization();
+
+        mSupportedOutputSizesCollector = new SupportedOutputSizesCollector(mCameraId,
+                mCharacteristics, mDisplayInfoManager);
     }
 
     String getCameraId() {
@@ -289,7 +310,26 @@
         return suggestedResolutionsMap;
     }
 
-    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig) {
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by {@link TargetAspectRatio} if it is
+     * {@link TargetAspectRatio#RATIO_4_3}, {@link TargetAspectRatio#RATIO_16_9} or
+     * {@link TargetAspectRatio#RATIO_MAX_JPEG}.
+     * 2. The use case's original aspect ratio if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has no target aspect ratio but has
+     * target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig,
+            @NonNull List<Size> resolutionCandidateList) {
         Rational outputRatio = null;
         // Gets the corrected aspect ratio due to device constraints or null if no correction is
         // needed.
@@ -307,7 +347,6 @@
                 outputRatio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
                 break;
             case TargetAspectRatio.RATIO_ORIGINAL:
-                Size targetSize = getTargetSize(imageOutputConfig);
                 if (imageOutputConfig.hasTargetAspectRatio()) {
                     @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
                     switch (aspectRatio) {
@@ -322,11 +361,15 @@
                         default:
                             Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
                     }
-                } else if (targetSize != null) {
-                    // Target size is calculated from the target resolution. If target size is not
-                    // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-                    // will be selected in priority.
-                    outputRatio = new Rational(targetSize.getWidth(), targetSize.getHeight());
+                } else {
+                    // The legacy resolution API will use the aspect ratio of the target size to
+                    // be the fallback target aspect ratio value when the use case has no target
+                    // aspect ratio setting.
+                    Size targetSize = getTargetSize(imageOutputConfig);
+                    if (targetSize != null) {
+                        outputRatio = getAspectRatioGroupKeyOfTargetSize(targetSize,
+                                resolutionCandidateList);
+                    }
                 }
                 break;
             default:
@@ -384,10 +427,30 @@
     List<Size> getSupportedOutputSizes(@NonNull UseCaseConfig<?> config) {
         int imageFormat = config.getInputFormat();
         ImageOutputConfig imageOutputConfig = (ImageOutputConfig) config;
+        ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
+
+        // Directly returns the output sizes retrieved from SupportedOutputSizesCollector when
+        // ResolutionSelector is used.
+        if (resolutionSelector != null) {
+            Size miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
+
+            if (resolutionSelector.getPreferredResolution() != null) {
+                miniBoundingSize = getTargetSizeByResolutionSelector(resolutionSelector,
+                        mDisplayInfoManager.getMaxSizeDisplay().getRotation(), mSensorOrientation,
+                        mLensFacing);
+            }
+
+            return mSupportedOutputSizesCollector.getSupportedOutputSizes(resolutionSelector,
+                    imageFormat, miniBoundingSize, config.isHigResolutionDisabled(false),
+                    getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig));
+        }
+
         Size[] outputSizes = getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig);
         if (outputSizes == null) {
             outputSizes = getAllOutputSizesByFormat(imageFormat);
         }
+        outputSizes = excludeProblematicSizesAndSort(outputSizes, imageFormat);
+
         List<Size> outputSizeCandidates = new ArrayList<>();
         Size maxSize = imageOutputConfig.getMaxResolution(null);
         Size maxOutputSizeByFormat = getMaxOutputSizeByFormat(imageFormat);
@@ -430,7 +493,7 @@
                             + imageFormat);
         }
 
-        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig);
+        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates);
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;
@@ -445,7 +508,7 @@
 
             // If the target resolution is set, use it to remove unnecessary larger sizes.
             if (targetSize != null) {
-                removeSupportedSizesByTargetSize(supportedResolutions, targetSize);
+                removeSupportedSizesByMiniBoundingSize(supportedResolutions, targetSize);
             }
         } else {
             // Rearrange the supported size to put the ones with the same aspect ratio in the front
@@ -459,14 +522,18 @@
             if (targetSize != null) {
                 // Remove unnecessary larger sizes from each aspect ratio size list
                 for (Rational key : aspectRatioSizeListMap.keySet()) {
-                    removeSupportedSizesByTargetSize(aspectRatioSizeListMap.get(key), targetSize);
+                    removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
+                            targetSize);
                 }
             }
 
             // Sort the aspect ratio key set by the target aspect ratio.
             List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+            Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+                    mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
             Collections.sort(aspectRatios,
-                    new AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio(aspectRatio));
+                    new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                            aspectRatio, fullFovRatio));
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
             for (Rational rational : aspectRatios) {
@@ -492,129 +559,36 @@
         int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
         // Calibrate targetSize by the target rotation value.
         Size targetSize = imageOutputConfig.getTargetResolution(null);
-        targetSize = flipSizeByRotation(targetSize, targetRotation);
+        targetSize = flipSizeByRotation(targetSize, targetRotation, mLensFacing,
+                mSensorOrientation);
         return targetSize;
     }
 
-    // Use target rotation to calibrate the size.
-    @Nullable
-    private Size flipSizeByRotation(@Nullable Size size, int targetRotation) {
-        Size outputSize = size;
-        // Calibrates the size with the display and sensor rotation degrees values.
-        if (size != null && isRotationNeeded(targetRotation)) {
-            outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
-        }
-        return outputSize;
-    }
-
-    private boolean isRotationNeeded(int targetRotation) {
-        Integer sensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
-        Preconditions.checkNotNull(sensorOrientation, "Camera HAL in bad state, unable to "
-                + "retrieve the SENSOR_ORIENTATION");
-        int relativeRotationDegrees =
-                CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
-
-        // Currently this assumes that a back-facing camera is always opposite to the screen.
-        // This may not be the case for all devices, so in the future we may need to handle that
-        // scenario.
-        Integer lensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
-        Preconditions.checkNotNull(lensFacing, "Camera HAL in bad state, unable to retrieve the "
-                + "LENS_FACING");
-
-        boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
-
-        int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
-                relativeRotationDegrees,
-                sensorOrientation,
-                isOppositeFacingScreen);
-        return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
-    }
-
-    private boolean isSensorLandscapeResolution() {
-        Size pixelArraySize =
-                mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
-
-        // Make the default value is true since usually the sensor resolution is landscape.
-        return pixelArraySize != null ? pixelArraySize.getWidth() >= pixelArraySize.getHeight()
-                : true;
-    }
-
-    private Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
-        Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
-
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap.put(ASPECT_RATIO_4_3, new ArrayList<>());
-        aspectRatioSizeListMap.put(ASPECT_RATIO_16_9, new ArrayList<>());
-
-        for (Size outputSize : sizes) {
-            Rational matchedKey = null;
-
-            for (Rational key : aspectRatioSizeListMap.keySet()) {
-                // Put the size into all groups that is matched in mod16 condition since a size
-                // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key;
-
-                    List<Size> sizeList = aspectRatioSizeListMap.get(matchedKey);
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize);
-                    }
-                }
-            }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap.put(
-                        new Rational(outputSize.getWidth(), outputSize.getHeight()),
-                        new ArrayList<>(Collections.singleton(outputSize)));
-            }
-        }
-
-        return aspectRatioSizeListMap;
-    }
-
     /**
-     * Removes unnecessary sizes by target size.
+     * Returns the aspect ratio group key of the target size when grouping the input resolution
+     * candidate list.
      *
-     * <p>If the target resolution is set, a size that is equal to or closest to the target
-     * resolution will be selected. If the list includes more than one size equal to or larger
-     * than the target resolution, only one closest size needs to be kept. The other larger sizes
-     * can be removed so that they won't be selected to use.
-     *
-     * @param supportedSizesList The list should have been sorted in descending order.
-     * @param targetSize         The target size used to remove unnecessary sizes.
+     * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
+     * also need to consider the mod 16 factor to find which aspect ratio of group the target size
+     * might be put in. So that sizes of the group will be selected to use in the highest priority.
      */
-    private void removeSupportedSizesByTargetSize(List<Size> supportedSizesList,
-            Size targetSize) {
-        if (supportedSizesList == null || supportedSizesList.isEmpty()) {
-            return;
+    @Nullable
+    private Rational getAspectRatioGroupKeyOfTargetSize(@Nullable Size targetSize,
+            @NonNull List<Size> resolutionCandidateList) {
+        if (targetSize == null) {
+            return null;
         }
 
-        int indexBigEnough = -1;
-        List<Size> removeSizes = new ArrayList<>();
+        List<Rational> aspectRatios = getResolutionListGroupingAspectRatioKeys(
+                resolutionCandidateList);
 
-        // Get the index of the item that is equal to or closest to the target size.
-        for (int i = 0; i < supportedSizesList.size(); i++) {
-            Size outputSize = supportedSizesList.get(i);
-            if (outputSize.getWidth() >= targetSize.getWidth()
-                    && outputSize.getHeight() >= targetSize.getHeight()) {
-                // New big enough item closer to the target size is found. Adding the previous
-                // one into the sizes list that will be removed.
-                if (indexBigEnough >= 0) {
-                    removeSizes.add(supportedSizesList.get(indexBigEnough));
-                }
-
-                indexBigEnough = i;
-            } else {
-                break;
+        for (Rational aspectRatio: aspectRatios) {
+            if (hasMatchingAspectRatio(targetSize, aspectRatio)) {
+                return aspectRatio;
             }
         }
 
-        // Remove the unnecessary items that are larger than the item closest to the target size.
-        supportedSizesList.removeAll(removeSizes);
+        return new Rational(targetSize.getWidth(), targetSize.getHeight());
     }
 
     private List<List<Size>> getAllPossibleSizeArrangements(
@@ -671,11 +645,18 @@
     }
 
     @NonNull
-    private Size[] excludeProblematicSizes(@NonNull Size[] outputSizes, int imageFormat) {
+    private Size[] excludeProblematicSizesAndSort(@NonNull Size[] outputSizes, int imageFormat) {
         List<Size> excludedSizes = fetchExcludedSizes(imageFormat);
         List<Size> resultSizesList = new ArrayList<>(Arrays.asList(outputSizes));
         resultSizesList.removeAll(excludedSizes);
-        return resultSizesList.toArray(new Size[0]);
+
+        Size[] resultSizes = resultSizesList.toArray(new Size[0]);
+
+        // Sort the result sizes. The Comparator result must be reversed to have a descending
+        // order result.
+        Arrays.sort(resultSizes, new CompareSizesByArea(true));
+
+        return resultSizes;
     }
 
     @Nullable
@@ -696,14 +677,6 @@
             }
         }
 
-        if (outputSizes != null) {
-            outputSizes = excludeProblematicSizes(outputSizes, imageFormat);
-
-            // Sort the output sizes. The Comparator result must be reversed to have a descending
-            // order result.
-            Arrays.sort(outputSizes, new CompareSizesByArea(true));
-        }
-
         return outputSizes;
     }
 
@@ -746,12 +719,6 @@
                     "Can not get supported output size for the format: " + imageFormat);
         }
 
-        outputSizes = excludeProblematicSizes(outputSizes, imageFormat);
-
-        // Sort the output sizes. The Comparator result must be reversed to have a descending order
-        // result.
-        Arrays.sort(outputSizes, new CompareSizesByArea(true));
-
         return outputSizes;
     }
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
new file mode 100644
index 0000000..54e5f41
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
@@ -0,0 +1,1364 @@
+/*
+ * 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.camera2.internal
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Pair
+import android.util.Size
+import android.view.Surface
+import android.view.WindowManager
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.CameraManagerCompat
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.ResolutionSelector
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.SizeCoordinate
+import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraXUtil
+import androidx.camera.testing.Configs
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
+
+private const val FAKE_USE_CASE = 0
+private const val PREVIEW_USE_CASE = 1
+private const val IMAGE_CAPTURE_USE_CASE = 2
+private const val IMAGE_ANALYSIS_USE_CASE = 3
+private const val UNKNOWN_ASPECT_RATIO = -1
+private const val DEFAULT_CAMERA_ID = "0"
+private const val SENSOR_ORIENTATION_0 = 0
+private const val SENSOR_ORIENTATION_90 = 90
+private val LANDSCAPE_PIXEL_ARRAY_SIZE = Size(4032, 3024)
+private val PORTRAIT_PIXEL_ARRAY_SIZE = Size(3024, 4032)
+private val DISPLAY_SIZE = Size(720, 1280)
+private val DEFAULT_SUPPORTED_SIZES = arrayOf(
+    Size(4032, 3024), // 4:3
+    Size(3840, 2160), // 16:9
+    Size(1920, 1440), // 4:3
+    Size(1920, 1080), // 16:9
+    Size(1280, 960), // 4:3
+    Size(1280, 720), // 16:9
+    Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+    Size(800, 450), // 16:9
+    Size(640, 480), // 4:3
+    Size(320, 240), // 4:3
+    Size(320, 180), // 16:9
+    Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
+)
+
+/** Robolectric test for [SupportedOutputSizesCollector] class */
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SupportedOutputSizesCollectorTest(
+    private val sizeCoordinate: SizeCoordinate
+) {
+    private val mockCamcorderProfileHelper = Mockito.mock(CamcorderProfileHelper::class.java)
+    private lateinit var cameraManagerCompat: CameraManagerCompat
+    private lateinit var cameraCharacteristicsCompat: CameraCharacteristicsCompat
+    private lateinit var displayInfoManager: DisplayInfoManager
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private var cameraFactory: FakeCameraFactory? = null
+    private var useCaseConfigFactory: UseCaseConfigFactory? = null
+
+    @Suppress("DEPRECATION") // defaultDisplay
+    @Before
+    fun setUp() {
+        DisplayInfoManager.releaseInstance()
+        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(DISPLAY_SIZE.width)
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(DISPLAY_SIZE.height)
+        Mockito.`when`(
+            mockCamcorderProfileHelper.hasProfile(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.anyInt()
+            )
+        ).thenReturn(true)
+
+        displayInfoManager = DisplayInfoManager.getInstance(context)
+    }
+
+    @After
+    fun tearDown() {
+        CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio4x3() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_4_3
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_inLegacyDevice() {
+        setupCameraAndInitCameraX()
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList: List<Size> = if (Build.VERSION.SDK_INT == 21) {
+            listOf(
+                // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched maximum JPEG resolution AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        } else {
+            listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                // Mismatched preferred AspectRatio items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240)
+            )
+        }
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1920x1080_InLimitedDevice() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
+        // after the preferred resolution.
+        val expectedList =
+            listOf(
+                // Matched preferred resolution size will be put in first priority.
+                Size(1920, 1080),
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1920x1080_InLegacyDevice() {
+        setupCameraAndInitCameraX()
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
+        // after the preferred resolution.
+        val expectedList = if (Build.VERSION.SDK_INT == 21) {
+                listOf(
+                    // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
+                    Size(1920, 1440),
+                    Size(1280, 960),
+                    Size(640, 480),
+                    Size(320, 240),
+                    // Mismatched maximum JPEG resolution AspectRatio items, sorted by area size.
+                    Size(1920, 1080),
+                    Size(1280, 720),
+                    Size(960, 544),
+                    Size(800, 450),
+                    Size(320, 180),
+                    Size(256, 144)
+                )
+            } else {
+                // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest
+                // priority just after the preferred resolution size.
+                listOf(
+                    // Matched default preferred resolution size will be put in first priority.
+                    Size(1920, 1080),
+                    // Matched preferred AspectRatio items, sorted by area size.
+                    Size(1920, 1440),
+                    Size(1280, 960),
+                    Size(640, 480),
+                    Size(320, 240),
+                    // Mismatched preferred default AspectRatio items, sorted by area size.
+                    Size(1280, 720),
+                    Size(960, 544),
+                    Size(800, 450),
+                    Size(320, 180),
+                    Size(256, 144),
+                )
+            }
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    @Suppress("DEPRECATION") /* defaultDisplay */
+    fun getSupportedOutputSizes_smallDisplay_withMaxResolution1920x1080() {
+        // Sets up small display.
+        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(240)
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(320)
+        displayInfoManager = DisplayInfoManager.getInstance(context)
+
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            PREVIEW_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            maxResolution = Size(1920, 1080)
+        )
+        // Max resolution setting will remove sizes larger than 1920x1080. The auto-resolution
+        // mechanism will try to select the sizes which aspect ratio is nearest to the aspect ratio
+        // of target resolution in priority. Therefore, sizes of aspect ratio 16/9 will be in front
+        // of the returned sizes list and the list is sorted in descending order. Other items will
+        // be put in the following that are sorted by aspect ratio delta and then area size.
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1800x1440NearTo4x3() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1800, 1440),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList =
+            listOf(
+                // No matched preferred resolution size found.
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1280x600NearTo16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1280, 600),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_maxResolution1280x720() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(1280, 720)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_maxResolution720x1280() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(720, 1280)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            defaultResolution = Size(1280, 720)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_defaultResolution1280x720_preferredResolution1920x1080() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            defaultResolution = Size(1280, 720),
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred resolution size will be put in first priority.
+            Size(1920, 1080),
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(320, 240),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(320, 180)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 180), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxSizeSmallerThanBigPreferredResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(3840, 2160),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(1920, 1080)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found after filtering by max resolution setting.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenNoSizeBetweenMaxSizeAndPreferredResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(320, 190),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(320, 200)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 180), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenPreferredResolutionSmallerThanAnySize() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(192, 144),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 240), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
+        setupCameraAndInitCameraX(
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(192, 144)
+        )
+        // All sizes will be filtered out by the max resolution 192x144 setting and an
+        // IllegalArgumentException will be thrown.
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        }
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(296, 144),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(185, 90),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(256, 144),
+            Size(296, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(1920, 1080),
+                Size(1440, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
+                Size(768, 432),
+                Size(640, 480),
+                Size(640, 360),
+                Size(480, 360),
+                Size(384, 288)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1080, 2016),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(480, 360),
+            Size(384, 288),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(864, 480),
+            Size(768, 432),
+            Size(640, 360)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
+        // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
+        // phone device which majorly supports portrait output sizes.
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0,
+            pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
+            supportedSizes = arrayOf(
+                Size(1080, 1920),
+                Size(1080, 1440),
+                Size(960, 1280),
+                Size(720, 1280),
+                Size(960, 540),
+                Size(480, 640),
+                Size(640, 480),
+                Size(360, 480)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(720, 1280),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(360, 480),
+            Size(640, 480),
+            Size(960, 540)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
+        // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
+        // tablet device which majorly supports portrait output sizes.
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_90,
+            pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
+            supportedSizes = arrayOf(
+                Size(1080, 1920),
+                Size(1080, 1440),
+                Size(960, 1280),
+                Size(720, 1280),
+                Size(960, 540),
+                Size(480, 640),
+                Size(640, 480),
+                Size(360, 480)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        // Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in front of
+        // the returned sizes list and the list is sorted in descending order. Other items will be
+        // put in the following that are sorted by aspect ratio delta and then area size.
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(720, 1280),
+            // Mismatched preferred AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(360, 480),
+            Size(640, 480),
+            Size(960, 540)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTablet_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0,
+            pixelArraySize = LANDSCAPE_PIXEL_ARRAY_SIZE
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0, supportedSizes = arrayOf(
+                Size(1920, 1080),
+                Size(1440, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(540, 960),
+                Size(640, 480),
+                Size(480, 640),
+                Size(480, 360)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(480, 360),
+            Size(480, 640),
+            Size(540, 960)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun getSupportedOutputSizes_whenHighResolutionIsEnabled_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(8000, 4500),
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(8000, 6000),
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionCanNotBeSelected_whenHighResolutionForceDisabled() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true,
+            highResolutionForceDisabled = true
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    /**
+     * Sets up camera according to the specified settings and initialize [CameraX].
+     *
+     * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
+     * @param hardwareLevel the hardware level of the camera. Default value is
+     * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
+     * @param sensorOrientation the sensor orientation of the camera. Default value is
+     * [SENSOR_ORIENTATION_90].
+     * @param pixelArraySize the active pixel array size of the camera. Default value is
+     * [LANDSCAPE_PIXEL_ARRAY_SIZE].
+     * @param supportedSizes the supported sizes of the camera. Default value is
+     * [DEFAULT_SUPPORTED_SIZES].
+     * @param capabilities the capabilities of the camera. Default value is null.
+     */
+    private fun setupCameraAndInitCameraX(
+        cameraId: String = DEFAULT_CAMERA_ID,
+        hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+        sensorOrientation: Int = SENSOR_ORIENTATION_90,
+        pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
+        supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+        supportedHighResolutionSizes: Array<Size>? = null,
+        capabilities: IntArray? = null
+    ) {
+        setupCamera(
+            cameraId,
+            hardwareLevel,
+            sensorOrientation,
+            pixelArraySize,
+            supportedSizes,
+            supportedHighResolutionSizes,
+            capabilities
+        )
+
+        @CameraSelector.LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+        cameraManagerCompat = CameraManagerCompat.from(context)
+        val cameraInfo = FakeCameraInfoInternal(
+            cameraId,
+            sensorOrientation,
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+
+        cameraFactory = FakeCameraFactory().apply {
+            insertCamera(lensFacingEnum, cameraId) {
+                FakeCamera(cameraId, null, cameraInfo)
+            }
+        }
+
+        cameraCharacteristicsCompat = cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
+
+        initCameraX()
+    }
+
+    /**
+     * Initializes the [CameraX].
+     */
+    private fun initCameraX() {
+        val surfaceManagerProvider =
+            CameraDeviceSurfaceManager.Provider { context, _, availableCameraIds ->
+                Camera2DeviceSurfaceManager(
+                    context,
+                    mockCamcorderProfileHelper,
+                    CameraManagerCompat.from(this@SupportedOutputSizesCollectorTest.context),
+                    availableCameraIds
+                )
+            }
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
+            .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
+            .setCameraFactoryProvider { _, _, _ -> cameraFactory!! }
+            .build()
+        val cameraX: CameraX = try {
+            CameraXUtil.getOrCreateInstance(context) { cameraXConfig }.get()
+        } catch (e: ExecutionException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        } catch (e: InterruptedException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        }
+        useCaseConfigFactory = cameraX.defaultConfigFactory
+    }
+
+    /**
+     * Gets the supported output sizes by the converted ResolutionSelector use case config which
+     * will also be converted when a use case is bound to the lifecycle.
+     */
+    private fun getSupportedOutputSizes(
+        supportedOutputSizesCollector: SupportedOutputSizesCollector,
+        useCase: UseCase,
+        cameraId: String = DEFAULT_CAMERA_ID,
+        sensorOrientation: Int = SENSOR_ORIENTATION_90,
+        useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
+    ): List<Size?> {
+        // Converts the use case config to new ResolutionSelector config
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            listOf(useCase),
+            useCaseConfigFactory
+        )
+
+        val useCaseConfig = useCaseToConfigMap[useCase]!!
+        val resolutionSelector = (useCaseConfig as ImageOutputConfig).resolutionSelector
+        val imageFormat = useCaseConfig.inputFormat
+        val isHighResolutionDisabled = useCaseConfig.isHigResolutionDisabled(false)
+        val customizedSupportSizes = getCustomizedSupportSizesFromConfig(imageFormat, useCaseConfig)
+        val miniBoundingSize = SupportedOutputSizesCollector.getTargetSizeByResolutionSelector(
+            resolutionSelector,
+            Surface.ROTATION_0,
+            sensorOrientation,
+            CameraCharacteristics.LENS_FACING_BACK
+        ) ?: useCaseConfig.getDefaultResolution(null)
+
+        return supportedOutputSizesCollector.getSupportedOutputSizes(
+            resolutionSelector,
+            imageFormat,
+            miniBoundingSize,
+            isHighResolutionDisabled,
+            customizedSupportSizes
+        )
+    }
+
+    companion object {
+
+        /**
+         * Sets up camera according to the specified settings.
+         *
+         * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
+         * @param hardwareLevel the hardware level of the camera. Default value is
+         * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
+         * @param sensorOrientation the sensor orientation of the camera. Default value is
+         * [SENSOR_ORIENTATION_90].
+         * @param pixelArraySize the active pixel array size of the camera. Default value is
+         * [LANDSCAPE_PIXEL_ARRAY_SIZE].
+         * @param supportedSizes the supported sizes of the camera. Default value is
+         * [DEFAULT_SUPPORTED_SIZES].
+         * @param capabilities the capabilities of the camera. Default value is null.
+         */
+        @JvmStatic
+        fun setupCamera(
+            cameraId: String = DEFAULT_CAMERA_ID,
+            hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            sensorOrientation: Int = SENSOR_ORIENTATION_90,
+            pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
+            supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+            supportedHighResolutionSizes: Array<Size>? = null,
+            capabilities: IntArray? = null
+        ) {
+            val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
+                // Sets up the supported sizes
+                Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt()))
+                    .thenReturn(supportedSizes)
+                // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
+                // output sizes need to be retrieved via SurfaceTexture.class.
+                Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java))
+                    .thenReturn(supportedSizes)
+                // This is setup for the test to determine RECORD size from StreamConfigurationMap
+                Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java))
+                    .thenReturn(supportedSizes)
+
+                // Sets up the supported high resolution sizes
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    Mockito.`when`(it.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
+                        .thenReturn(supportedHighResolutionSizes)
+                }
+            }
+
+            val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+            Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
+                set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
+                set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
+                set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
+                set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
+                set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+                capabilities?.let {
+                    set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
+                }
+            }
+
+            val cameraManager = ApplicationProvider.getApplicationContext<Context>()
+                .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+            (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
+                .addCamera(cameraId, characteristics)
+        }
+
+        /**
+         * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
+         * FakeUseCase by the legacy or new ResolutionSelector API according to the specified settings.
+         *
+         * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
+         * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
+         * @param preferredAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
+         * and no target aspect ratio will be set to the created use case.
+         * @param preferredResolution the preferred resolution setting which should be specified in the
+         * camera sensor coordinate. The resolution will be transformed to set via
+         * [ResolutionSelector.Builder.setPreferredResolutionByViewSize] if size coordinate is
+         * [SizeCoordinate.ANDROID_VIEW]. Default is null.
+         * @param maxResolution the max resolution setting. Default is null.
+         * @param highResolutionEnabled the high resolution setting, Default is false.
+         * @param highResolutionForceDisabled the high resolution force disabled setting, Default
+         * is false. This will be set in the use case config to force disable high resolution.
+         * @param defaultResolution the default resolution setting. Default is null.
+         * @param supportedResolutions the customized supported resolutions. Default is null.
+         */
+        @JvmStatic
+        fun createUseCaseByResolutionSelector(
+            useCaseType: Int,
+            preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
+            preferredResolution: Size? = null,
+            sizeCoordinate: SizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
+            maxResolution: Size? = null,
+            highResolutionEnabled: Boolean = false,
+            highResolutionForceDisabled: Boolean = false,
+            defaultResolution: Size? = null,
+            supportedResolutions: List<Pair<Int, Array<Size>>>? = null
+        ): UseCase {
+            val builder = when (useCaseType) {
+                PREVIEW_USE_CASE -> Preview.Builder()
+                IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
+                IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
+                else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+            }
+
+            val resolutionSelectorBuilder = ResolutionSelector.Builder()
+
+            if (preferredAspectRatio != UNKNOWN_ASPECT_RATIO) {
+                resolutionSelectorBuilder.setPreferredAspectRatio(preferredAspectRatio)
+            }
+
+            preferredResolution?.let {
+                if (sizeCoordinate == SizeCoordinate.CAMERA_SENSOR) {
+                    resolutionSelectorBuilder.setPreferredResolution(it)
+                } else {
+                    val flippedResolution = Size(
+                        /* width= */ it.height,
+                        /* height= */ it.width
+                    )
+                    resolutionSelectorBuilder.setPreferredResolutionByViewSize(flippedResolution)
+                }
+            }
+
+            maxResolution?.let { resolutionSelectorBuilder.setMaxResolution(it) }
+            resolutionSelectorBuilder.setHighResolutionEnabled(highResolutionEnabled)
+
+            builder.setResolutionSelector(resolutionSelectorBuilder.build())
+            builder.setHighResolutionDisabled(highResolutionForceDisabled)
+
+            defaultResolution?.let { builder.setDefaultResolution(it) }
+            supportedResolutions?.let { builder.setSupportedResolutions(it) }
+            return builder.build()
+        }
+
+        @JvmStatic
+        fun getCustomizedSupportSizesFromConfig(
+            imageFormat: Int,
+            config: ImageOutputConfig
+        ): Array<Size>? {
+            var outputSizes: Array<Size>? = null
+
+            // Try to retrieve customized supported resolutions from config.
+            val formatResolutionsPairList = config.getSupportedResolutions(null)
+            if (formatResolutionsPairList != null) {
+                for (formatResolutionPair in formatResolutionsPairList) {
+                    if (formatResolutionPair.first == imageFormat) {
+                        outputSizes = formatResolutionPair.second
+                        break
+                    }
+                }
+            }
+            return outputSizes
+        }
+
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "sizeCoordinate = {0}")
+        fun data() = listOf(
+            SizeCoordinate.CAMERA_SENSOR,
+            SizeCoordinate.ANDROID_VIEW
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 63f772c1..1d8366d5 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -17,13 +17,9 @@
 package androidx.camera.camera2.internal
 import android.content.Context
 import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
-import android.hardware.camera2.params.StreamConfigurationMap
 import android.media.CamcorderProfile
-import android.media.MediaRecorder
 import android.os.Build
 import android.util.Pair
 import android.util.Rational
@@ -32,6 +28,8 @@
 import android.view.WindowManager
 import androidx.annotation.NonNull
 import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.createUseCaseByResolutionSelector
+import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.setupCamera
 import androidx.camera.camera2.internal.compat.CameraManagerCompat
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector.LensFacing
@@ -46,6 +44,7 @@
 import androidx.camera.core.impl.CameraDeviceSurfaceManager
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.SizeCoordinate
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.SurfaceConfig.ConfigSize
@@ -53,9 +52,9 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
 import androidx.camera.testing.CamcorderProfileUtil
 import androidx.camera.testing.CameraUtil
@@ -93,15 +92,11 @@
 import org.robolectric.Shadows
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowCameraCharacteristics
-import org.robolectric.shadows.ShadowCameraManager
 
 private const val FAKE_USE_CASE = 0
 private const val PREVIEW_USE_CASE = 1
 private const val IMAGE_CAPTURE_USE_CASE = 2
 private const val IMAGE_ANALYSIS_USE_CASE = 3
-private const val VIDEO_CAPTURE_USE_CASE = 4
 private const val UNKNOWN_ROTATION = -1
 private const val UNKNOWN_ASPECT_RATIO = -1
 private const val DEFAULT_CAMERA_ID = "0"
@@ -124,7 +119,6 @@
     Size(1920, 1080), // 16:9
     Size(1280, 960), // 4:3
     Size(1280, 720), // 16:9
-    Size(1280, 720), // duplicate the size since Nexus 5X emulator has the case.
     Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
     Size(800, 450), // 16:9
     Size(640, 480), // 4:3
@@ -137,7 +131,7 @@
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class SupportedSurfaceCombinationTest() {
+class SupportedSurfaceCombinationTest {
     private val mockCamcorderProfileHelper = Mockito.mock(
         CamcorderProfileHelper::class.java
     )
@@ -146,22 +140,95 @@
     )
     private var cameraManagerCompat: CameraManagerCompat? = null
     private val profileUhd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_2160P, RECORD_SIZE.getWidth(), RECORD_SIZE.getHeight()
+        CamcorderProfile.QUALITY_2160P, RECORD_SIZE.width, RECORD_SIZE.height
     )
     private val profileFhd = CamcorderProfileUtil.createCamcorderProfileProxy(
         CamcorderProfile.QUALITY_1080P, 1920, 1080
     )
     private val profileHd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_720P, PREVIEW_SIZE.getWidth(), PREVIEW_SIZE.getHeight()
+        CamcorderProfile.QUALITY_720P, PREVIEW_SIZE.width, PREVIEW_SIZE.height
     )
     private val profileSd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_480P, RESOLUTION_VGA.getWidth(),
-        RESOLUTION_VGA.getHeight()
+        CamcorderProfile.QUALITY_480P, RESOLUTION_VGA.width,
+        RESOLUTION_VGA.height
     )
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private var cameraFactory: FakeCameraFactory? = null
     private var useCaseConfigFactory: UseCaseConfigFactory? = null
 
+    private val legacyUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByLegacyApi(
+                useCaseType,
+                targetRotation,
+                preferredAspectRatio,
+                preferredResolution,
+                maxResolution,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
+    private val resolutionSelectorUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByResolutionSelector(
+                useCaseType,
+                preferredAspectRatio,
+                preferredResolution,
+                sizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
+                maxResolution,
+                highResolutionEnabled,
+                highResolutionForceDisabled = false,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
+    private val viewSizeResolutionSelectorUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByResolutionSelector(
+                useCaseType,
+                preferredAspectRatio,
+                preferredResolution,
+                sizeCoordinate = SizeCoordinate.ANDROID_VIEW,
+                maxResolution,
+                highResolutionEnabled,
+                highResolutionForceDisabled = false,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
     @Suppress("DEPRECATION") // defaultDisplay
     @Before
     fun setUp() {
@@ -416,13 +483,25 @@
     }
 
     @Test
-    fun checkTargetAspectRatioInLegacyDevice() {
+    fun checkTargetAspectRatioInLegacyDevice_LegacyApi() {
+        checkTargetAspectRatioInLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkTargetAspectRatioInLegacyDevice_ResolutionSelector() {
+        checkTargetAspectRatioInLegacyDevice(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkTargetAspectRatioInLegacyDevice(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX()
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         val targetAspectRatio = ASPECT_RATIO_16_9
-        val useCase = createUseCase(FAKE_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
+        val useCase = useCaseCreator.createUseCase(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
@@ -443,15 +522,30 @@
     }
 
     @Test
-    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice() {
+    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_LegacyApi() {
+        checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_ResolutionSelector() {
+        checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX()
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // The test case make sure the selected result is expected after the regular flow.
         val targetAspectRatio = ASPECT_RATIO_16_9
-        val preview =
-            createUseCase(PREVIEW_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9) as Preview
+        val preview = useCaseCreator.createUseCase(
+            PREVIEW_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        ) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -460,10 +554,14 @@
                 )
             )
         )
-        val imageCapture =
-            createUseCase(IMAGE_CAPTURE_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
-        val imageAnalysis =
-            createUseCase(IMAGE_ANALYSIS_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
+        val imageCapture = useCaseCreator.createUseCase(
+            IMAGE_CAPTURE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val imageAnalysis = useCaseCreator.createUseCase(
+            IMAGE_ANALYSIS_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
         val suggestedResolutionMap = getSuggestedResolutionMap(
@@ -517,14 +615,25 @@
     }
 
     @Test
-    fun checkDefaultAspectRatioAndResolutionForMixedUseCase() {
+    fun checkDefaultAspectRatioAndResolutionForMixedUseCase_LegacyApi() {
+        checkDefaultAspectRatioAndResolutionForMixedUseCase(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkDefaultAspectRatioAndResolutionForMixedUseCase_ResolutionSelector() {
+        checkDefaultAspectRatioAndResolutionForMixedUseCase(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkDefaultAspectRatioAndResolutionForMixedUseCase(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -533,8 +642,8 @@
                 )
             )
         )
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE)
-        val imageAnalysis = createUseCase(IMAGE_ANALYSIS_USE_CASE)
+        val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
+        val imageAnalysis = useCaseCreator.createUseCase(IMAGE_ANALYSIS_USE_CASE)
 
         // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
         // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
@@ -577,8 +686,10 @@
         */
         val displayWidth = 1080
         val displayHeight = 2220
-        val preview =
-            createUseCase(PREVIEW_USE_CASE, targetResolution = Size(displayHeight, displayWidth))
+        val preview = createUseCaseByLegacyApi(
+            PREVIEW_USE_CASE,
+            targetResolution = Size(displayHeight, displayWidth)
+        )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, preview)
         // Checks the preconditions.
         val preconditionSize = Size(256, 144)
@@ -593,7 +704,21 @@
     }
 
     @Test
-    fun checkAspectRatioMatchedSizeCanBeSelected() {
+    fun checkAllSupportedSizesCanBeSelected_LegacyApi() {
+        checkAllSupportedSizesCanBeSelected(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_SensorSize() {
+        checkAllSupportedSizesCanBeSelected(resolutionSelectorUseCaseCreator)
+    }
+
+    @Test
+    fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_ViewSize() {
+        checkAllSupportedSizesCanBeSelected(viewSizeResolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkAllSupportedSizesCanBeSelected(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -605,10 +730,10 @@
         // will be selected as the result. This test can also verify that size smaller than
         // 640x480 can be selected after set as target resolution.
         DEFAULT_SUPPORTED_SIZES.forEach {
-            val imageCapture = createUseCase(
+            val imageCapture = useCaseCreator.createUseCase(
                 IMAGE_CAPTURE_USE_CASE,
                 Surface.ROTATION_90,
-                targetResolution = it
+                preferredResolution = it
             )
             val suggestedResolutionMap =
                 getSuggestedResolutionMap(supportedSurfaceCombination, imageCapture)
@@ -617,76 +742,83 @@
     }
 
     @Test
-    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected() {
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_LegacyApi() {
+        // Sets target resolution as 1280x640, all supported resolutions will be put into
+        // aspect ratio not matched list. Then, 1280x720 will be the nearest matched one.
+        // Finally, checks whether 1280x720 is selected or not.
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(legacyUseCaseCreator, Size(1280, 720))
+    }
+
+    // 1280x640 is not included in the supported sizes list. So, the smallest size of the
+    // default aspect ratio 4:3 which is 1280x960 will be finally selected.
+    @Test
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_SensorSize() {
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+            resolutionSelectorUseCaseCreator,
+            Size(1280, 960)
+        )
+    }
+
+    @Test
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_ViewSize() {
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+            viewSizeResolutionSelectorUseCaseCreator,
+            Size(1280, 960)
+        )
+    }
+
+    private fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+        useCaseCreator: UseCaseCreator,
+        expectedResult: Size
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val resolution = Size(1200, 720)
-        val useCase = createUseCase(
+        val resolution = Size(1280, 640)
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             Surface.ROTATION_90,
-            targetResolution = resolution
+            preferredResolution = resolution
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
-        assertThat(Size(1280, 720)).isEqualTo(
-            suggestedResolutionMap[useCase]
-        )
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(expectedResult)
     }
 
     @Test
-    fun legacyVideo_suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        // An IllegalArgumentException will be thrown because a LEGACY level device can't support
-        // ImageCapture + VideoCapture + Preview
-        assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(
-                supportedSurfaceCombination,
-                imageCapture,
-                videoCapture,
-                preview
-            )
-        }
-    }
-
-    @Test
-    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture()
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         // An IllegalArgumentException will be thrown because a LEGACY level device can't support
         // ImageCapture + VideoCapture + Preview
@@ -701,38 +833,20 @@
     }
 
     @Test
-    fun legacyVideo_suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
-        val videoResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(RECORD_SIZE))
-        )
-        val previewResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(PREVIEW_SIZE))
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            maxResolution = RECORD_SIZE, // Override the default max resolution in VideoCapture
-            supportedResolutions = videoResolutionsPairs
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            supportedResolutions = previewResolutionsPairs
-        )
-        // An IllegalArgumentException will be thrown because the VideoCapture requests to only
-        // support a RECORD size but the configuration can't be supported on a LEGACY level device.
-        assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(supportedSurfaceCombination, videoCapture, preview)
-        }
+    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
+    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
         )
@@ -744,7 +858,7 @@
             Pair.create(ImageFormat.PRIVATE, arrayOf(PREVIEW_SIZE))
         )
         val videoCapture: VideoCapture<TestVideoOutput> = createVideoCapture(Quality.UHD)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             supportedResolutions = previewResolutionsPairs
         )
@@ -756,53 +870,32 @@
     }
 
     @Test
-    fun legacyVideo_getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
-            supportedSurfaceCombination,
-            imageCapture,
-            videoCapture,
-            preview
-        )
-        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(LEGACY_VIDEO_MAXIMUM_SIZE)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_LegacyApi() {
+        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
+        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture(Quality.HIGHEST)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -821,24 +914,38 @@
     // VideoCapture should have higher priority to choose size than ImageCapture.
     @Test
     @Throws(CameraUnavailableException::class)
-    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage() {
+    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
+        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class)
+    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
+        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture(QualitySelector.from(
             Quality.UHD,
             FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
         ))
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -855,25 +962,38 @@
     }
 
     @Test
-    fun getSuggestedResolutionsInFullDevice_videoRecordSizeLowPriority_imageCanGetMaxSize() {
+    fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_LegacyApi() {
+        imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_ResolutionSelector() {
+        imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_4_3 // mMaximumSize(4032x3024) is 4:3
+            preferredAspectRatio = AspectRatio.RATIO_4_3 // mMaximumSize(4032x3024) is 4:3
         )
         val videoCapture = createVideoCapture(
             QualitySelector.fromOrderedList(
-                listOf<androidx.camera.video.Quality>(Quality.HD, Quality.FHD, Quality.UHD)
+                listOf<Quality>(Quality.HD, Quality.FHD, Quality.UHD)
             )
         )
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -890,9 +1010,50 @@
     }
 
     @Test
-    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases() {
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_LegacyApi() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            legacyUseCaseCreator,
+            DISPLAY_SIZE
+        )
+    }
+
+    @Test
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            resolutionSelectorUseCaseCreator,
+            PREVIEW_SIZE
+        )
+    }
+
+    @Test
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            viewSizeResolutionSelectorUseCaseCreator,
+            PREVIEW_SIZE
+        )
+    }
+
+    private fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+        useCaseCreator: UseCaseCreator,
+        preferredResolution: Size
+    ) {
         setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            supportedSizes = arrayOf(
+                Size(4032, 3024), // 4:3
+                Size(3840, 2160), // 16:9
+                Size(1920, 1440), // 4:3
+                Size(1920, 1080), // 16:9
+                Size(1280, 960), // 4:3
+                Size(1280, 720), // 16:9
+                Size(1280, 720), // duplicate the size since Nexus 5X emulator has the case.
+                Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+                Size(800, 450), // 16:9
+                Size(640, 480), // 4:3
+                Size(320, 240), // 4:3
+                Size(320, 180), // 16:9
+                Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
+            )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
@@ -903,9 +1064,18 @@
         2. supportedOutputSizes for ImageCapture and Preview in
         SupportedSurfaceCombination#getAllPossibleSizeArrangements are the same.
         */
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE, targetResolution = DISPLAY_SIZE)
-        val preview = createUseCase(PREVIEW_USE_CASE, targetResolution = DISPLAY_SIZE)
-        val imageAnalysis = createUseCase(IMAGE_ANALYSIS_USE_CASE, targetResolution = DISPLAY_SIZE)
+        val imageCapture = useCaseCreator.createUseCase(
+            IMAGE_CAPTURE_USE_CASE,
+            preferredResolution = preferredResolution
+        )
+        val preview = useCaseCreator.createUseCase(
+            PREVIEW_USE_CASE,
+            preferredResolution = preferredResolution
+        )
+        val imageAnalysis = useCaseCreator.createUseCase(
+            IMAGE_ANALYSIS_USE_CASE,
+            preferredResolution = preferredResolution
+        )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
             imageCapture,
@@ -918,24 +1088,33 @@
     }
 
     @Test
-    fun setTargetAspectRatioForMixedUseCases() {
+    fun setTargetAspectRatioForMixedUseCases_LegacyApi() {
+        setTargetAspectRatioForMixedUseCases(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun setTargetAspectRatioForMixedUseCases_ResolutionSelector() {
+        setTargetAspectRatioForMixedUseCases(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun setTargetAspectRatioForMixedUseCases(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -964,45 +1143,18 @@
     }
 
     @Test
-    fun legacyVideo_getSuggestedResolutionsForCustomizedSupportedResolutions() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val formatResolutionsPairList = arrayListOf<Pair<Int, Array<Size>>>().apply {
-            add(Pair.create(ImageFormat.JPEG, arrayOf(RESOLUTION_VGA)))
-            add(Pair.create(ImageFormat.YUV_420_888, arrayOf(RESOLUTION_VGA)))
-            add(Pair.create(ImageFormat.PRIVATE, arrayOf(RESOLUTION_VGA)))
-        }
-        // Sets use cases customized supported resolutions to 640x480 only.
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
-            supportedSurfaceCombination,
-            imageCapture,
-            videoCapture,
-            preview
-        )
-        // Checks all suggested resolutions will become 640x480.
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(RESOLUTION_VGA)
+    fun getSuggestedResolutionsForCustomizedSupportedResolutions_LegacyApi() {
+        getSuggestedResolutionsForCustomizedSupportedResolutions(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForCustomizedSupportedResolutions() {
+    fun getSuggestedResolutionsForCustomizedSupportedResolutions_ResolutionSelector() {
+        getSuggestedResolutionsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun getSuggestedResolutionsForCustomizedSupportedResolutions(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -1015,12 +1167,12 @@
             add(Pair.create(ImageFormat.PRIVATE, arrayOf(RESOLUTION_VGA)))
         }
         // Sets use cases customized supported resolutions to 640x480 only.
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
             supportedResolutions = formatResolutionsPairList
         )
         val videoCapture = createVideoCapture(Quality.SD)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             supportedResolutions = formatResolutionsPairList
         )
@@ -1154,17 +1306,27 @@
     }
 
     @Test
-    fun isAspectRatioMatchWithSupportedMod16Resolution() {
+    fun isAspectRatioMatchWithSupportedMod16Resolution_LegacyApi() {
+        isAspectRatioMatchWithSupportedMod16Resolution(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun isAspectRatioMatchWithSupportedMod16Resolution_ResolutionSelector() {
+        isAspectRatioMatchWithSupportedMod16Resolution(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun isAspectRatioMatchWithSupportedMod16Resolution(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9,
-            defaultResolution = MOD16_SIZE
+            Surface.ROTATION_90,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            preferredResolution = MOD16_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
         assertThat(suggestedResolutionMap[useCase]).isEqualTo(MOD16_SIZE)
@@ -1196,7 +1358,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(FAKE_USE_CASE)
+        val useCase = createUseCaseByLegacyApi(FAKE_USE_CASE)
         // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
         // removed. No any aspect ratio related setting. The returned sizes list will be sorted in
         // descending order.
@@ -1223,7 +1385,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_4_3
         )
@@ -1249,14 +1411,14 @@
     }
 
     @Test
-    fun getSupportedOutputSizes_aspectRatio16x9() {
+    fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -1287,7 +1449,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -1335,14 +1497,14 @@
     }
 
     @Test
-    fun getSupportedOutputSizes_targetResolution1080x1920InRotation0() {
+    fun getSupportedOutputSizes_targetResolution1080x1920InRotation0_InLimitedDevice() {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 1920)
         )
@@ -1375,7 +1537,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 1920)
         )
@@ -1429,7 +1591,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1280, 960)
@@ -1464,7 +1626,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 240)
@@ -1494,7 +1656,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(320, 240)
         )
@@ -1518,7 +1680,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1800, 1440)
@@ -1553,7 +1715,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1280, 600)
@@ -1585,7 +1747,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(1280, 720)
         )
@@ -1598,7 +1760,20 @@
     }
 
     @Test
-    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution() {
+    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_LegacyApi() {
+        previewCanSelectResolutionLargerThanDisplay_withMaxResolution(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_ResolutionSelector() {
+        previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -1606,7 +1781,7 @@
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // The max resolution is expressed in the sensor coordinate.
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             maxResolution = MAXIMUM_SIZE
         )
@@ -1623,7 +1798,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             defaultResolution = Size(1280, 720)
         )
@@ -1644,7 +1819,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             defaultResolution = Size(1280, 720),
@@ -1685,7 +1860,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1920, 1080)
@@ -1712,7 +1887,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(320, 240)
         )
@@ -1739,7 +1914,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 240),
@@ -1769,7 +1944,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 180),
@@ -1793,7 +1968,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(3840, 2160),
@@ -1834,7 +2009,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 190),
@@ -1864,7 +2039,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(192, 144)
@@ -1891,7 +2066,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(192, 144)
         )
@@ -1917,7 +2092,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(185, 90)
@@ -1951,7 +2126,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 2016)
         )
@@ -1990,7 +2165,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2036,7 +2211,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2070,7 +2245,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2113,7 +2288,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2152,7 +2327,21 @@
     }
 
     @Test
-    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists() {
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_LegacyApi() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_SensorSize() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(resolutionSelectorUseCaseCreator)
+    }
+
+    @Test
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_ViewSize() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(viewSizeResolutionSelectorUseCaseCreator)
+    }
+
+    private fun canGet640x480_whenAnotherGroupMatchedInMod16Exists(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
             supportedSizes = arrayOf(
@@ -2171,10 +2360,10 @@
         )
         // Sets the target resolution as 640x480 with target rotation as ROTATION_90 because the
         // sensor orientation is 90.
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RESOLUTION_VGA
+            preferredResolution = RESOLUTION_VGA
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
         // Checks 640x480 is final selected for the use case.
@@ -2182,7 +2371,20 @@
     }
 
     @Test
-    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet() {
+    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_LegacyApi() {
+        canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_ResolutionSelector() {
+        canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
             supportedSizes = arrayOf(Size(480, 480))
@@ -2191,7 +2393,7 @@
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // Sets the max resolution as 720x1280
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             maxResolution = DISPLAY_SIZE
         )
@@ -2201,14 +2403,40 @@
     }
 
     @Test
-    fun previewSizeIsSelectedForImageAnalysis_imageCaptureHasNoSetSizeInLimitedDevice() {
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_LegacyApi() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            legacyUseCaseCreator, PREVIEW_SIZE
+        )
+    }
+
+    // For the ResolutionSelector API, RECORD_SIZE can't be used because it exceeds
+    // PREVIEW_SIZE. Therefore, the logic will fallback to select a 4:3 PREVIEW_SIZE. Then,
+    // 640x480 will be selected.
+    @Test
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_SensorSize() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            resolutionSelectorUseCaseCreator, RESOLUTION_VGA
+        )
+    }
+
+    @Test
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_ViewSize() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            viewSizeResolutionSelectorUseCaseCreator, RESOLUTION_VGA
+        )
+    }
+
+    private fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+        useCaseCreator: UseCaseCreator,
+        expectedResult: Size
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -2218,7 +2446,7 @@
             )
         )
         // ImageCapture has no explicit target resolution setting
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE)
+        val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
         // A LEGACY-level above device supports the following configuration.
         //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
         //
@@ -2228,10 +2456,10 @@
         // Even there is a RECORD size target resolution setting for ImageAnalysis, ImageCapture
         // will still have higher priority to have a MAXIMUM size resolution if the app doesn't
         // explicitly specify a RECORD size target resolution to ImageCapture.
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -2239,18 +2467,40 @@
             imageCapture,
             imageAnalysis
         )
-        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(expectedResult)
     }
 
     @Test
-    fun recordSizeIsSelectedForImageAnalysis_imageCaptureHasExplicitSizeInLimitedDevice() {
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_LegacyApi() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            legacyUseCaseCreator
+        )
+    }
+
+    @Test
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_SensorSize() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    @Test
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_ViewSize() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            viewSizeResolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -2260,10 +2510,10 @@
             )
         )
         // ImageCapture has no explicit RECORD size target resolution setting
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         // A LEGACY-level above device supports the following configuration.
         //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
@@ -2274,10 +2524,10 @@
         // A RECORD can be selected for ImageAnalysis if the ImageCapture has a explicit RECORD
         // size target resolution setting. It means that the application know the trade-off and
         // the ImageAnalysis has higher priority to get a larger resolution than ImageCapture.
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -2288,6 +2538,92 @@
         assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(RECORD_SIZE)
     }
 
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsSelected_whenHighResolutionIsEnabled() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 6000))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsNotSelected_whenHighResolutionIsEnabled_withoutBurstCaptureCapability() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsNotSelected_whenHighResolutionIsNotEnabled_targetResolution8000x6000() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase =
+            createUseCaseByResolutionSelector(FAKE_USE_CASE, preferredResolution = Size(8000, 6000))
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsSelected_whenHighResolutionIsEnabled_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true
+        )
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 4500))
+    }
+
     /**
      * Sets up camera according to the specified settings and initialize [CameraX].
      *
@@ -2308,33 +2644,18 @@
         sensorOrientation: Int = SENSOR_ORIENTATION_90,
         pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
         supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+        supportedHighResolutionSizes: Array<Size>? = null,
         capabilities: IntArray? = null
     ) {
-        val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
-            Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
-            // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
-            // output sizes need to be retrieved via SurfaceTexture.class.
-            Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java)).thenReturn(supportedSizes)
-            // This is setup for the test to determine RECORD size from StreamConfigurationMap
-            Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java)).thenReturn(supportedSizes)
-        }
-
-        val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
-        Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
-            set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
-            set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
-            set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
-            set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
-            set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
-            capabilities?.let {
-                set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
-            }
-        }
-
-        val cameraManager = ApplicationProvider.getApplicationContext<Context>()
-            .getSystemService(Context.CAMERA_SERVICE) as CameraManager
-        (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
-            .addCamera(cameraId, characteristics)
+        setupCamera(
+            cameraId,
+            hardwareLevel,
+            sensorOrientation,
+            pixelArraySize,
+            supportedSizes,
+            supportedHighResolutionSizes,
+            capabilities
+        )
 
         @LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
             CameraCharacteristics.LENS_FACING_BACK
@@ -2482,7 +2803,7 @@
      * @param supportedResolutions the customized supported resolutions. Default is null.
      */
     @Suppress("DEPRECATION")
-    private fun createUseCase(
+    private fun createUseCaseByLegacyApi(
         useCaseType: Int,
         targetRotation: Int = UNKNOWN_ROTATION,
         targetAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
@@ -2495,7 +2816,6 @@
             PREVIEW_USE_CASE -> Preview.Builder()
             IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
             IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
-            VIDEO_CAPTURE_USE_CASE -> androidx.camera.core.VideoCapture.Builder()
             else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
         }
         if (targetRotation != UNKNOWN_ROTATION) {
@@ -2548,4 +2868,17 @@
             this.sourceState = sourceState
         }
     }
+
+    private interface UseCaseCreator {
+        fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int = UNKNOWN_ROTATION,
+            preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
+            preferredResolution: Size? = null,
+            maxResolution: Size? = null,
+            highResolutionEnabled: Boolean = false,
+            defaultResolution: Size? = null,
+            supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
+        ): UseCase
+    }
 }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
index 5771cbb..4fae45a 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
@@ -193,5 +193,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
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/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
index 3f88ffa..884de88 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/processing/DefaultSurfaceProcessorTest.kt
@@ -22,7 +22,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.ImageFormatConstants
@@ -315,7 +314,6 @@
             CameraEffect.PREVIEW,
             ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
             Size(WIDTH, HEIGHT),
-            USE_SURFACE_TEXTURE_TRANSFORM,
             Size(WIDTH, HEIGHT),
             Rect(0, 0, WIDTH, HEIGHT),
             /*rotationDegrees=*/0,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 9cd621b..f6d8e3c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -23,6 +23,7 @@
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_FORMAT;
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_ROTATION_ENABLED;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -31,6 +32,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_CLASS;
@@ -263,9 +265,35 @@
                     ? mSubscribedAnalyzer.getDefaultTargetResolution() : null;
         }
 
-        if (analyzerResolution != null
-                && !builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
-            builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, analyzerResolution);
+        if (analyzerResolution != null) {
+            if (!builder.getMutableConfig().containsOption(OPTION_RESOLUTION_SELECTOR)) {
+                int targetRotation = builder.getMutableConfig().retrieveOption(
+                        OPTION_TARGET_ROTATION, Surface.ROTATION_0);
+                // analyzerResolution is a size in the sensor coordinate system, but the legacy
+                // target resolution setting is in the view coordinate system. Flips the
+                // analyzerResolution according to the sensor rotation degrees.
+                if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
+                    analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
+                            /* height= */ analyzerResolution.getWidth());
+                }
+
+                if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
+                    builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
+                            analyzerResolution);
+                }
+            } else {
+                // Merges analyzerResolution or default resolution to ResolutionSelector.
+                ResolutionSelector resolutionSelector =
+                        builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR);
+
+                if (resolutionSelector.getPreferredResolution() == null) {
+                    ResolutionSelector.Builder resolutionSelectorBuilder =
+                            ResolutionSelector.Builder.fromSelector(resolutionSelector);
+                    resolutionSelectorBuilder.setPreferredResolution(analyzerResolution);
+                    builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+                            resolutionSelectorBuilder.build());
+                }
+            }
         }
 
         return builder.getUseCaseConfig();
@@ -1119,15 +1147,9 @@
         @Override
         @NonNull
         public ImageAnalysis build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same"
-                                + " config.");
-            }
-            return new ImageAnalysis(getUseCaseConfig());
+            ImageAnalysisConfig imageAnalysisConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(imageAnalysisConfig);
+            return new ImageAnalysis(imageAnalysisConfig);
         }
 
         // Implementations of TargetConfig.Builder default methods
@@ -1317,6 +1339,55 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>ImageAnalysis has a default minimal bounding size as 640x480. The input
+         * {@link ResolutionSelector}'s' preferred resolution can override the minimal bounding
+         * size to find the best resolution.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, which resolution will
+         * be finally selected will depend on the camera device's hardware level, capabilities
+         * and the bound use cases combination. The device hardware level and capabilities
+         * information can be retrieved via the interop class
+         * {@link androidx.camera.camera2.interop.Camera2CameraInfo#getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key)}
+         * with
+         * {@link android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL} and
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES}.
+         *
+         * <p>A {@code LIMITED-level} above device can support a {@code RECORD} size resolution
+         * for {@link ImageAnalysis} when it is bound together with {@link Preview} and
+         * {@link ImageCapture}. The trade-off is the selected resolution for the
+         * {@link ImageCapture} will also be restricted by the {@code RECORD} size. To
+         * successfully select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
+         * {@code RECORD} size preferred resolution should be set on both {@link ImageCapture} and
+         * {@link ImageAnalysis}. This indicates that the application clearly understand the
+         * trade-off and prefer the {@link ImageAnalysis} to have a larger resolution rather than
+         * the {@link ImageCapture} to have a {@code MAXIMUM} size resolution. For the
+         * definitions of {@code RECORD}, {@code MAXIMUM} sizes and more details see the
+         * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
+         * section in {@link android.hardware.camera2.CameraDevice}'s. The {@code RECORD} size
+         * refers to the camera device's maximum supported recording resolution, as determined by
+         * {@link CamcorderProfile}. The {@code MAXIMUM} size refers to the camera device's
+         * maximum output resolution for that format or target from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link ImageAnalysis} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1421,5 +1492,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index a689360..8d9b4ed 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -41,7 +41,9 @@
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.is90or270;
@@ -2799,15 +2801,6 @@
         @Override
         @NonNull
         public ImageCapture build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-
             // Update the input format base on the other options set (mainly whether processing
             // is done)
             Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
@@ -2824,7 +2817,9 @@
                 }
             }
 
-            ImageCapture imageCapture = new ImageCapture(getUseCaseConfig());
+            ImageCaptureConfig imageCaptureConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(imageCaptureConfig);
+            ImageCapture imageCapture = new ImageCapture(imageCaptureConfig);
 
             // Makes the crop aspect ratio match the target resolution setting as what mentioned
             // in javadoc of setTargetResolution(). When the target resolution is set, {@link
@@ -3142,6 +3137,33 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>If no resolution selector is set, the largest available resolution will be selected
+         * to use. Usually, users will intend to get the largest still image that the camera
+         * device can support. Unlike {@link Builder#setTargetResolution(Size)},
+         * {@link #setCropAspectRatio(Rational)} won't be automatically called to set the
+         * corresponding value and crop the output image when a target resolution is set. Use
+         * {@link ViewPort} instead if the output images need to be cropped in a specific
+         * aspect ratio.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link ImageCapture} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         /** @hide */
         @NonNull
         @RestrictTo(Scope.LIBRARY_GROUP)
@@ -3305,5 +3327,18 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
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..f266b2d 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
@@ -16,9 +16,9 @@
 
 package androidx.camera.core;
 
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.PreviewConfig.IMAGE_INFO_PROCESSOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_BACKGROUND_EXECUTOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
@@ -34,10 +34,10 @@
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_CLASS;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_NAME;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_RESOLUTION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_ROTATION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 
 import static java.util.Collections.singletonList;
@@ -314,7 +314,7 @@
         clearPipeline();
 
         // Create nodes and edges.
-        mNode = new SurfaceProcessorNode(camera, USE_SURFACE_TEXTURE_TRANSFORM, mSurfaceProcessor);
+        mNode = new SurfaceProcessorNode(camera, mSurfaceProcessor);
         SettableSurface cameraSurface = new SettableSurface(
                 CameraEffect.PREVIEW,
                 resolution,
@@ -323,7 +323,7 @@
                 /*hasEmbeddedTransform=*/true,
                 requireNonNull(getCropRect(resolution)),
                 getRelativeRotation(camera),
-                /*mirroring=*/false,
+                /*mirroring=*/true,
                 this::notifyReset);
         SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
         SurfaceEdge outputEdge = mNode.transform(inputEdge);
@@ -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));
+            }
         }
     }
 
@@ -628,6 +633,21 @@
             builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                     ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
         }
+
+        // Merges Preview's default max resolution setting when resolution selector is used
+        ResolutionSelector resolutionSelector =
+                builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
+        if (resolutionSelector != null && resolutionSelector.getMaxResolution() == null) {
+            Size maxResolution = builder.getMutableConfig().retrieveOption(OPTION_MAX_RESOLUTION);
+            if (maxResolution != null) {
+                ResolutionSelector.Builder resolutionSelectorBuilder =
+                        ResolutionSelector.Builder.fromSelector(resolutionSelector);
+                resolutionSelectorBuilder.setMaxResolution(maxResolution);
+                builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+                        resolutionSelectorBuilder.build());
+            }
+        }
+
         return builder.getUseCaseConfig();
     }
 
@@ -861,16 +881,9 @@
         @NonNull
         @Override
         public Preview build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-
-            return new Preview(getUseCaseConfig());
+            PreviewConfig previewConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(previewConfig);
+            return new Preview(previewConfig);
         }
 
         // Implementations of TargetConfig.Builder default methods
@@ -1069,6 +1082,38 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, the selected
+         * resolution will be limited by the {@code PREVIEW} size which is defined as the best
+         * size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
+         * smaller. See the
+         * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
+         * section in {@link android.hardware.camera2.CameraDevice}'. If the
+         * {@link ResolutionSelector} contains the max resolution setting larger than the {@code
+         * PREVIEW} size, a size larger than the device's screen resolution or 1080p can be
+         * selected to use for {@link Preview}.
+         *
+         * <p>Note that due to compatibility reasons, CameraX may select a resolution that is
+         * larger than the default screen resolution on certain devices.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link Preview} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1200,5 +1245,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
new file mode 100644
index 0000000..6e4a640
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
@@ -0,0 +1,387 @@
+/*
+ * 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;
+
+import android.util.Size;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.impl.SizeCoordinate;
+
+/**
+ * A set of requirements and priorities used to select a resolution for the use case.
+ *
+ * <p>The resolution selection mechanism is determined by the following three steps:
+ * <ol>
+ *     <li> Collect supported output sizes to the candidate resolution list
+ *     <li> Determine the selecting priority of the candidate resolution list by the preference
+ *     settings
+ *     <li> Consider all the resolution selector settings of bound use cases to find the best
+ *     resolution for each use case
+ * </ol>
+ *
+ * <p>For the first step, all supported resolution output sizes are put into the candidate
+ * resolution list as the base in the beginning.
+ *
+ * <p>ResolutionSelector provides the following two functions for applications to adjust the
+ * conditions of the candidate resolutions.
+ * <ul>
+ *     <li> {@link Builder#setMaxResolution(Size)}
+ *     <li> {@link Builder#setHighResolutionEnabled(boolean)}
+ * </ul>
+ *
+ * <p>For the second step, ResolutionSelector provides the following three functions for
+ * applications to determine which resolution has higher priority to be selected.
+ * <ul>
+ *     <li> {@link Builder#setPreferredResolution(Size)}
+ *     <li> {@link Builder#setPreferredResolutionByViewSize(Size)}
+ *     <li> {@link Builder#setPreferredAspectRatio(int)}
+ * </ul>
+ *
+ * <p>The resolution that exactly matches the preferred resolution is selected in first priority.
+ * If the resolution can't be found, CameraX falls back to use the sizes of the preferred aspect
+ * ratio. In this case, the preferred resolution is treated as the minimal bounding size to find
+ * the best resolution.
+ *
+ * <p>Different types of use cases might have their own additional conditions. Please see the use
+ * case config builders’ {@code setResolutionSelector()} function to know the condition details
+ * for each type of use case.
+ *
+ * <p>For the third step, CameraX selects the final resolution for the use case based on the
+ * camera device's hardware level, capabilities and the bound use case combination. Applications
+ * can check which resolution is finally selected by using the use case's {@code
+ * getResolutionInfo()} function.
+ *
+ * @hide
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ResolutionSelector {
+    @Nullable
+    private final Size mPreferredResolution;
+
+    private final SizeCoordinate mSizeCoordinate;
+
+    private final int mPreferredAspectRatio;
+
+    @Nullable
+    private final Size mMaxResolution;
+
+    private final boolean mIsHighResolutionEnabled;
+
+    ResolutionSelector(int preferredAspectRatio,
+            @Nullable Size preferredResolution,
+            @NonNull SizeCoordinate sizeCoordinate,
+            @Nullable Size maxResolution,
+            boolean isHighResolutionEnabled) {
+        mPreferredAspectRatio = preferredAspectRatio;
+        mPreferredResolution = preferredResolution;
+        mSizeCoordinate = sizeCoordinate;
+        mMaxResolution = maxResolution;
+        mIsHighResolutionEnabled = isHighResolutionEnabled;
+    }
+
+    /**
+     * Retrieves the preferred aspect ratio in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @AspectRatio.Ratio
+    public int getPreferredAspectRatio() {
+        return mPreferredAspectRatio;
+    }
+
+    /**
+     * Retrieves the preferred resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public Size getPreferredResolution() {
+        return mPreferredResolution;
+    }
+
+    /**
+     * Retrieves the size coordinate of the preferred resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public SizeCoordinate getSizeCoordinate() {
+        return mSizeCoordinate;
+    }
+
+    /**
+     * Returns the max resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public Size getMaxResolution() {
+        return mMaxResolution;
+    }
+
+    /**
+     * Returns {@code true} if high resolutions are allowed to be selected, otherwise {@code false}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public boolean isHighResolutionEnabled() {
+        return mIsHighResolutionEnabled;
+    }
+
+    /**
+     * Builder for a {@link ResolutionSelector}.
+     */
+    public static final class Builder {
+        @AspectRatio.Ratio
+        private int mPreferredAspectRatio = AspectRatio.RATIO_4_3;
+        @Nullable
+        private Size mPreferredResolution = null;
+        @NonNull
+        private SizeCoordinate mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
+        @Nullable
+        private Size mMaxResolution = null;
+        private boolean mIsHighResolutionEnabled = false;
+
+        /**
+         * Creates a new Builder object.
+         */
+        public Builder() {
+        }
+
+        private Builder(@NonNull ResolutionSelector selector) {
+            mPreferredAspectRatio = selector.getPreferredAspectRatio();
+            mPreferredResolution = selector.getPreferredResolution();
+            mSizeCoordinate = selector.getSizeCoordinate();
+            mMaxResolution = selector.getMaxResolution();
+            mIsHighResolutionEnabled = selector.isHighResolutionEnabled();
+        }
+
+        /**
+         * Generates a Builder from another {@link ResolutionSelector} object.
+         *
+         * @param selector an existing {@link ResolutionSelector}.
+         * @return the new Builder.
+         */
+        @NonNull
+        public static Builder fromSelector(@NonNull ResolutionSelector selector) {
+            return new Builder(selector);
+        }
+
+        /**
+         * Sets the preferred aspect ratio that the output images are expected to have.
+         *
+         * <p>The aspect ratio is the ratio of width to height in the camera sensor's natural
+         * orientation. If set, CameraX finds the sizes that match the aspect ratio with priority
+         * . Among the sizes that match the aspect ratio, the larger the size, the higher the
+         * priority.
+         *
+         * <p>If CameraX can't find any available sizes that match the preferred aspect ratio,
+         * CameraX falls back to select the sizes with the nearest aspect ratio that can contain
+         * the full field of view of the sizes with preferred aspect ratio.
+         *
+         * <p>If preferred aspect ratio is not set, the default aspect ratio is
+         * {@link AspectRatio#RATIO_4_3}, which usually has largest field of view because most
+         * camera sensor are {@code 4:3}.
+         *
+         * <p>This API is useful for apps that want to capture images matching the {@code 16:9}
+         * display aspect ratio. Apps can set preferred aspect ratio as
+         * {@link AspectRatio#RATIO_16_9} to achieve this.
+         *
+         * <p>The actual aspect ratio of the output may differ from the specified preferred
+         * aspect ratio value. Application code should check the resulting output's resolution.
+         *
+         * @param preferredAspectRatio the aspect ratio you prefer to use.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredAspectRatio(@AspectRatio.Ratio int preferredAspectRatio) {
+            mPreferredAspectRatio = preferredAspectRatio;
+            return this;
+        }
+
+        /**
+         * Sets the preferred resolution you expect to select. The resolution is expressed in the
+         * camera sensor's natural orientation (landscape), which means you can set the size
+         * retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes} directly.
+         *
+         * <p>Once the preferred resolution is set, CameraX finds exactly matched size first
+         * regardless of the preferred aspect ratio. This API is useful for apps that want to
+         * select an exact size retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
+         *
+         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
+         * to establish a minimal bound for the given resolution. The actual resolution is the
+         * closest available resolution that is not smaller than the preferred resolution.
+         * However, if no resolution exists that is equal to or larger than the preferred
+         * resolution, the nearest available resolution smaller than the preferred resolution is
+         * chosen.
+         *
+         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
+         * the preferred aspect ratio to find the sizes that either match it or are close to it.
+         * Using preferred resolution as the minimal bound is useful for apps that want to shrink
+         * the size for the surface. For example, for apps that just show the camera preview in a
+         * small view, apps can specify a size smaller than display size. CameraX can effectively
+         * select a smaller size for better efficiency.
+         *
+         * <p>If both {@link Builder#setPreferredResolution(Size)} and
+         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, which one set
+         * later overrides the one set before.
+         *
+         * @param preferredResolution the preferred resolution expressed in the orientation of
+         *                            the device camera sensor coordinate to choose the preferred
+         *                            resolution from supported output sizes list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredResolution(@NonNull Size preferredResolution) {
+            mPreferredResolution = preferredResolution;
+            mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
+            return this;
+        }
+
+        /**
+         * Sets the preferred resolution you expect to select. The resolution is expressed in the
+         * Android {@link View} coordinate system.
+         *
+         * <p>For phone devices, the sensor coordinate orientation usually has 90 degrees
+         * difference from the phone device display’s natural orientation. Depending on the
+         * display rotation value when the use case is bound, CameraX transforms the input
+         * resolution into the camera sensor's natural orientation to find the best suitable
+         * resolution.
+         *
+         * <p>Once the preferred resolution is set, CameraX finds the size that exactly matches
+         * the preferred resolution first regardless of the preferred aspect ratio.
+         *
+         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
+         * to establish a minimal bound for the given resolution. The actual resolution is the
+         * closest available resolution that is not smaller than the preferred resolution.
+         * However, if no resolution exists that is equal to or larger than the preferred
+         * resolution, the nearest available resolution smaller than the preferred resolution is
+         * chosen.
+         *
+         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
+         * the preferred aspect ratio to find the sizes that either match it or are close to it.
+         * Using Android {@link View} size as preferred resolution is useful for apps that want
+         * to shrink the size for the surface. For example, for apps that just show the camera
+         * preview in a small view, apps can specify the small size of Android {@link View}.
+         * CameraX can effectively select a smaller size for better efficiency.
+         *
+         * <p>If both {@link Builder#setPreferredResolution(Size)} and
+         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, the later setting
+         * overrides the former one.
+         *
+         * @param preferredResolutionByViewSize the preferred resolution expressed in the
+         *                                      orientation of the app layout's Android
+         *                                      {@link View} to choose the preferred resolution
+         *                                      from supported output sizes list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredResolutionByViewSize(
+                @NonNull Size preferredResolutionByViewSize) {
+            mPreferredResolution = preferredResolutionByViewSize;
+            mSizeCoordinate = SizeCoordinate.ANDROID_VIEW;
+            return this;
+        }
+
+        /**
+         * Sets the max resolution condition for the use case.
+         *
+         * <p>The max resolution prevents the use case to select the sizes which either width or
+         * height exceeds the specified resolution.
+         *
+         * <p>The resolution should be expressed in the camera sensor's natural orientation
+         * (landscape).
+         *
+         * <p>For example, if applications want to select a resolution smaller than a specific
+         * resolution to have better performance, a {@link ResolutionSelector} which sets this
+         * specific resolution as the max resolution can be used. Or, if applications want to
+         * select a larger resolution for a {@link Preview} which has the default max resolution
+         * of the small one of device's screen size and 1080p (1920x1080), use a
+         * {@link ResolutionSelector} with max resolution.
+         *
+         * @param resolution the max resolution limitation to choose from supported output sizes
+         *                   list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setMaxResolution(@NonNull Size resolution) {
+            mMaxResolution = resolution;
+            return this;
+        }
+
+        /**
+         * Sets whether high resolutions are allowed to be selected for the use cases.
+         *
+         * <p>Calling this function allows the use case to select the high resolution output
+         * sizes if it is supported for the camera device.
+         *
+         * <p>When high resolution is enabled, if an {@link ImageCapture} with
+         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is bound, the
+         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is forced disabled.
+         *
+         * <p>When using the {@code camera-extensions} to enable an extension mode, even if high
+         * resolution is enabled, the supported high resolution output sizes are still excluded
+         * from the candidate resolution list.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, the supported
+         * high resolutions are retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}.
+         * Be noticed that the high resolution sizes might cause the entire capture session to
+         * not meet the 20 fps frame rate. Even if only an ImageCapture use case selects a high
+         * resolution, it might still impact the FPS of the Preview, ImageAnalysis or
+         * VideoCapture use cases which are bound together. This function only takes effect on
+         * devices with
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
+         * capability. For devices without
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
+         * capability, all resolutions can be retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes(int)},
+         * but it is not guaranteed to meet >= 20 fps for any resolution in the list.
+         *
+         * @param enabled {@code true} to allow to select high resolution for the use case.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setHighResolutionEnabled(boolean enabled) {
+            mIsHighResolutionEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ResolutionSelector}.
+         *
+         * @return the {@link ResolutionSelector} built with the specified resolution settings.
+         */
+        @NonNull
+        public ResolutionSelector build() {
+            return new ResolutionSelector(mPreferredAspectRatio, mPreferredResolution,
+                    mSizeCoordinate, mMaxResolution, mIsHighResolutionEnabled);
+        }
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 6bf27c5..1ee0da0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -177,16 +177,4 @@
             return new AutoValue_SurfaceOutput_Event(code, surfaceOutput);
         }
     }
-
-    /** OpenGL transformation options for SurfaceOutput. */
-    enum GlTransformOptions {
-        /** Apply only the value of {@link SurfaceTexture#getTransformMatrix(float[])}. */
-        USE_SURFACE_TEXTURE_TRANSFORM,
-
-        /**
-         * Discard the value of {@link SurfaceTexture#getTransformMatrix(float[])} and calculate
-         * the transform based on crop rect, rotation degrees and mirroring.
-         */
-        APPLY_CROP_ROTATE_AND_MIRRORING,
-    }
 }
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/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index fc294a5..515f2b18 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -236,6 +236,13 @@
             mergedConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO);
         }
 
+        // Forces disable ZSL when high resolution is enabled.
+        if (mergedConfig.containsOption(ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)
+                && mergedConfig.retrieveOption(
+                ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).isHighResolutionEnabled()) {
+            mergedConfig.insertOption(UseCaseConfig.OPTION_ZSL_DISABLED, true);
+        }
+
         return onMergeConfig(cameraInfo, getUseCaseConfigBuilder(mergedConfig));
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 6e77562..bd6734b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -26,6 +27,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
@@ -1495,15 +1497,9 @@
         @Override
         @NonNull
         public VideoCapture build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-            return new VideoCapture(getUseCaseConfig());
+            VideoCaptureConfig videoCaptureConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(videoCaptureConfig);
+            return new VideoCapture(videoCaptureConfig);
         }
 
         /**
@@ -1753,6 +1749,15 @@
             return this;
         }
 
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1849,6 +1854,15 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 
     /**
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/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index 334bc6c..aa1eaed 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -26,6 +26,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.AspectRatio;
+import androidx.camera.core.ResolutionSelector;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -88,6 +89,12 @@
     Option<List<Pair<Integer, Size[]>>> OPTION_SUPPORTED_RESOLUTIONS =
             Option.create("camerax.core.imageOutput.supportedResolutions", List.class);
 
+    /**
+     * Option: camerax.core.imageOutput.resolutionSelector
+     */
+    Option<ResolutionSelector> OPTION_RESOLUTION_SELECTOR =
+            Option.create("camerax.core.imageOutput.resolutionSelector", ResolutionSelector.class);
+
     // *********************************************************************************************
 
     /**
@@ -243,6 +250,29 @@
     }
 
     /**
+     * Retrieves the resolution selector can be used by the target from this configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default ResolutionSelector getResolutionSelector(@Nullable ResolutionSelector valueIfMissing) {
+        return retrieveOption(OPTION_RESOLUTION_SELECTOR, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the resolution selector can be used by the target from this configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @NonNull
+    default ResolutionSelector getResolutionSelector() {
+        return retrieveOption(OPTION_RESOLUTION_SELECTOR);
+    }
+
+    /**
      * Retrieves the supported resolutions can be used by the target from this configuration.
      *
      * <p>Pair list is composed with {@link ImageFormat} and {@link Size} array. The returned
@@ -258,6 +288,39 @@
     }
 
     /**
+     * Checks whether the input config contains any conflicted settings.
+     *
+     * @param config to be validated.
+     * @throws IllegalArgumentException if both the target aspect ratio and the target resolution
+     * settings are contained in the config, or if either the target aspect ratio or the target
+     * resolution is contained when a resolution selector has been set in the config.
+     */
+    static void validateConfig(@NonNull ImageOutputConfig config) {
+        boolean hasTargetAspectRatio = config.hasTargetAspectRatio();
+        boolean hasTargetResolution = config.getTargetResolution(null) != null;
+
+        // Case 1. Error at runtime for using both setTargetResolution and setTargetAspectRatio on
+        // the same config.
+        if (hasTargetAspectRatio && hasTargetResolution) {
+            throw new IllegalArgumentException(
+                    "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+                            + "config.");
+        }
+
+        ResolutionSelector resolutionSelector = config.getResolutionSelector(null);
+
+        if (resolutionSelector != null) {
+            // Case 2. Error at runtime for using setTargetResolution or setTargetAspectRatio
+            // with setResolutionSelector on the same config.
+            if (hasTargetAspectRatio || hasTargetResolution) {
+                throw new IllegalArgumentException(
+                        "Cannot use setTargetResolution or setTargetAspectRatio with "
+                                + "setResolutionSelector on the same config.");
+            }
+        }
+    }
+
+    /**
      * Builder for a {@link ImageOutputConfig}.
      *
      * @param <B> The top level builder type for which this builder is composed with.
@@ -336,6 +399,15 @@
          */
         @NonNull
         B setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutionsList);
+
+        /**
+         * Sets the resolution selector can be used by target from this configuration.
+         *
+         * @param resolutionSelector The resolution selector to select a preferred resolution.
+         * @return The current Builder.
+         */
+        @NonNull
+        B setResolutionSelector(@NonNull ResolutionSelector resolutionSelector);
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
new file mode 100644
index 0000000..b1cfbf1
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
@@ -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.camera.core.impl;
+
+/**
+ * The size coordinate system enum.
+ */
+public enum SizeCoordinate {
+    /**
+     * Size is expressed in the camera sensor's natural orientation (landscape).
+     */
+    CAMERA_SENSOR,
+
+    /**
+     * Size is expressed in the Android View's orientation.
+     */
+    ANDROID_VIEW
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 4acaae6..15c47e1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -88,6 +88,12 @@
     Option<Boolean> OPTION_ZSL_DISABLED =
             Option.create("camerax.core.useCase.zslDisabled", boolean.class);
 
+    /**
+     * Option: camerax.core.useCase.highResolutionDisabled
+     */
+    Option<Boolean> OPTION_HIGH_RESOLUTION_DISABLED =
+            Option.create("camerax.core.useCase.highResolutionDisabled", boolean.class);
+
 
     // *********************************************************************************************
 
@@ -297,6 +303,17 @@
     }
 
     /**
+     * Retrieves the flag whether high resolution is disabled.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in
+     * this configuration
+     */
+    default boolean isHigResolutionDisabled(boolean valueIfMissing) {
+        return retrieveOption(OPTION_HIGH_RESOLUTION_DISABLED, valueIfMissing);
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
@@ -392,6 +409,17 @@
         B setZslDisabled(boolean disabled);
 
         /**
+         * Sets high resolution disabled or not.
+         *
+         * <p> High resolution will be disabled when Extension is ON.
+         *
+         * @param disabled True if high resolution should be disabled. Otherwise, should not be
+         *                 disabled.
+         */
+        @NonNull
+        B setHighResolutionDisabled(boolean disabled);
+
+        /**
          * Retrieves the configuration used by this builder.
          *
          * @return the configuration used by this builder.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
index 7528936..e0eac35 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.internal.utils.SizeUtil.getArea;
 
+import android.graphics.RectF;
 import android.util.Rational;
 import android.util.Size;
 
@@ -38,6 +39,7 @@
     public static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
     public static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
     public static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
+
     private static final int ALIGN16 = 16;
 
     private AspectRatioUtil() {
@@ -94,7 +96,6 @@
         return false;
     }
 
-
     private static boolean ratioIntersectsMod16Segment(int height, int mod16Width,
             Rational aspectRatio) {
         Preconditions.checkArgument(mod16Width % 16 == 0);
@@ -104,14 +105,26 @@
                 mod16Width + ALIGN16);
     }
 
-    /** Comparator based on how close they are to the target aspect ratio. */
+    /**
+     * Comparator based on how close they are to the target aspect ratio by comparing the
+     * transformed mapping area in the full FOV ratio space.
+     *
+     * The mapping area will be the region that the images of the specific aspect ratio cropped
+     * from the full FOV images. Therefore, we can compare the mapping areas to know which one is
+     * closer to the mapping area of the target aspect ratio setting.
+     */
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-    public static final class CompareAspectRatiosByDistanceToTargetRatio implements
+    public static final class CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace implements
             Comparator<Rational> {
-        private Rational mTargetRatio;
+        private final Rational mTargetRatio;
+        private final RectF mTransformedMappingArea;
+        private final Rational mFullFovRatio;
 
-        public CompareAspectRatiosByDistanceToTargetRatio(@NonNull Rational targetRatio) {
+        public CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                @NonNull Rational targetRatio, @Nullable Rational fullFovRatio) {
             mTargetRatio = targetRatio;
+            mFullFovRatio = fullFovRatio != null ? fullFovRatio : new Rational(4, 3);
+            mTransformedMappingArea = getTransformedMappingArea(mTargetRatio);
         }
 
         @Override
@@ -120,11 +133,81 @@
                 return 0;
             }
 
-            final Float lhsRatioDelta = Math.abs(lhs.floatValue() - mTargetRatio.floatValue());
-            final Float rhsRatioDelta = Math.abs(rhs.floatValue() - mTargetRatio.floatValue());
+            RectF lhsMappingArea = getTransformedMappingArea(lhs);
+            RectF rhsMappingArea = getTransformedMappingArea(rhs);
 
-            int result = (int) Math.signum(lhsRatioDelta - rhsRatioDelta);
-            return result;
+            boolean isCoveredByLhs = isMappingAreaCovered(lhsMappingArea,
+                    mTransformedMappingArea);
+            boolean isCoveredByRhs = isMappingAreaCovered(rhsMappingArea,
+                    mTransformedMappingArea);
+
+            if (isCoveredByLhs && isCoveredByRhs) {
+                // When both ratios can cover the transformed target aspect mapping area in the
+                // full FOV space, checks which area is smaller to determine which ratio is
+                // closer to the target aspect ratio.
+                return (int) Math.signum(
+                        getMappingAreaSize(lhsMappingArea) - getMappingAreaSize(rhsMappingArea));
+            } else if (isCoveredByLhs) {
+                return -1;
+            } else if (isCoveredByRhs) {
+                return 1;
+            } else {
+                // When both ratios can't cover the transformed target aspect mapping area in the
+                // full FOV space, checks which overlapping area is larger to determine which
+                // ratio is closer to the target aspect ratio.
+                float lhsOverlappingArea = getOverlappingAreaSize(lhsMappingArea,
+                        mTransformedMappingArea);
+                float rhsOverlappingArea = getOverlappingAreaSize(rhsMappingArea,
+                        mTransformedMappingArea);
+                return -((int) Math.signum(lhsOverlappingArea - rhsOverlappingArea));
+            }
+        }
+
+        /**
+         * Returns the rectangle after transforming the input rational into full FOV aspect ratio
+         * space.
+         */
+        private RectF getTransformedMappingArea(Rational ratio) {
+            if (ratio.floatValue() == mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        mFullFovRatio.getDenominator());
+            } else if (ratio.floatValue() > mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        (float) ratio.getDenominator() * (float) mFullFovRatio.getNumerator()
+                                / (float) ratio.getNumerator());
+            } else {
+                return new RectF(0, 0,
+                        (float) ratio.getNumerator() * (float) mFullFovRatio.getDenominator()
+                                / (float) ratio.getDenominator(), mFullFovRatio.getDenominator());
+            }
+        }
+
+        /**
+         * Returns {@code true} if the source transformed mapping area can fully cover the target
+         * transformed mapping area. Otherwise, returns {@code false};
+         */
+        private boolean isMappingAreaCovered(RectF sourceMappingArea, RectF targetMappingArea) {
+            return sourceMappingArea.width() >= targetMappingArea.width()
+                    && sourceMappingArea.height() >= targetMappingArea.height();
+        }
+
+        /**
+         * Returns the input mapping area's size value.
+         */
+        private float getMappingAreaSize(RectF mappingArea) {
+            return mappingArea.width() * mappingArea.height();
+        }
+
+        /**
+         * Returns the overlapping area value between the input two mapping areas in the full FOV
+         * space.
+         */
+        private float getOverlappingAreaSize(RectF mappingArea1, RectF mappingArea2) {
+            float overlappingAreaWidth = mappingArea1.width() < mappingArea2.width()
+                    ? mappingArea1.width() : mappingArea2.width();
+            float overlappingAreaHeight = mappingArea1.height() < mappingArea2.height()
+                    ? mappingArea1.height() : mappingArea2.height();
+            return overlappingAreaWidth * overlappingAreaHeight;
         }
     }
 }
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/internal/utils/SizeUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
index 0c8df56..1eb7a4a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
@@ -19,7 +19,12 @@
 import android.util.Size;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Utility class for size related operations.
@@ -40,4 +45,34 @@
     public static int getArea(@NonNull Size size) {
         return size.getWidth() * size.getHeight();
     }
+
+    /**
+     * Returns {@code true} if the source size area is smaller than the target size area.
+     * Otherwise, returns {@code false}.
+     */
+    public static boolean isSmallerByArea(@NonNull Size sourceSize, @NonNull Size targetSize) {
+        return getArea(sourceSize) < getArea(targetSize);
+    }
+
+    /**
+     * Returns {@code true} if any edge of the source size is longer than the corresponding edge of
+     * the target size. Otherwise, returns {@code false}.
+     */
+    public static boolean isLongerInAnyEdge(@NonNull Size sourceSize, @NonNull Size targetSize) {
+        return sourceSize.getWidth() > targetSize.getWidth()
+                || sourceSize.getHeight() > targetSize.getHeight();
+    }
+
+    /**
+     * Returns the size which has the max area in the input size list. Returns null if the input
+     * size list is empty.
+     */
+    @Nullable
+    public static Size getMaxSize(@NonNull List<Size> sizeList) {
+        if (sizeList.isEmpty()) {
+            return null;
+        }
+
+        return Collections.max(sizeList, new CompareSizesByArea());
+    }
 }
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..8584670 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
@@ -37,7 +37,6 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraEffect;
 import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceOutput.GlTransformOptions;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.SurfaceRequest.TransformationInfo;
@@ -254,7 +253,6 @@
      * <p>Do not provide the {@link SurfaceOutput} to external target if the
      * {@link ListenableFuture} fails.
      *
-     * @param glTransformOptions OpenGL transformation options for SurfaceOutput
      * @param resolution         resolution of input image buffer
      * @param cropRect           crop rect of input image buffer
      * @param rotationDegrees    expected rotation to the input image buffer
@@ -262,8 +260,7 @@
      */
     @MainThread
     @NonNull
-    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(
-            @NonNull GlTransformOptions glTransformOptions, @NonNull Size resolution,
+    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size resolution,
             @NonNull Rect cropRect, int rotationDegrees, boolean mirroring) {
         checkMainThread();
         Preconditions.checkState(!mHasConsumer, "Consumer can only be linked once.");
@@ -276,9 +273,9 @@
                     } catch (SurfaceClosedException e) {
                         return Futures.immediateFailedFuture(e);
                     }
-                    SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(
-                            surface, getTargets(), getFormat(), getSize(), glTransformOptions,
-                            resolution, cropRect, rotationDegrees, mirroring);
+                    SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
+                            getTargets(), getFormat(), getSize(), resolution, cropRect,
+                            rotationDegrees, mirroring);
                     surfaceOutputImpl.getCloseFuture().addListener(this::decrementUseCount,
                             directExecutor());
                     mConsumerToNotify = surfaceOutputImpl;
@@ -411,7 +408,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/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 6a2894a..04956c5 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -16,7 +16,6 @@
 
 package androidx.camera.core.processing;
 
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING;
 import static androidx.camera.core.impl.utils.MatrixExt.preRotate;
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
 import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
@@ -64,7 +63,6 @@
     private final int mFormat;
     @NonNull
     private final Size mSize;
-    private final GlTransformOptions mGlTransformOptions;
     private final Size mInputSize;
     private final Rect mInputCropRect;
     private final int mRotationDegrees;
@@ -93,8 +91,6 @@
             int targets,
             int format,
             @NonNull Size size,
-            // TODO(b/241910577): remove this flag when PreviewView handles cropped stream.
-            @NonNull GlTransformOptions glTransformOptions,
             @NonNull Size inputSize,
             @NonNull Rect inputCropRect,
             int rotationDegree,
@@ -103,20 +99,11 @@
         mTargets = targets;
         mFormat = format;
         mSize = size;
-        mGlTransformOptions = glTransformOptions;
         mInputSize = inputSize;
         mInputCropRect = new Rect(inputCropRect);
         mMirroring = mirroring;
-
-        if (mGlTransformOptions == APPLY_CROP_ROTATE_AND_MIRRORING) {
-            mRotationDegrees = rotationDegree;
-            calculateGlTransform();
-        } else {
-            // TODO(b/241910577): remove this assignment when the PreviewView handles cropped
-            //  stream.
-            mRotationDegrees = 0;
-        }
-
+        mRotationDegrees = rotationDegree;
+        calculateGlTransform();
         mCloseFuture = CallbackToFutureAdapter.getFuture(
                 completer -> {
                     mCloseFutureCompleter = completer;
@@ -248,16 +235,7 @@
     @AnyThread
     @Override
     public void updateTransformMatrix(@NonNull float[] output, @NonNull float[] input) {
-        switch (mGlTransformOptions) {
-            case USE_SURFACE_TEXTURE_TRANSFORM:
-                System.arraycopy(input, 0, output, 0, 16);
-                break;
-            case APPLY_CROP_ROTATE_AND_MIRRORING:
-                System.arraycopy(mGlTransform, 0, output, 0, 16);
-                break;
-            default:
-                throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
-        }
+        System.arraycopy(input, 0, output, 0, 16);
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
index 6007535..347a8ca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceProcessorNode.java
@@ -36,7 +36,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceOutput.GlTransformOptions;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.impl.CameraInternal;
@@ -60,7 +59,6 @@
 @SuppressWarnings("UnusedVariable")
 public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
 
-    private final GlTransformOptions mGlTransformOptions;
     @NonNull
     final SurfaceProcessorInternal mSurfaceProcessor;
     @NonNull
@@ -74,15 +72,12 @@
     /**
      * Constructs the {@link SurfaceProcessorNode}.
      *
-     * @param cameraInternal     the associated camera instance.
-     * @param glTransformOptions the OpenGL transformation options.
-     * @param surfaceProcessor   the interface to wrap around.
+     * @param cameraInternal   the associated camera instance.
+     * @param surfaceProcessor the interface to wrap around.
      */
     public SurfaceProcessorNode(@NonNull CameraInternal cameraInternal,
-            @NonNull GlTransformOptions glTransformOptions,
             @NonNull SurfaceProcessorInternal surfaceProcessor) {
         mCameraInternal = cameraInternal;
-        mGlTransformOptions = glTransformOptions;
         mSurfaceProcessor = surfaceProcessor;
     }
 
@@ -112,63 +107,43 @@
         final Runnable onSurfaceInvalidated = inputSurface::invalidate;
 
         SettableSurface outputSurface;
-        switch (mGlTransformOptions) {
-            case APPLY_CROP_ROTATE_AND_MIRRORING:
-                Size resolution = inputSurface.getSize();
-                Rect cropRect = inputSurface.getCropRect();
-                int rotationDegrees = inputSurface.getRotationDegrees();
-                boolean mirroring = inputSurface.getMirroring();
+        Size resolution = inputSurface.getSize();
+        Rect cropRect = inputSurface.getCropRect();
+        int rotationDegrees = inputSurface.getRotationDegrees();
+        boolean mirroring = inputSurface.getMirroring();
 
-                // Calculate rotated resolution and cropRect
-                Size rotatedCroppedSize = is90or270(rotationDegrees)
-                        ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
-                        : rectToSize(cropRect);
+        // Calculate rotated resolution and cropRect
+        Size rotatedCroppedSize = is90or270(rotationDegrees)
+                ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
+                : rectToSize(cropRect);
 
-                // Calculate sensorToBufferTransform
-                android.graphics.Matrix sensorToBufferTransform =
-                        new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
-                android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
-                        new RectF(cropRect), rotationDegrees, mirroring);
-                sensorToBufferTransform.postConcat(imageTransform);
+        // Calculate sensorToBufferTransform
+        android.graphics.Matrix sensorToBufferTransform =
+                new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
+        android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
+                new RectF(cropRect), rotationDegrees, mirroring);
+        sensorToBufferTransform.postConcat(imageTransform);
 
-                outputSurface = new SettableSurface(
-                        inputSurface.getTargets(),
-                        rotatedCroppedSize,
-                        inputSurface.getFormat(),
-                        sensorToBufferTransform,
-                        // The Surface transform cannot be carried over during buffer copy.
-                        /*hasEmbeddedTransform=*/false,
-                        sizeToRect(rotatedCroppedSize),
-                        /*rotationDegrees=*/0,
-                        /*mirroring=*/false,
-                        onSurfaceInvalidated);
-                break;
-            case USE_SURFACE_TEXTURE_TRANSFORM:
-                // No transform output as placeholder.
-                outputSurface = new SettableSurface(
-                        inputSurface.getTargets(),
-                        inputSurface.getSize(),
-                        inputSurface.getFormat(),
-                        inputSurface.getSensorToBufferTransform(),
-                        // The Surface transform cannot be carried over during buffer copy.
-                        /*hasEmbeddedTransform=*/false,
-                        inputSurface.getCropRect(),
-                        inputSurface.getRotationDegrees(),
-                        inputSurface.getMirroring(),
-                        onSurfaceInvalidated);
-                break;
-            default:
-                throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
-        }
+        outputSurface = new SettableSurface(
+                inputSurface.getTargets(),
+                rotatedCroppedSize,
+                inputSurface.getFormat(),
+                sensorToBufferTransform,
+                // The Surface transform cannot be carried over during buffer copy.
+                /*hasEmbeddedTransform=*/false,
+                sizeToRect(rotatedCroppedSize),
+                /*rotationDegrees=*/0,
+                /*mirroring=*/false,
+                onSurfaceInvalidated);
+
         return outputSurface;
     }
 
     private void sendSurfacesToProcessorWhenReady(@NonNull SettableSurface input,
             @NonNull SettableSurface output) {
         SurfaceRequest surfaceRequest = input.createSurfaceRequest(mCameraInternal);
-        Futures.addCallback(output.createSurfaceOutputFuture(mGlTransformOptions,
-                        input.getSize(), input.getCropRect(), input.getRotationDegrees(),
-                        input.getMirroring()),
+        Futures.addCallback(output.createSurfaceOutputFuture(input.getSize(), input.getCropRect(),
+                        input.getRotationDegrees(), input.getMirroring()),
                 new FutureCallback<SurfaceOutput>() {
                     @Override
                     public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 9cb4ec3e..0bc5cd9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.content.Context;
@@ -37,6 +38,7 @@
 import androidx.camera.core.impl.TagBundle;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
+import androidx.camera.core.internal.utils.SizeUtil;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CameraXUtil;
 import androidx.camera.testing.fakes.FakeAppConfig;
@@ -75,6 +77,8 @@
 
     private static final Size APP_RESOLUTION = new Size(100, 200);
     private static final Size ANALYZER_RESOLUTION = new Size(300, 400);
+    private static final Size FLIPPED_ANALYZER_RESOLUTION = new Size(400, 300);
+    private static final Size DEFAULT_RESOLUTION = new Size(640, 480);
 
     private static final int QUEUE_DEPTH = 8;
     private static final int IMAGE_TAG = 0;
@@ -139,31 +143,71 @@
     }
 
     @Test
-    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution() {
-        assertThat(getMergedAnalyzerResolution(APP_RESOLUTION, ANALYZER_RESOLUTION)).isEqualTo(
+    public void canSetQueueDepth() {
+        assertThat(getMergedImageAnalysisConfig(null, null, QUEUE_DEPTH,
+                false).getImageQueueDepth()).isEqualTo(QUEUE_DEPTH);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_legacyApi() {
+        assertThat(getMergedImageAnalysisConfig(APP_RESOLUTION, ANALYZER_RESOLUTION, -1,
+                false).getTargetResolution()).isEqualTo(APP_RESOLUTION);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_resolutionSelector() {
+        ImageAnalysisConfig config = getMergedImageAnalysisConfig(APP_RESOLUTION,
+                ANALYZER_RESOLUTION, -1, true);
+        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
                 APP_RESOLUTION);
     }
 
     @Test
-    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution() {
-        assertThat(getMergedAnalyzerResolution(null, ANALYZER_RESOLUTION)).isEqualTo(
+    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_legacyApi() {
+        assertThat(
+                getMergedImageAnalysisConfig(null, ANALYZER_RESOLUTION, -1,
+                        false).getTargetResolution()).isEqualTo(FLIPPED_ANALYZER_RESOLUTION);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_resolutionSelector() {
+        ImageAnalysisConfig config = getMergedImageAnalysisConfig(null,
+                ANALYZER_RESOLUTION, -1, true);
+        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
                 ANALYZER_RESOLUTION);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void noAppOrAnalyzerResolution_noMergedOption() {
-        getMergedAnalyzerResolution(null, null);
+    public void noAppOrAnalyzerResolution_noMergedOption_legacyApi() {
+        getMergedImageAnalysisConfig(null, null, -1, false).getTargetResolution();
     }
 
     @NonNull
-    private Size getMergedAnalyzerResolution(
+    private ImageAnalysisConfig getMergedImageAnalysisConfig(
             @Nullable Size appResolution,
-            @Nullable Size analyzerResolution) {
-        // Arrange: set up ImageAnalysis with target resolution.
-        ImageAnalysis.Builder builder = new ImageAnalysis.Builder().setImageQueueDepth(QUEUE_DEPTH);
-        if (appResolution != null) {
-            builder.setTargetResolution(appResolution);
+            @Nullable Size analyzerResolution,
+            int queueDepth,
+            boolean useResolutionSelector) {
+        // Arrange: set up ImageAnalysis.
+        ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
+
+        // Sets preferred resolution by ResolutionSelector or legacy API
+        if (useResolutionSelector) {
+            ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder();
+            if (appResolution != null) {
+                resolutionSelectorBuilder.setPreferredResolution(appResolution);
+            }
+            builder.setResolutionSelector(resolutionSelectorBuilder.build());
+        } else {
+            if (appResolution != null) {
+                builder.setTargetResolution(appResolution);
+            }
         }
+
+        if (queueDepth >= 0) {
+            builder.setImageQueueDepth(QUEUE_DEPTH);
+        }
+
         mImageAnalysis = builder.build();
         // Analyzer that overrides the resolution.
         ImageAnalysis.Analyzer analyzer = new ImageAnalysis.Analyzer() {
@@ -181,12 +225,16 @@
         // Act: set the analyzer.
         mImageAnalysis.setAnalyzer(mBackgroundExecutor, analyzer);
 
-        // Assert: only the target resolution is overridden.
-        ImageAnalysisConfig mergedConfig = (ImageAnalysisConfig) mImageAnalysis.mergeConfigs(
-                new FakeCameraInfoInternal(), null, null);
+        return (ImageAnalysisConfig) mImageAnalysis.mergeConfigs(
+                new FakeCameraInfoInternal(90, CameraSelector.LENS_FACING_BACK), null,
+                null);
+    }
 
-        assertThat(mergedConfig.getImageQueueDepth()).isEqualTo(QUEUE_DEPTH);
-        return mergedConfig.getTargetResolution();
+    @NonNull
+    private ImageAnalysisConfig createDefaultConfig() {
+        ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
+        builder.setDefaultResolution(DEFAULT_RESOLUTION);
+        return builder.getUseCaseConfig();
     }
 
     @Test
@@ -360,6 +408,33 @@
         assertCanReceiveAnalysisImage(mImageAnalysis);
     }
 
+    @Test
+    public void throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                        .build());
+    }
+
+    @Test
+    public void throwException_whenSetTargetResolutionWithResolutionSelector() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                        .setResolutionSelector(new ResolutionSelector.Builder().build())
+                        .build());
+    }
+
+    @Test
+    public void throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                        .setResolutionSelector(new ResolutionSelector.Builder().build())
+                        .build());
+    }
+
     void assertCanReceiveAnalysisImage(ImageAnalysis imageAnalysis) throws InterruptedException {
         CountDownLatch latch = new CountDownLatch(1);
         imageAnalysis.setAnalyzer(CameraXExecutors.directExecutor(), image -> {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index 1ef77cb..7211a30 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -40,6 +40,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.internal.utils.SizeUtil
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
@@ -60,6 +61,7 @@
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicReference
 import org.junit.After
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -576,6 +578,32 @@
         assertThat(cameraControl.isZslConfigAdded).isTrue()
     }
 
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
     private fun bindImageCapture(
         captureMode: Int = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
         viewPort: ViewPort? = null,
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..9920500 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,8 @@
 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.internal.utils.SizeUtil
+import androidx.camera.core.processing.SettableSurface
 import androidx.camera.core.processing.SurfaceProcessorInternal
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -56,6 +58,8 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import kotlin.jvm.Throws
+import org.junit.Assert
 
 private val TEST_CAMERA_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
 
@@ -215,6 +219,78 @@
     }
 
     @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 createPreviewWithProcessor_mirroringIsTrue() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+
+        // Act: create pipeline
+        val preview = createPreview(processor)
+
+        // Assert: preview is mirrored by default.
+        assertThat(preview.getCameraSurface().mirroring).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.getCameraSurface().rotationDegrees).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.getCameraSurface().rotationDegrees).isEqualTo(180)
+
+        // Clean up
+        preview.onDetached()
+    }
+
+    private fun Preview.getCameraSurface(): SettableSurface {
+        return this.sessionConfig.surfaces.single() as SettableSurface
+    }
+
+    @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
         val processor = FakeSurfaceProcessorInternal(
@@ -223,7 +299,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 +339,7 @@
         val processor = FakeSurfaceProcessorInternal(
             mainThreadExecutor()
         )
-        val preview = createPreviewPipelineAndAttachProcessor(processor)
+        val preview = createPreview(processor)
         val originalSessionConfig = preview.sessionConfig
 
         // Act: invoke the error listener.
@@ -399,6 +475,32 @@
         assertThat(receivedAfterAttach).isTrue()
     }
 
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
     private fun bindToLifecycleAndGetSurfaceRequest(): SurfaceRequest {
         return bindToLifecycleAndGetResult(null).first
     }
@@ -438,9 +540,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/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
index 0d11439..184b2b8 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
@@ -21,6 +21,7 @@
 import android.os.Looper
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.utils.SizeUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
@@ -40,6 +41,7 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.junit.Assert
 
 @RunWith(RobolectricTestRunner::class)
 @Suppress("DEPRECATION")
@@ -87,4 +89,30 @@
 
         verify(callback).onError(eq(VideoCapture.ERROR_INVALID_CAMERA), anyString(), any())
     }
+
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
 }
\ No newline at end of file
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/impl/utils/AspectRatioUtilTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
index f42c007..c1ee35b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
@@ -19,7 +19,9 @@
 import android.os.Build
 import android.util.Rational
 import android.util.Size
-import com.google.common.truth.Truth
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import com.google.common.truth.Truth.assertThat
+import java.util.Collections
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -31,7 +33,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNullAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 null
@@ -41,7 +43,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withSameAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 Rational(16, 9)
@@ -51,7 +53,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_720p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1280, 720),
                 Rational(16, 9)
@@ -61,7 +63,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1080p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1920, 1088),
                 Rational(16, 9)
@@ -71,7 +73,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1440p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(2560, 1440),
                 Rational(16, 9)
@@ -81,7 +83,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_2160p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(3840, 2160),
                 Rational(16, 9)
@@ -91,7 +93,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1x1() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1088, 1088),
                 Rational(1, 1)
@@ -101,7 +103,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_4x3() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1024, 768),
                 Rational(4, 3)
@@ -111,11 +113,44 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNonMod16AspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1281, 721),
                 Rational(16, 9)
             )
         ).isFalse()
     }
+
+    @Test
+    fun sortAspectRatios() {
+        // Sort the aspect ratio key set by the target aspect ratio.
+        val aspectRatios = listOf(
+            Rational(1, 1),
+            Rational(4, 3),
+            Rational(16, 9),
+            Rational(18, 9),
+            Rational(14, 9),
+        )
+
+        val targetAspectRatio = Rational(16, 9)
+        val fullFovAspectRatio = Rational(4, 3)
+
+        Collections.sort(
+            aspectRatios,
+            CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                targetAspectRatio,
+                fullFovAspectRatio
+            )
+        )
+
+        val expectedResult = listOf(
+            Rational(16, 9),
+            Rational(14, 9),
+            Rational(4, 3),
+            Rational(18, 9),
+            Rational(1, 1),
+        )
+
+        assertThat(aspectRatios == expectedResult).isTrue()
+    }
 }
\ No newline at end of file
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..93b374f 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
@@ -26,7 +26,6 @@
 import android.view.Surface
 import androidx.camera.core.CameraEffect
 import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.Result.RESULT_REQUEST_CANCELLED
 import androidx.camera.core.SurfaceRequest.TransformationInfo
@@ -188,6 +187,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
@@ -281,7 +309,6 @@
 
     private fun createSurfaceOutputFuture(settableSurface: SettableSurface) =
         settableSurface.createSurfaceOutputFuture(
-            USE_SURFACE_TEXTURE_TRANSFORM,
             INPUT_SIZE,
             sizeToRect(INPUT_SIZE),
             /*rotationDegrees=*/0,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index 621eb1d..6f44748 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -23,8 +23,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
-import androidx.camera.core.SurfaceOutput.GlTransformOptions
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import com.google.common.truth.Truth.assertThat
@@ -119,29 +117,11 @@
         assertThat(hasRequestedClose).isFalse()
     }
 
-    @Test
-    fun updateMatrix_useSurfaceTextureTransform_sameResult() {
-        // Arrange.
-        val surfaceOut =
-            createFakeSurfaceOutputImpl(glTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM)
-
-        // Act.
-        val input = floatArrayOf(1f, 1f, 1f, 1f, 2f, 2f, 2f, 2f, 3f, 3f, 3f, 3f, 4f, 4f, 4f, 4f)
-        val result = FloatArray(16)
-        surfaceOut.updateTransformMatrix(result, input)
-
-        // Assert.
-        assertThat(result).isEqualTo(input)
-    }
-
-    private fun createFakeSurfaceOutputImpl(
-        glTransformOptions: GlTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM
-    ) = SurfaceOutputImpl(
+    private fun createFakeSurfaceOutputImpl() = SurfaceOutputImpl(
         fakeSurface,
         TARGET,
         FORMAT,
         OUTPUT_SIZE,
-        glTransformOptions,
         INPUT_SIZE,
         sizeToRect(INPUT_SIZE),
         /*rotationDegrees=*/0,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
index 3027ece..43fac3e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceProcessorNodeTest.kt
@@ -24,9 +24,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput.GlTransformOptions
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.utils.TransformUtils.is90or270
@@ -94,33 +91,11 @@
     }
 
     @Test
-    fun transformInput_useSurfaceTextureTransform_outputHasTheSameProperty() {
-        // Arrange.
-        createSurfaceProcessorNode()
-        createInputEdge()
-        val inputSurface = inputEdge.surfaces[0]
-
-        // Act.
-        val outputEdge = node.transform(inputEdge)
-
-        // Assert: without transformation, the output has the same property as the input.
-        assertThat(outputEdge.surfaces).hasSize(1)
-        val outputSurface = outputEdge.surfaces[0]
-        assertThat(outputSurface.size).isEqualTo(inputSurface.size)
-        assertThat(outputSurface.format).isEqualTo(inputSurface.format)
-        assertThat(outputSurface.targets).isEqualTo(inputSurface.targets)
-        assertThat(outputSurface.cropRect).isEqualTo(inputSurface.cropRect)
-        assertThat(outputSurface.rotationDegrees).isEqualTo(inputSurface.rotationDegrees)
-        assertThat(outputSurface.mirroring).isEqualTo(inputSurface.mirroring)
-        assertThat(outputSurface.hasEmbeddedTransform()).isFalse()
-    }
-
-    @Test
     fun transformInput_applyCropRotateAndMirroring_outputIsCroppedAndRotated() {
         val cropRect = Rect(200, 100, 600, 400)
         for (rotationDegrees in arrayOf(0, 90, 180, 270)) {
             // Arrange.
-            createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+            createSurfaceProcessorNode()
             createInputEdge(
                 size = rectToSize(cropRect),
                 cropRect = cropRect,
@@ -153,7 +128,7 @@
     fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
         for (mirroring in arrayOf(false, true)) {
             // Arrange.
-            createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+            createSurfaceProcessorNode()
             createInputEdge(mirroring = mirroring)
 
             // Act.
@@ -173,7 +148,7 @@
     @Test
     fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
         // Arrange.
-        createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+        createSurfaceProcessorNode()
         createInputEdge(rotationDegrees = 90, cropRect = Rect(0, 0, 600, 400))
 
         // Act.
@@ -192,7 +167,7 @@
     @Test
     fun setRotationToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
         // Arrange.
-        createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+        createSurfaceProcessorNode()
         createInputEdge(rotationDegrees = 90)
         val inputSurface = inputEdge.surfaces[0]
         val outputEdge = node.transform(inputEdge)
@@ -269,12 +244,9 @@
         inputEdge = SurfaceEdge.create(listOf(surface))
     }
 
-    private fun createSurfaceProcessorNode(
-        glTransformOptions: GlTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM
-    ) {
+    private fun createSurfaceProcessorNode() {
         node = SurfaceProcessorNode(
             FakeCamera(),
-            glTransformOptions,
             surfaceProcessorInternal
         )
     }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
index f0be504..ea27418 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
@@ -73,9 +73,12 @@
 
     private lateinit var baseCameraSelector: CameraSelector
 
+    private lateinit var extensionsCameraSelector: CameraSelector
+
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
     @Before
-    @Throws(Exception::class)
-    fun setUp() {
+    fun setUp(): Unit = runBlocking {
         assumeTrue(
             ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(
                 lensFacing,
@@ -91,6 +94,15 @@
         )[10000, TimeUnit.MILLISECONDS]
 
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
+
+        extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
+            baseCameraSelector,
+            extensionMode
+        )
+
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
+        }
     }
 
     @After
@@ -143,13 +155,6 @@
                     })
             )
 
-            val fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
-
-            val extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
-                baseCameraSelector,
-                extensionMode
-            )
-
             cameraProvider.bindToLifecycle(
                 fakeLifecycleOwner,
                 extensionsCameraSelector,
@@ -180,4 +185,18 @@
             )
         )
     }
+
+    @Test
+    fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
+        val imageCapture = ImageCapture.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                extensionsCameraSelector,
+                imageCapture)
+        }
+
+        assertThat(imageCapture.currentConfig.isHigResolutionDisabled(false)).isTrue()
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
index 241e9ab..f6f6235 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
@@ -70,6 +70,10 @@
 
     private lateinit var baseCameraSelector: CameraSelector
 
+    private lateinit var extensionsCameraSelector: CameraSelector
+
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
     private val surfaceTextureLatch = CountDownLatch(1)
     private val frameReceivedLatch = CountDownLatch(1)
     private var isSurfaceTextureReleased = false
@@ -106,8 +110,7 @@
     }
 
     @Before
-    @Throws(Exception::class)
-    fun setUp() {
+    fun setUp(): Unit = runBlocking {
         assumeTrue(
             ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(
                 lensFacing,
@@ -123,6 +126,15 @@
         )[10000, TimeUnit.MILLISECONDS]
 
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
+
+        extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
+            baseCameraSelector,
+            extensionMode
+        )
+
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
+        }
     }
 
     @After
@@ -156,17 +168,9 @@
                 SurfaceTextureProvider.createSurfaceTextureProvider(createSurfaceTextureCallback())
             )
 
-            val fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
-
-            val extensionEnabledCameraSelector =
-                extensionsManager.getExtensionEnabledCameraSelector(
-                    baseCameraSelector,
-                    extensionMode
-                )
-
             cameraProvider.bindToLifecycle(
                 fakeLifecycleOwner,
-                extensionEnabledCameraSelector,
+                extensionsCameraSelector,
                 preview
             )
         }
@@ -178,6 +182,20 @@
         assertThat(frameReceivedLatch.await(10000, TimeUnit.MILLISECONDS)).isTrue()
     }
 
+    @Test
+    fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                extensionsCameraSelector,
+                preview)
+        }
+
+        assertThat(preview.currentConfig.isHigResolutionDisabled(false)).isTrue()
+    }
+
     private fun createSurfaceTextureCallback(): SurfaceTextureProvider.SurfaceTextureCallback =
         object : SurfaceTextureProvider.SurfaceTextureCallback {
             override fun onSurfaceTextureReady(
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index d12a1ac3..fa38f9c 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -127,6 +127,7 @@
         List<Pair<Integer, Size[]>> supportedResolutions =
                 vendorExtender.getSupportedCaptureOutputResolutions();
         builder.setSupportedResolutions(supportedResolutions);
+        builder.setHighResolutionDisabled(true);
     }
 
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index b97d8d0..2019e0b 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -131,6 +131,7 @@
         List<Pair<Integer, Size[]>> supportedResolutions =
                 vendorExtender.getSupportedPreviewOutputResolutions();
         builder.setSupportedResolutions(supportedResolutions);
+        builder.setHighResolutionDisabled(true);
     }
 
     /**
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index 98bfd46..40a7f15 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -26,7 +26,10 @@
 }
 
 dependencies {
-    implementation("androidx.test:core:1.4.0")
+    implementation(libs.testCore)
+    // force runner 1.5.1 so implementation stays consistent with androidTestImplementation
+
+    implementation(libs.testRunner)
     implementation(libs.testRules)
     implementation(libs.testUiautomator)
     api("androidx.annotation:annotation:1.2.0")
@@ -35,8 +38,8 @@
     api(project(":camera:camera-core"))
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
-    implementation("androidx.test.espresso:espresso-core:3.3.0")
-    implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
+    implementation("androidx.test.espresso:espresso-core:3.5.0")
+    implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
     implementation(libs.junit)
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesAndroid)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 71bc549..d4871bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
@@ -223,6 +224,13 @@
             return this;
         }
 
+        @NonNull
+        @Override
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         /**
          * Sets specific image format to the fake use case.
          */
@@ -238,5 +246,12 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
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..5f8641f 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
@@ -17,9 +17,9 @@
 package androidx.camera.video;
 
 import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
@@ -27,6 +27,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
@@ -66,6 +67,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
@@ -455,7 +457,7 @@
             } else {
                 surfaceRequest.updateTransformationInfo(
                         SurfaceRequest.TransformationInfo.of(cropRect, relativeRotation,
-                                targetRotation));
+                                targetRotation, /*hasCameraTransform=*/true));
             }
         }
     }
@@ -732,7 +734,6 @@
         if (mSurfaceProcessor != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded) {
             Logger.d(TAG, "Surface processing is enabled.");
             return new SurfaceProcessorNode(requireNonNull(getCamera()),
-                    APPLY_CROP_ROTATE_AND_MIRRORING,
                     mSurfaceProcessor != null ? mSurfaceProcessor : new DefaultSurfaceProcessor());
         }
         return null;
@@ -1410,6 +1411,15 @@
             return this;
         }
 
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1506,5 +1516,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index e27e9b7..a371cb4 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -19,7 +19,7 @@
     method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Quality? getVideoCaptureTargetQuality();
     method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
     method @MainThread public boolean isImageAnalysisEnabled();
@@ -43,10 +43,11 @@
     method @MainThread public void setPinchToZoomEnabled(boolean);
     method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setTapToFocusEnabled(boolean);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void setVideoCaptureTargetQuality(androidx.camera.video.Quality?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void startRecording(androidx.camera.view.video.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.view.video.OnVideoSavedCallback);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void stopRecording();
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+    method @MainThread @RequiresApi(26) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
     method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
     field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
@@ -160,45 +161,14 @@
 
 package androidx.camera.view.video {
 
+  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo public class AudioConfig {
+    method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+    method public boolean getAudioEnabled();
+    field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+  }
+
   @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
   }
 
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class Metadata {
-    method public static androidx.camera.view.video.Metadata.Builder builder();
-    method public abstract android.location.Location? getLocation();
-  }
-
-  @com.google.auto.value.AutoValue.Builder public abstract static class Metadata.Builder {
-    method public abstract androidx.camera.view.video.Metadata build();
-    method public abstract androidx.camera.view.video.Metadata.Builder setLocation(android.location.Location?);
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo public interface OnVideoSavedCallback {
-    method public void onError(int, String, Throwable?);
-    method public void onVideoSaved(androidx.camera.view.video.OutputFileResults);
-    field public static final int ERROR_ENCODER = 1; // 0x1
-    field public static final int ERROR_FILE_IO = 4; // 0x4
-    field public static final int ERROR_INVALID_CAMERA = 5; // 0x5
-    field public static final int ERROR_MUXER = 2; // 0x2
-    field public static final int ERROR_RECORDING_IN_PROGRESS = 3; // 0x3
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class OutputFileOptions {
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(java.io.File);
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(android.os.ParcelFileDescriptor);
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
-    method public abstract androidx.camera.view.video.Metadata getMetadata();
-  }
-
-  @com.google.auto.value.AutoValue.Builder public abstract static class OutputFileOptions.Builder {
-    method public abstract androidx.camera.view.video.OutputFileOptions build();
-    method public abstract androidx.camera.view.video.OutputFileOptions.Builder setMetadata(androidx.camera.view.video.Metadata);
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class OutputFileResults {
-    method public abstract android.net.Uri? getSavedUri();
-  }
-
 }
 
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index fc7bb75..9c4856f 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -27,6 +27,7 @@
     api("androidx.lifecycle:lifecycle-common:2.0.0")
     api("androidx.annotation:annotation:1.2.0")
     api(project(":camera:camera-core"))
+    api(project(":camera:camera-video"))
     implementation(project(":camera:camera-lifecycle"))
     implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
     implementation(libs.guavaListenableFuture)
@@ -61,6 +62,7 @@
     androidTestImplementation(project(":camera:camera-camera2"))
     androidTestImplementation(project(":camera:camera-testing"))
     androidTestImplementation(project(":camera:camera-camera2-pipe-integration"))
+    androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
     androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
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/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
new file mode 100644
index 0000000..d037ab4
--- /dev/null
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -0,0 +1,665 @@
+/*
+ * 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.camera.view
+
+import android.Manifest
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.provider.MediaStore
+import android.util.Log
+import androidx.annotation.MainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError
+import androidx.camera.testing.fakes.FakeActivity
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.camera.video.FileDescriptorOutputOptions
+import androidx.camera.video.FileOutputOptions
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.OutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.camera.view.CameraController.VIDEO_CAPTURE
+import androidx.camera.view.video.AudioConfig
+import androidx.core.util.Consumer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class VideoCaptureDeviceTest(
+    private val initialQuality: TargetQuality,
+    private val nextQuality: TargetQuality
+) {
+
+    /**
+     * The helper class to workaround the issue that "null" cannot be accepted as a parameter value
+     * in Parameterized tests, ref: b/37086576
+     */
+    enum class TargetQuality {
+        None, FHD, HD, HIGHEST, LOWEST, SD, UHD;
+
+        fun get(): Quality? {
+            return when (this) {
+                None -> null
+                FHD -> Quality.FHD
+                HD -> Quality.HD
+                HIGHEST -> Quality.HIGHEST
+                LOWEST -> Quality.LOWEST
+                SD -> Quality.SD
+                UHD -> Quality.UHD
+            }
+        }
+    }
+
+    companion object {
+        private const val VIDEO_TIMEOUT_SEC = 10L
+        private const val VIDEO_RECORDING_COUNT_DOWN = 5
+        private const val VIDEO_STARTED_COUNT_DOWN = 1
+        private const val VIDEO_SAVED_COUNT_DOWN = 1
+        private const val TAG = "VideoRecordingTest"
+
+        @JvmStatic
+        @BeforeClass
+        @Throws(ForegroundOccupiedError::class)
+        fun classSetUp() {
+            CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+        }
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "initialQuality={0}, nextQuality={1}")
+        fun data() = mutableListOf<Array<TargetQuality>>().apply {
+            add(arrayOf(TargetQuality.None, TargetQuality.FHD))
+            add(arrayOf(TargetQuality.FHD, TargetQuality.HD))
+            add(arrayOf(TargetQuality.HD, TargetQuality.HIGHEST))
+            add(arrayOf(TargetQuality.HIGHEST, TargetQuality.LOWEST))
+            add(arrayOf(TargetQuality.LOWEST, TargetQuality.SD))
+            add(arrayOf(TargetQuality.SD, TargetQuality.UHD))
+            add(arrayOf(TargetQuality.UHD, TargetQuality.None))
+        }
+    }
+
+    @get:Rule
+    val cameraRule: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    @get:Rule
+    val activityRule: ActivityScenarioRule<FakeActivity> =
+        ActivityScenarioRule(FakeActivity::class.java)
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        Manifest.permission.RECORD_AUDIO
+    )
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val audioEnabled = AudioConfig.create(true)
+    private val audioDisabled = AudioConfig.AUDIO_DISABLED
+    private lateinit var previewView: PreviewView
+    private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private lateinit var cameraController: LifecycleCameraController
+    private lateinit var activeRecording: Recording
+    private lateinit var latchForVideoStarted: CountDownLatch
+    private lateinit var latchForVideoPaused: CountDownLatch
+    private lateinit var latchForVideoResumed: CountDownLatch
+    private lateinit var latchForVideoSaved: CountDownLatch
+    private lateinit var latchForVideoRecording: CountDownLatch
+    private lateinit var finalize: VideoRecordEvent.Finalize
+
+    private val videoRecordEventListener = Consumer<VideoRecordEvent> {
+        when (it) {
+            is VideoRecordEvent.Start -> {
+                Log.d(TAG, "Recording start")
+                latchForVideoStarted.countDown()
+            }
+            is VideoRecordEvent.Finalize -> {
+                Log.d(TAG, "Recording finalize")
+                finalize = it
+                latchForVideoSaved.countDown()
+            }
+            is VideoRecordEvent.Status -> {
+                // Make sure the recording proceed for a while.
+                Log.d(TAG, "Recording Status")
+                latchForVideoRecording.countDown()
+            }
+            is VideoRecordEvent.Pause -> {
+                Log.d(TAG, "Recording Pause")
+                latchForVideoPaused.countDown()
+            }
+            is VideoRecordEvent.Resume -> {
+                Log.d(TAG, "Recording Resume")
+                latchForVideoResumed.countDown()
+            }
+            else -> {
+                throw IllegalStateException()
+            }
+        }
+    }
+
+    @Before
+    fun setUp() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        initialLifecycleOwner()
+        initialPreviewView()
+        initialController()
+    }
+
+    @After
+    fun tearDown() {
+        if (this::cameraController.isInitialized) {
+            instrumentation.runOnMainSync {
+                cameraController.shutDownForTests()
+            }
+        }
+    }
+
+    @Test
+    fun canRecordToMediaStore() {
+        // Arrange.
+        val resolver: ContentResolver = context.contentResolver
+        val outputOptions = createMediaStoreOutputOptions(resolver)
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = finalize.outputResults.outputUri
+        assertThat(uri).isNotEqualTo(Uri.EMPTY)
+        checkFileHasAudioAndVideo(uri)
+
+        // Cleanup.
+        resolver.delete(uri, null, null)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun canRecordToFileDescriptor() {
+        // Arrange.
+        val file = createTempFile()
+        val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
+        val outputOptions = FileDescriptorOutputOptions.Builder(fileDescriptor).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+
+        // Cleanup.
+        fileDescriptor.close()
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_withoutAudio_whenAudioDisabled() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioDisabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileOnlyHasVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenLifecycleStops() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                lifecycleOwner.pauseAndStop()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenTargetQualityChanged() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                cameraController.videoCaptureTargetQuality = nextQuality.get()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenEnabledUseCasesChanged() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                cameraController.setEnabledUseCases(IMAGE_ANALYSIS)
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_rightAfterPreviousRecordingStopped() {
+        // Arrange.
+        val file1 = createTempFile()
+        val file2 = createTempFile()
+        val outputOptions1 = FileOutputOptions.Builder(file1).build()
+        val outputOptions2 = FileOutputOptions.Builder(file2).build()
+
+        // Pre Act.
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+        recordVideo(outputOptions1, audioEnabled)
+        instrumentation.runOnMainSync {
+            activeRecording.stop()
+            assertThat(cameraController.isRecording).isFalse()
+        }
+
+        // Act.
+        instrumentation.runOnMainSync {
+            startRecording(outputOptions2, audioEnabled)
+            assertThat(cameraController.isRecording).isTrue()
+        }
+
+        // Wait for the Finalize event of the previous recording.
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // reset latches and wait for Start and Status events
+        latchForVideoStarted = CountDownLatch(VIDEO_STARTED_COUNT_DOWN)
+        latchForVideoRecording = CountDownLatch(VIDEO_RECORDING_COUNT_DOWN)
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+        assertThat(latchForVideoStarted.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+        assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Stop the second recording and wait for the Finalize event
+        instrumentation.runOnMainSync {
+            activeRecording.stop()
+            assertThat(cameraController.isRecording).isFalse()
+        }
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+        val uri1 = Uri.fromFile(file1)
+        checkFileHasAudioAndVideo(uri1)
+        val uri2 = Uri.fromFile(file2)
+        checkFileHasAudioAndVideo(uri2)
+
+        // Cleanup.
+        file1.delete()
+        file2.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenPauseAndStop() {
+        val pauseTimes = 1
+
+        // Arrange.
+        latchForVideoPaused = CountDownLatch(pauseTimes)
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                activeRecording.pause()
+            }
+            assertThat(latchForVideoPaused.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenPauseAndResumeInTheMiddle() {
+        val pauseTimes = 1
+        val resumeTimes = 1
+
+        // Arrange.
+        latchForVideoPaused = CountDownLatch(pauseTimes)
+        latchForVideoResumed = CountDownLatch(resumeTimes)
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                activeRecording.pause()
+            }
+            assertThat(latchForVideoPaused.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.resume()
+            }
+            assertThat(latchForVideoResumed.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun startRecording_throwsExceptionWhenAlreadyInRecording() {
+        // Arrange.
+        val file1 = createTempFile()
+        val file2 = createTempFile()
+        val outputOptions1 = FileOutputOptions.Builder(file1).build()
+        val outputOptions2 = FileOutputOptions.Builder(file2).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions1, audioEnabled) {
+            instrumentation.runOnMainSync {
+                assertThrows(java.lang.IllegalStateException::class.java) {
+                    activeRecording = cameraController.startRecording(
+                        outputOptions2,
+                        audioEnabled,
+                        CameraXExecutors.directExecutor()
+                    ) {}
+                }
+                activeRecording.stop()
+            }
+        }
+
+        // Cleanup.
+        file1.delete()
+        file2.delete()
+    }
+
+    private fun initialLifecycleOwner() {
+        lifecycleOwner = FakeLifecycleOwner()
+        lifecycleOwner.startAndResume()
+    }
+
+    private fun initialPreviewView() {
+        activityRule.scenario.onActivity { activity ->
+            previewView = PreviewView(context)
+            previewView.implementationMode = PreviewView.ImplementationMode.PERFORMANCE
+            activity.setContentView(previewView)
+        }
+    }
+
+    private fun initialController() {
+        cameraController = LifecycleCameraController(context)
+        cameraController.initializationFuture.get()
+        instrumentation.runOnMainSync {
+            if (initialQuality != TargetQuality.None) {
+                cameraController.videoCaptureTargetQuality = initialQuality.get()
+            }
+
+            //  If the PreviewView is not attached, the enabled use cases will not be applied.
+            previewView.controller = cameraController
+
+            cameraController.bindToLifecycle(lifecycleOwner)
+            cameraController.setEnabledUseCases(VIDEO_CAPTURE)
+        }
+    }
+
+    private fun createTempFile(): File {
+        return File.createTempFile("CameraX", ".tmp").apply {
+            deleteOnExit()
+        }
+    }
+
+    private fun createMediaStoreOutputOptions(resolver: ContentResolver): MediaStoreOutputOptions {
+        val videoFileName = "video_" + System.currentTimeMillis()
+        val contentValues = ContentValues()
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName)
+        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
+        return MediaStoreOutputOptions
+            .Builder(resolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+            .setContentValues(contentValues)
+            .build()
+    }
+
+    private fun recordVideoCompletely(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioConfig) {
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+    }
+
+    private fun recordVideoWithInterruptAction(
+        outputOptions: OutputOptions,
+        audioConfig: AudioConfig,
+        runInterruptAction: () -> Unit
+    ) {
+        // Arrange.
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+
+        // Act.
+        recordVideo(outputOptions, audioConfig)
+        runInterruptAction()
+
+        // Verify.
+        // Wait for finalize event to saved file.
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        instrumentation.runOnMainSync {
+            assertThat(cameraController.isRecording).isFalse()
+        }
+    }
+
+    private fun recordVideo(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        // Arrange.
+        latchForVideoStarted = CountDownLatch(VIDEO_STARTED_COUNT_DOWN)
+        latchForVideoRecording = CountDownLatch(VIDEO_RECORDING_COUNT_DOWN)
+
+        // Act.
+        instrumentation.runOnMainSync {
+            startRecording(outputOptions, audioConfig)
+            assertThat(cameraController.isRecording).isTrue()
+        }
+
+        // Verify.
+        assertThat(latchForVideoStarted.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Wait for status event to proceed recording for a while.
+        assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @MainThread
+    private fun startRecording(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        if (outputOptions is FileOutputOptions) {
+            activeRecording = cameraController.startRecording(
+                outputOptions,
+                audioConfig,
+                CameraXExecutors.directExecutor(),
+                videoRecordEventListener
+            )
+        } else if (outputOptions is FileDescriptorOutputOptions) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                activeRecording = cameraController.startRecording(
+                    outputOptions,
+                    audioConfig,
+                    CameraXExecutors.directExecutor(),
+                    videoRecordEventListener
+                )
+            } else {
+                throw UnsupportedOperationException(
+                    "File descriptors are not supported on pre-Android O (API 26) devices."
+                )
+            }
+        } else if (outputOptions is MediaStoreOutputOptions) {
+            activeRecording = cameraController.startRecording(
+                outputOptions,
+                audioConfig,
+                CameraXExecutors.directExecutor(),
+                videoRecordEventListener
+            )
+        } else {
+            throw IllegalArgumentException("Unsupported OutputOptions type.")
+        }
+    }
+
+    private fun checkFileOnlyHasVideo(uri: Uri) {
+        checkFileHasVideo(uri)
+        checkFileHasAudio(uri, false)
+    }
+
+    private fun checkFileHasAudioAndVideo(uri: Uri) {
+        checkFileHasVideo(uri)
+        checkFileHasAudio(uri, true)
+    }
+
+    private fun checkFileHasVideo(uri: Uri) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(context, uri)
+            val hasVideo = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+            assertThat(hasVideo).isEqualTo("yes")
+        }
+    }
+
+    private fun checkFileHasAudio(uri: Uri, hasAudio: Boolean) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(context, uri)
+            val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
+
+            assertThat(value).isEqualTo(if (hasAudio) "yes" else null)
+        }
+    }
+
+    private fun skipVideoRecordingTestOnCuttlefishApi29() {
+        // Skip test for b/168175357
+        Assume.assumeFalse(
+            "Cuttlefish has MediaCodec dequeInput/Output buffer fails issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+        )
+    }
+
+    private fun skipTestWithSurfaceProcessingOnCuttlefishApi30() {
+        // Skip test for b/253211491
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 70afe86..f92738c 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -19,9 +19,11 @@
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
 import static androidx.camera.view.CameraController.OutputSize.UNASSIGNED_ASPECT_RATIO;
+import static androidx.core.content.ContextCompat.getMainExecutor;
 
 import static java.util.Collections.emptyList;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -38,6 +40,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.AspectRatio;
@@ -64,15 +67,28 @@
 import androidx.camera.core.ViewPort;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.FallbackStrategy;
+import androidx.camera.video.FileDescriptorOutputOptions;
+import androidx.camera.video.FileOutputOptions;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.OutputOptions;
+import androidx.camera.video.PendingRecording;
+import androidx.camera.video.Quality;
+import androidx.camera.video.QualitySelector;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
 import androidx.camera.view.transform.OutputTransform;
+import androidx.camera.view.video.AudioConfig;
 import androidx.camera.view.video.ExperimentalVideo;
-import androidx.camera.view.video.OnVideoSavedCallback;
-import androidx.camera.view.video.OutputFileOptions;
-import androidx.camera.view.video.OutputFileResults;
+import androidx.core.content.PermissionChecker;
+import androidx.core.util.Consumer;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -81,10 +97,11 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * The abstract base camera controller class.
@@ -108,7 +125,6 @@
  * {@link UseCase}s freezes the preview for a short period of time. To avoid the glitch, the
  * {@link UseCase}s need to be enabled/disabled before the controller is set on {@link PreviewView}.
  */
-@SuppressWarnings("deprecation")
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public abstract class CameraController {
 
@@ -121,6 +137,8 @@
     private static final String CAMERA_NOT_ATTACHED = "Use cases not attached to camera.";
     private static final String IMAGE_CAPTURE_DISABLED = "ImageCapture disabled.";
     private static final String VIDEO_CAPTURE_DISABLED = "VideoCapture disabled.";
+    private static final String VIDEO_RECORDING_UNFINISHED = "Recording video. Only one recording"
+            + " can be active at a time.";
 
     // Auto focus is 1/6 of the area.
     private static final float AF_SIZE = 1.0f / 6.0f;
@@ -254,18 +272,17 @@
     @Nullable
     OutputSize mImageAnalysisTargetSize;
 
-    // Synthetic access
-    @SuppressWarnings("WeakerAccess")
     @NonNull
-    androidx.camera.core.VideoCapture mVideoCapture;
-
-    // Synthetic access
-    @SuppressWarnings("WeakerAccess")
-    @NonNull
-    final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
+    VideoCapture<Recorder> mVideoCapture;
 
     @Nullable
-    OutputSize mVideoCaptureOutputSize;
+    Recording mActiveRecording = null;
+
+    @NonNull
+    Map<Consumer<VideoRecordEvent>, Recording> mRecordingMap = new HashMap<>();
+
+    @Nullable
+    Quality mVideoCaptureQuality;
 
     // The latest bound camera.
     // Synthetic access
@@ -322,7 +339,7 @@
         mPreview = new Preview.Builder().build();
         mImageCapture = new ImageCapture.Builder().build();
         mImageAnalysis = new ImageAnalysis.Builder().build();
-        mVideoCapture = new androidx.camera.core.VideoCapture.Builder().build();
+        mVideoCapture = createNewVideoCapture();
 
         // Wait for camera to be initialized before binding use cases.
         mInitializationFuture = Futures.transform(
@@ -344,6 +361,18 @@
         };
     }
 
+    private static Recorder generateVideoCaptureRecorder(Quality videoQuality) {
+        Recorder.Builder builder = new Recorder.Builder();
+        if (videoQuality != null) {
+            builder.setQualitySelector(QualitySelector.from(
+                    videoQuality,
+                    FallbackStrategy.lowerQualityOrHigherThan(videoQuality)
+            ));
+        }
+
+        return builder.build();
+    }
+
     /**
      * Gets the application context and preserves the attribution tag.
      *
@@ -427,7 +456,6 @@
      * // Switch to video capture to shoot video.
      * controller.setEnabledUseCases(VIDEO_CAPTURE);
      * controller.startRecording(...);
-     * controller.stopRecording(...);
      *
      * // Switch back to image capture and image analysis before taking another picture.
      * controller.setEnabledUseCases(IMAGE_CAPTURE|IMAGE_ANALYSIS);
@@ -453,7 +481,7 @@
         }
         int oldEnabledUseCases = mEnabledUseCases;
         mEnabledUseCases = enabledUseCases;
-        if (!isVideoCaptureEnabled()) {
+        if (!isVideoCaptureEnabled() && isRecording()) {
             stopRecording();
         }
         startCameraAndTrackStates(() -> mEnabledUseCases = oldEnabledUseCases);
@@ -650,7 +678,7 @@
      * <p> By default, the saved image is mirrored to match the output of the preview if front
      * camera is used. To override this behavior, the app needs to explicitly set the flag to
      * {@code false} using {@link ImageCapture.Metadata#setReversedHorizontal} and
-     * {@link OutputFileOptions.Builder#setMetadata}.
+     * {@link ImageCapture.OutputFileOptions.Builder#setMetadata}.
      *
      * @param outputFileOptions  Options to store the newly captured image.
      * @param executor           The executor in which the callback methods will be run.
@@ -1120,112 +1148,308 @@
     }
 
     /**
-     * Takes a video and calls the OnVideoSavedCallback when done.
+     * Takes a video to a given file.
      *
-     * @param outputFileOptions Options to store the newly captured video.
-     * @param executor          The executor in which the callback methods will be run.
-     * @param callback          Callback which will receive success or failure.
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
      */
     @SuppressLint("MissingPermission")
     @ExperimentalVideo
     @MainThread
-    public void startRecording(@NonNull OutputFileOptions outputFileOptions,
-            @NonNull Executor executor, final @NonNull OnVideoSavedCallback callback) {
-        checkMainThread();
-        Preconditions.checkState(isCameraInitialized(), CAMERA_NOT_INITIALIZED);
-        Preconditions.checkState(isVideoCaptureEnabled(), VIDEO_CAPTURE_DISABLED);
-
-        mVideoCapture.startRecording(outputFileOptions.toVideoCaptureOutputFileOptions(), executor,
-                new androidx.camera.core.VideoCapture.OnVideoSavedCallback() {
-                    @Override
-                    public void onVideoSaved(
-                            @NonNull androidx.camera.core.VideoCapture.OutputFileResults
-                                    outputFileResults) {
-                        mVideoIsRecording.set(false);
-                        callback.onVideoSaved(
-                                OutputFileResults.create(outputFileResults.getSavedUri()));
-                    }
-
-                    @Override
-                    public void onError(int videoCaptureError, @NonNull String message,
-                            @Nullable Throwable cause) {
-                        mVideoIsRecording.set(false);
-                        callback.onError(videoCaptureError, message, cause);
-                    }
-                });
-        mVideoIsRecording.set(true);
+    @NonNull
+    public Recording startRecording(
+            @NonNull FileOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
     }
 
     /**
-     * Stops a in progress video recording.
+     * Takes a video to a given file descriptor.
+     *
+     * <p> Currently, file descriptors as output destinations are not supported on pre-Android O
+     * (API 26) devices.
+     *
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
      */
+    @SuppressLint("MissingPermission")
+    @ExperimentalVideo
+    @RequiresApi(26)
+    @MainThread
+    @NonNull
+    public Recording startRecording(
+            @NonNull FileDescriptorOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
+    }
+
+    /**
+     * Takes a video to MediaStore.
+     *
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+     */
+    @SuppressLint("MissingPermission")
     @ExperimentalVideo
     @MainThread
-    public void stopRecording() {
+    @NonNull
+    public Recording startRecording(
+            @NonNull MediaStoreOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
+    }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @ExperimentalVideo
+    @MainThread
+    private Recording startRecordingInternal(
+            @NonNull OutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
         checkMainThread();
-        if (mVideoIsRecording.get()) {
-            mVideoCapture.stopRecording();
+        Preconditions.checkState(isCameraInitialized(), CAMERA_NOT_INITIALIZED);
+        Preconditions.checkState(isVideoCaptureEnabled(), VIDEO_CAPTURE_DISABLED);
+        Preconditions.checkState(!isRecording(), VIDEO_RECORDING_UNFINISHED);
+
+        Consumer<VideoRecordEvent> wrappedListener =
+                wrapListenerToDeactivateRecordingOnFinalized(listener);
+        PendingRecording pendingRecording = prepareRecording(outputOptions);
+        boolean isAudioEnabled = audioConfig.getAudioEnabled();
+        if (isAudioEnabled) {
+            checkAudioPermissionGranted();
+            pendingRecording.withAudioEnabled();
+        }
+        Recording recording = pendingRecording.start(executor, wrappedListener);
+        setActiveRecording(recording, wrappedListener);
+
+        return recording;
+    }
+
+    private void checkAudioPermissionGranted() {
+        int permissionState = PermissionChecker.checkSelfPermission(mAppContext,
+                Manifest.permission.RECORD_AUDIO);
+        if (permissionState == PermissionChecker.PERMISSION_DENIED) {
+            throw new SecurityException("Attempted to start recording with audio, but "
+                    + "application does not have RECORD_AUDIO permission granted.");
         }
     }
 
     /**
-     * Returns whether there is a in progress video recording.
+     * Generates a {@link PendingRecording} instance for starting a recording.
+     *
+     * <p> This method handles {@code prepareRecording()} methods for different output formats,
+     * and makes {@link #startRecordingInternal(OutputOptions, AudioConfig, Executor, Consumer)}
+     * only handle the general flow.
+     *
+     * <p> This method uses the parent class {@link OutputOptions} as the parameter. On the other
+     * hand, the public {@code startRecording()} is overloaded with subclasses. The reason is to
+     * enforce compile-time check for API levels.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private PendingRecording prepareRecording(@NonNull OutputOptions options) {
+        Recorder recorder = mVideoCapture.getOutput();
+        if (options instanceof FileOutputOptions) {
+            return recorder.prepareRecording(mAppContext, (FileOutputOptions) options);
+        } else if (options instanceof FileDescriptorOutputOptions) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                throw new UnsupportedOperationException(
+                        "File descriptors are not supported on pre-Android O (API 26) devices."
+                );
+            }
+            return recorder.prepareRecording(mAppContext, (FileDescriptorOutputOptions) options);
+        } else if (options instanceof MediaStoreOutputOptions) {
+            return recorder.prepareRecording(mAppContext, (MediaStoreOutputOptions) options);
+        } else {
+            throw new IllegalArgumentException("Unsupported OutputOptions type.");
+        }
+    }
+
+    @ExperimentalVideo
+    private Consumer<VideoRecordEvent> wrapListenerToDeactivateRecordingOnFinalized(
+            @NonNull final Consumer<VideoRecordEvent> listener) {
+        final Executor mainExecutor = getMainExecutor(mAppContext);
+
+        return new Consumer<VideoRecordEvent>() {
+            @Override
+            public void accept(VideoRecordEvent videoRecordEvent) {
+                if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
+                    if (!Threads.isMainThread()) {
+                        // Post on main thread to ensure thread safety.
+                        mainExecutor.execute(() -> deactivateRecordingByListener(this));
+                    } else {
+                        deactivateRecordingByListener(this);
+                    }
+                }
+                listener.accept(videoRecordEvent);
+            }
+        };
+    }
+
+    @ExperimentalVideo
+    @MainThread
+    void deactivateRecordingByListener(@NonNull Consumer<VideoRecordEvent> listener) {
+        Recording recording = mRecordingMap.remove(listener);
+        if (recording != null) {
+            deactivateRecording(recording);
+        }
+    }
+
+    /**
+     * Clears the active video recording reference if the recording to be deactivated matches.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private void deactivateRecording(@NonNull Recording recording) {
+        if (mActiveRecording == recording) {
+            mActiveRecording = null;
+        }
+    }
+
+    @ExperimentalVideo
+    @MainThread
+    private void setActiveRecording(
+            @NonNull Recording recording,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        mRecordingMap.put(listener, recording);
+        mActiveRecording = recording;
+    }
+
+    /**
+     * Stops an in-progress video recording.
+     *
+     * <p> Once the current recording has been stopped, the next recording can be started.
+     *
+     * <p> If the recording completes successfully, a {@link VideoRecordEvent.Finalize} event with
+     * {@link VideoRecordEvent.Finalize#ERROR_NONE} will be sent to the provided listener.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private void stopRecording() {
+        checkMainThread();
+
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+            deactivateRecording(mActiveRecording);
+        }
+    }
+
+    /**
+     * Returns whether there is an in-progress video recording.
      */
     @ExperimentalVideo
     @MainThread
     public boolean isRecording() {
         checkMainThread();
-        return mVideoIsRecording.get();
+        return mActiveRecording != null && !mActiveRecording.isClosed();
     }
 
     /**
-     * Sets the intended video size for {@code VideoCapture}.
+     * Sets the intended video quality for {@code VideoCapture}.
      *
-     * <p> The value is used as a hint when determining the resolution and aspect ratio of
-     * the video. The actual output may differ from the requested value due to device constraints.
+     * <p> The value is used as a hint when determining the resolution of the video.
+     * The actual output may differ from the requested value due to device constraints.
+     * The {@link FallbackStrategy#lowerQualityOrHigherThan(Quality)} fallback strategy
+     * will be applied when the quality is not supported.
      *
-     * <p> When set to null, the output will be based on the default config of {@code VideoCapture}.
+     * <p> When set to null, the output will be based on the default config of {@link
+     * Recorder#DEFAULT_QUALITY_SELECTOR}.
      *
-     * <p> Changing the value will reconfigure the camera which will cause video capture to stop.
-     * To avoid this, set the value before controller is bound to lifecycle.
+     * <p> Changing the value will reconfigure the camera which will cause video
+     * capture to stop. To avoid this, set the value before controller is bound to
+     * lifecycle.
      *
-     * @param targetSize the intended video size for {@code VideoCapture}.
+     * @param targetQuality the intended video quality for {@code VideoCapture}.
      */
     @ExperimentalVideo
     @MainThread
-    public void setVideoCaptureTargetSize(@Nullable OutputSize targetSize) {
+    public void setVideoCaptureTargetQuality(@Nullable Quality targetQuality) {
         checkMainThread();
-        if (isOutputSizeEqual(mVideoCaptureOutputSize, targetSize)) {
+        if (targetQuality == mVideoCaptureQuality) {
             return;
         }
-        mVideoCaptureOutputSize = targetSize;
-        unbindVideoAndRecreate();
+        mVideoCaptureQuality = targetQuality;
         startCameraAndTrackStates();
     }
 
     /**
-     * Returns the intended output size for {@code VideoCapture} set by
-     * {@link #setVideoCaptureTargetSize(OutputSize)}, or null if not set.
+     * Returns the intended quality for {@code VideoCapture} set by
+     * {@link #setVideoCaptureTargetQuality(Quality)}, or null if not set.
      */
     @ExperimentalVideo
     @MainThread
     @Nullable
-    public OutputSize getVideoCaptureTargetSize() {
+    public Quality getVideoCaptureTargetQuality() {
         checkMainThread();
-        return mVideoCaptureOutputSize;
+        return mVideoCaptureQuality;
     }
 
-    /**
-     * Unbinds VideoCapture and recreate with the latest parameters.
-     */
-    private void unbindVideoAndRecreate() {
-        if (isCameraInitialized()) {
-            mCameraProvider.unbind(mVideoCapture);
-        }
-        androidx.camera.core.VideoCapture.Builder builder =
-                new androidx.camera.core.VideoCapture.Builder();
-        setTargetOutputSize(builder, mVideoCaptureOutputSize);
-        mVideoCapture = builder.build();
+    private VideoCapture<Recorder> createNewVideoCapture() {
+        return VideoCapture.withOutput(generateVideoCaptureRecorder(mVideoCaptureQuality));
     }
 
     // -----------------
@@ -1761,10 +1985,11 @@
             mCameraProvider.unbind(mImageAnalysis);
         }
 
+        // TODO: revert aosp/2280599 to reuse VideoCapture when VideoCapture supports reuse.
+        mCameraProvider.unbind(mVideoCapture);
         if (isVideoCaptureEnabled()) {
+            mVideoCapture = createNewVideoCapture();
             builder.addUseCase(mVideoCapture);
-        } else {
-            mCameraProvider.unbind(mVideoCapture);
         }
 
         builder.setViewPort(mViewPort);
@@ -1806,7 +2031,6 @@
      * @see #setImageAnalysisTargetSize(OutputSize)
      * @see #setPreviewTargetSize(OutputSize)
      * @see #setImageCaptureTargetSize(OutputSize)
-     * @see #setVideoCaptureTargetSize(OutputSize)
      */
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     public static final class OutputSize {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index 4049e5c..a51658b 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -109,6 +109,8 @@
     private int mTargetRotation;
     // Whether the preview is using front camera.
     private boolean mIsFrontCamera;
+    // Whether the Surface contains camera transform.
+    private boolean mHasCameraTransform;
 
     private PreviewView.ScaleType mScaleType = DEFAULT_SCALE_TYPE;
 
@@ -129,12 +131,21 @@
         mTargetRotation = transformationInfo.getTargetRotation();
         mResolution = resolution;
         mIsFrontCamera = isFrontCamera;
+        mHasCameraTransform = transformationInfo.hasCameraTransform();
     }
 
     /**
      * Override with display rotation when Preview does not have a target rotation set.
+     *
+     * TODO: move the PreviewView#updateDisplayRotationIfNeeded logic into PreviewTransformation
+     *  so all the transformation logic will be in one place.
      */
     void overrideWithDisplayRotation(int rotationDegrees, int displayRotation) {
+        if (!mHasCameraTransform) {
+            // When the Surface doesn't have the camera transform, we use mPreviewRotationDegrees
+            // from the core directly. There is no need to override the values.
+            return;
+        }
         mPreviewRotationDegrees = rotationDegrees;
         mTargetRotation = displayRotation;
     }
@@ -154,10 +165,35 @@
     Matrix getTextureViewCorrectionMatrix() {
         Preconditions.checkState(isTransformationInfoReady());
         RectF surfaceRect = new RectF(0, 0, mResolution.getWidth(), mResolution.getHeight());
-        int rotationDegrees = -surfaceRotationToDegrees(mTargetRotation);
+        int rotationDegrees = getRemainingRotationDegrees();
         return getRectToRect(surfaceRect, surfaceRect, rotationDegrees);
     }
 
+
+    /**
+     * Gets the remaining rotation degrees after the preview is transformed by Android Views.
+     *
+     * <p>Both {@link TextureView} or {@link SurfaceView} uses the camera transform encoded in
+     * the {@link Surface} to correct the output. The remaining rotation degrees depends on
+     * whether the camera transform is present.
+     */
+    private int getRemainingRotationDegrees() {
+        if (mTargetRotation == ROTATION_NOT_SPECIFIED && !mHasCameraTransform) {
+            // If the Surface is not connected to the camera, then the SurfaceView/TextureView will
+            // not apply any transformation. In that case, we need to apply the rotation
+            // calculated by CameraX.
+            return mPreviewRotationDegrees;
+        } else if (mHasCameraTransform && mTargetRotation != ROTATION_NOT_SPECIFIED) {
+            // If the Surface is connected to the camera, then the SurfaceView/TextureView
+            // will be the one to apply the camera orientation. In that case, only the Surface
+            // rotation needs to be applied by PreviewView.
+            return -surfaceRotationToDegrees(mTargetRotation);
+        } else {
+            throw new IllegalStateException("Target rotation must be specified. Target rotation: "
+                    + mTargetRotation + " hasCameraTransform " + mHasCameraTransform);
+        }
+    }
+
     /**
      * Calculates the transformation and applies it to the inner view of {@link PreviewView}.
      *
@@ -180,9 +216,12 @@
         } else {
             // Logs an error if non-display rotation is used with SurfaceView.
             Display display = preview.getDisplay();
-            if (display != null && display.getRotation() != mTargetRotation) {
-                Logger.e(TAG, "Non-display rotation not supported with SurfaceView / PERFORMANCE "
-                        + "mode.");
+            boolean mismatchedDisplayRotation = mHasCameraTransform && display != null
+                    && display.getRotation() != mTargetRotation;
+            boolean hasRemainingRotation =
+                    !mHasCameraTransform && getRemainingRotationDegrees() != 0;
+            if (mismatchedDisplayRotation || hasRemainingRotation) {
+                Logger.e(TAG, "Custom rotation not supported with SurfaceView/PERFORMANCE mode.");
             }
         }
 
@@ -441,7 +480,10 @@
     }
 
     private boolean isTransformationInfoReady() {
+        // Ignore target rotation if Surface doesn't have camera transform.
+        boolean isTargetRotationSpecified =
+                !mHasCameraTransform || (mTargetRotation != ROTATION_NOT_SPECIFIED);
         return mSurfaceCropRect != null && mResolution != null
-                && mTargetRotation != ROTATION_NOT_SPECIFIED;
+                && isTargetRotationSpecified;
     }
 }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
new file mode 100644
index 0000000..67977b7
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
@@ -0,0 +1,63 @@
+/*
+ * 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.view.video;
+
+import android.Manifest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+
+/**
+ * A class providing configuration for audio settings in the video recording.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@ExperimentalVideo
+public class AudioConfig {
+
+    /**
+     * The audio configuration with audio disabled.
+     */
+    @NonNull
+    public static final AudioConfig AUDIO_DISABLED = new AudioConfig(false);
+
+    private final boolean mIsAudioEnabled;
+
+    AudioConfig(boolean audioEnabled) {
+        mIsAudioEnabled = audioEnabled;
+    }
+
+    /**
+     * Creates a default {@link AudioConfig} with the given audio enabled state.
+     *
+     * <p> The {@link android.Manifest.permission#RECORD_AUDIO} permission is required to
+     * enable audio in video recording; for the use cases where audio is always disabled, please
+     * use {@link AudioConfig#AUDIO_DISABLED} instead, which has no permission requirements.
+     */
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @NonNull
+    public static AudioConfig create(boolean enableAudio) {
+        return new AudioConfig(enableAudio);
+    }
+
+    /**
+     * Get the audio enabled state.
+     */
+    public boolean getAudioEnabled() {
+        return mIsAudioEnabled;
+    }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java b/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java
deleted file mode 100644
index 121a020..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import android.location.Location;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import com.google.auto.value.AutoValue;
-
-/** Holder class for metadata that should be saved alongside captured video. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class Metadata {
-    /**
-     * Returns a {@link Location} object representing the geographic location where the video was
-     * taken.
-     *
-     * @return The location object or {@code null} if no location was set.
-     */
-    @Nullable
-    public abstract Location getLocation();
-
-    /** Creates a {@link Builder}. */
-    @NonNull
-    public static Builder builder() {
-        return new AutoValue_Metadata.Builder();
-    }
-
-    // Don't allow inheritance outside of package
-    Metadata() {
-    }
-
-    /** The builder for {@link Metadata}. */
-    @SuppressWarnings("StaticFinalBuilder")
-    @AutoValue.Builder
-    public abstract static class Builder {
-        /**
-         * Sets a {@link Location} object representing a geographic location where the video was
-         * taken.
-         *
-         * <p>If {@code null}, no location information will be saved with the video. Default
-         * value is {@code null}.
-         */
-        @NonNull
-        public abstract Builder setLocation(@Nullable Location location);
-
-        /** Build the {@link Metadata} from this builder. */
-        @NonNull
-        public abstract Metadata build();
-
-        Builder() {
-        }
-    }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java
deleted file mode 100644
index 5783d89..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** Listener containing callbacks for video file I/O events. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@SuppressWarnings("deprecation")
-@ExperimentalVideo
-public interface OnVideoSavedCallback {
-    /**
-     * An unknown error occurred.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     */
-    int ERROR_UNKNOWN = androidx.camera.core.VideoCapture.ERROR_UNKNOWN;
-    /**
-     * An error occurred with encoder state, either when trying to change state or when an
-     * unexpected state change occurred.
-     */
-    int ERROR_ENCODER = androidx.camera.core.VideoCapture.ERROR_ENCODER;
-    /** An error with muxer state such as during creation or when stopping. */
-    int ERROR_MUXER = androidx.camera.core.VideoCapture.ERROR_MUXER;
-    /**
-     * An error indicating start recording was called when video recording is still in progress.
-     */
-    int ERROR_RECORDING_IN_PROGRESS = androidx.camera.core.VideoCapture.ERROR_RECORDING_IN_PROGRESS;
-    /**
-     * An error indicating the file saving operations.
-     */
-    int ERROR_FILE_IO = androidx.camera.core.VideoCapture.ERROR_FILE_IO;
-    /**
-     * An error indicating this VideoCapture is not bound to a camera.
-     */
-    int ERROR_INVALID_CAMERA = androidx.camera.core.VideoCapture.ERROR_INVALID_CAMERA;
-
-    /**
-     * Describes the error that occurred during video capture operations.
-     *
-     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
-     * OnVideoSavedCallback#onError(int, String, Throwable)}.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     *
-     * @hide
-     */
-    @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
-            ERROR_FILE_IO, ERROR_INVALID_CAMERA})
-    @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @interface VideoCaptureError {
-    }
-
-    /** Called when the video has been successfully saved. */
-    void onVideoSaved(@NonNull OutputFileResults outputFileResults);
-
-    /** Called when an error occurs while attempting to save the video. */
-    void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
-            @Nullable Throwable cause);
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java
deleted file mode 100644
index 48360e0..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.os.Build;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.core.util.Preconditions;
-
-import com.google.auto.value.AutoValue;
-
-import java.io.File;
-
-/**
- * Options for saving newly captured video.
- *
- * <p> this class is used to configure save location and metadata. Save location can be
- * either a {@link File}, {@link MediaStore}. The metadata will be
- * stored with the saved video.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class OutputFileOptions {
-
-    // Empty metadata object used as a placeholder for no user-supplied metadata.
-    // Should be initialized to all default values.
-    private static final Metadata EMPTY_METADATA = Metadata.builder().build();
-
-    // Restrict constructor to same package
-    OutputFileOptions() {
-    }
-
-    /**
-     * Creates options to write captured video to a {@link File}.
-     *
-     * @param file save location of the video.
-     */
-    @NonNull
-    public static Builder builder(@NonNull File file) {
-        return new AutoValue_OutputFileOptions.Builder().setMetadata(EMPTY_METADATA).setFile(file);
-    }
-
-    /**
-     * Creates options to write captured video to a {@link ParcelFileDescriptor}.
-     *
-     * <p>Using a ParcelFileDescriptor to record a video is only supported for Android 8.0 or
-     * above.
-     *
-     * @param fileDescriptor to save the video.
-     * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
-     */
-    @NonNull
-    public static Builder builder(@NonNull ParcelFileDescriptor fileDescriptor) {
-        Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
-                "Using a ParcelFileDescriptor to record a video is only supported for Android 8"
-                        + ".0 or above.");
-
-        return new AutoValue_OutputFileOptions.Builder().setMetadata(
-                EMPTY_METADATA).setFileDescriptor(fileDescriptor);
-    }
-
-    /**
-     * Creates options to write captured video to {@link MediaStore}.
-     *
-     * Example:
-     *
-     * <pre>{@code
-     *
-     * ContentValues contentValues = new ContentValues();
-     * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
-     * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-     *
-     * OutputFileOptions options = OutputFileOptions.builder(
-     *         getContentResolver(),
-     *         MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-     *         contentValues).build();
-     *
-     * }</pre>
-     *
-     * @param contentResolver to access {@link MediaStore}
-     * @param saveCollection  The URI of the table to insert into.
-     * @param contentValues   to be included in the created video file.
-     */
-    @NonNull
-    public static Builder builder(@NonNull ContentResolver contentResolver,
-            @NonNull Uri saveCollection,
-            @NonNull ContentValues contentValues) {
-        return new AutoValue_OutputFileOptions.Builder()
-                .setMetadata(EMPTY_METADATA)
-                .setContentResolver(contentResolver)
-                .setSaveCollection(saveCollection).setContentValues(contentValues);
-    }
-
-    /**
-     * Returns the File object which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract File getFile();
-
-    /**
-     * Returns the ParcelFileDescriptor object which is set by the
-     * {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ParcelFileDescriptor getFileDescriptor();
-
-    /**
-     * Returns the content resolver which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ContentResolver getContentResolver();
-
-    /**
-     * Returns the URI which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract Uri getSaveCollection();
-
-    /**
-     * Returns the content values which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ContentValues getContentValues();
-
-    /** Returns the metadata which is set by the {@link OutputFileOptions.Builder}. */
-    @NonNull
-    public abstract Metadata getMetadata();
-
-    /**
-     * Checking the caller wants to save video to MediaStore.
-     */
-    private boolean isSavingToMediaStore() {
-        return getSaveCollection() != null && getContentResolver() != null
-                && getContentValues() != null;
-    }
-
-    /**
-     * Checking the caller wants to save video to a File.
-     */
-    private boolean isSavingToFile() {
-        return getFile() != null;
-    }
-
-    /**
-     * Checking the caller wants to save video to a ParcelFileDescriptor.
-     */
-    private boolean isSavingToFileDescriptor() {
-        return getFileDescriptor() != null;
-    }
-
-    /**
-     * Converts to a {@link androidx.camera.core.VideoCapture.OutputFileOptions}.
-     *
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    @SuppressWarnings("deprecation")
-    @NonNull
-    public androidx.camera.core.VideoCapture.OutputFileOptions toVideoCaptureOutputFileOptions() {
-        androidx.camera.core.VideoCapture.OutputFileOptions.Builder
-                internalOutputFileOptionsBuilder;
-        if (isSavingToFile()) {
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getFile()));
-        } else if (isSavingToFileDescriptor()) {
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getFileDescriptor()).getFileDescriptor());
-        } else {
-            Preconditions.checkState(isSavingToMediaStore());
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getContentResolver()),
-                            Preconditions.checkNotNull(getSaveCollection()),
-                            Preconditions.checkNotNull(getContentValues()));
-        }
-
-        androidx.camera.core.VideoCapture.Metadata internalMetadata =
-                new androidx.camera.core.VideoCapture.Metadata();
-        internalMetadata.location = getMetadata().getLocation();
-        internalOutputFileOptionsBuilder.setMetadata(internalMetadata);
-
-        return internalOutputFileOptionsBuilder.build();
-    }
-
-    /**
-     * Builder class for {@link OutputFileOptions}.
-     */
-    @AutoValue.Builder
-    @SuppressWarnings("StaticFinalBuilder")
-    public abstract static class Builder {
-
-        // Restrict construction to same package
-        Builder() {
-        }
-
-        abstract Builder setFile(@Nullable File file);
-
-        abstract Builder setFileDescriptor(@Nullable ParcelFileDescriptor fileDescriptor);
-
-        abstract Builder setContentResolver(@Nullable ContentResolver contentResolver);
-
-        abstract Builder setSaveCollection(@Nullable Uri uri);
-
-        abstract Builder setContentValues(@Nullable ContentValues contentValues);
-
-        /**
-         * Sets the metadata to be stored with the saved video.
-         *
-         * @param metadata Metadata to be stored with the saved video.
-         */
-        @NonNull
-        public abstract Builder setMetadata(@NonNull Metadata metadata);
-
-        /**
-         * Builds {@link OutputFileOptions}.
-         */
-        @NonNull
-        public abstract OutputFileOptions build();
-    }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java
deleted file mode 100644
index d9a64a0..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.provider.MediaStore;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import com.google.auto.value.AutoValue;
-
-/**
- * Info about the saved video file.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class OutputFileResults {
-
-    // Restrict constructor to package
-    OutputFileResults() {
-    }
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @NonNull
-    public static OutputFileResults create(@Nullable Uri savedUri) {
-        return new AutoValue_OutputFileResults(savedUri);
-    }
-
-    /**
-     * Returns the {@link Uri} of the saved video file.
-     *
-     * @return URI of saved video file if the {@link OutputFileOptions} is backed by
-     * {@link MediaStore} using
-     * {@link OutputFileOptions#builder(ContentResolver, Uri, ContentValues)}, {@code null}
-     * otherwise.
-     */
-    @Nullable
-    public abstract Uri getSavedUri();
-}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index d69c153..b85fa8a 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -36,6 +36,7 @@
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
 import androidx.camera.view.transform.OutputTransform
+import androidx.camera.video.Quality
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -64,6 +65,7 @@
         CameraController.OutputSize(AspectRatio.RATIO_16_9)
     private val targetSizeWithResolution =
         CameraController.OutputSize(Size(1080, 1960))
+    private val targetVideoQuality = Quality.HIGHEST
 
     @Before
     public fun setUp() {
@@ -287,22 +289,9 @@
 
     @UiThreadTest
     @Test
-    public fun setVideoCaptureResolution() {
-        controller.videoCaptureTargetSize = targetSizeWithResolution
-        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithResolution)
-
-        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
-        assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
-    }
-
-    @UiThreadTest
-    @Test
-    public fun setVideoCaptureAspectRatio() {
-        controller.videoCaptureTargetSize = targetSizeWithAspectRatio
-        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
-
-        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
-        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    fun setVideoCaptureQuality() {
+        controller.videoCaptureTargetQuality = targetVideoQuality
+        assertThat(controller.videoCaptureTargetQuality).isEqualTo(targetVideoQuality)
     }
 
     @UiThreadTest
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..bacd6d4 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
@@ -23,6 +23,7 @@
 import android.view.Surface
 import android.view.View
 import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.ImageOutputConfig.ROTATION_NOT_SPECIFIED
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue
 import androidx.camera.core.impl.utils.TransformUtils.sizeToVertices
 import androidx.test.core.app.ApplicationProvider
@@ -111,7 +112,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
         )
@@ -119,6 +123,36 @@
     }
 
     @Test
+    fun withoutCameraTransform_isScalingOnly() {
+        // Arrange: set up a stream that is already corrected, crop rect is full rect, no
+        // rotation and no camera transform.
+        val croppedSize = Size(40, 20)
+        mPreviewTransform.setTransformationInfo(
+            SurfaceRequest.TransformationInfo.of(
+                Rect(0, 0, croppedSize.width, croppedSize.height),
+                /*rotationDegrees*/0,
+                ROTATION_NOT_SPECIFIED,
+                /*hasCameraTransform=*/false
+            ),
+            croppedSize,
+            /*isFrontCamera=*/false
+        )
+
+        // Act.
+        mPreviewTransform.transformView(PREVIEW_VIEW_SIZE, LayoutDirection.LTR, mView)
+
+        // Assert: PreviewView simply scales the output.
+        assertThat(mView.scaleX).isWithin(FLOAT_ERROR)
+            .of(PREVIEW_VIEW_SIZE.width / croppedSize.width.toFloat())
+        assertThat(mView.scaleY).isWithin(FLOAT_ERROR)
+            .of(PREVIEW_VIEW_SIZE.height / croppedSize.height.toFloat())
+        assertThat(mView.translationX).isWithin(FLOAT_ERROR).of(0f)
+        assertThat(mView.translationY).isWithin(FLOAT_ERROR).of(0f)
+        // Assert: no correction needed because the stream is already correct.
+        assertThat(mPreviewTransform.textureViewCorrectionMatrix.isIdentity).isTrue()
+    }
+
+    @Test
     fun correctTextureViewWith0Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_0)).isEqualTo(
             intArrayOf(
@@ -195,7 +229,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 +262,8 @@
             SurfaceRequest.TransformationInfo.of(
                 CROP_RECT,
                 90,
-                ARBITRARY_ROTATION
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
             ),
             SURFACE_SIZE, BACK_CAMERA
         )
@@ -351,7 +391,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 +481,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/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index eb2773a..453701b 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -87,6 +87,8 @@
     // Testing resource dependency for manifest
     debugImplementation(project(":camera:camera-testing"))
     debugImplementation(libs.testCore)
+    // explicitly add runner here to force consistency with androidTestImplementation
+    debugImplementation(libs.testRunner)
 
     // Testing framework
     androidTestImplementation(libs.testCore)
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/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/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 0d374a3..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();
@@ -1739,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();
                 }
             }
@@ -1872,6 +1874,11 @@
         return mLastTakePictureErrorMessage;
     }
 
+    @VisibleForTesting
+    void cleanTakePictureErrorMessage() {
+        mLastTakePictureErrorMessage = null;
+    }
+
     @SuppressWarnings("unchecked")
     VideoCapture<Recorder> getVideoCapture() {
         return findUseCase(VideoCapture.class);
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index cab06b9..6eec96a 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,15 +32,16 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoRecordEvent
 import androidx.camera.view.CameraController
 import androidx.camera.view.CameraController.IMAGE_CAPTURE
 import androidx.camera.view.CameraController.VIDEO_CAPTURE
 import androidx.camera.view.LifecycleCameraController
 import androidx.camera.view.PreviewView
+import androidx.camera.view.video.AudioConfig
 import androidx.camera.view.video.ExperimentalVideo
-import androidx.camera.view.video.OnVideoSavedCallback
-import androidx.camera.view.video.OutputFileOptions
-import androidx.camera.view.video.OutputFileResults
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import java.text.SimpleDateFormat
@@ -62,10 +63,11 @@
 import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalVideo::class)
-@SuppressLint("NullAnnotationGroup")
+@SuppressLint("NullAnnotationGroup", "MissingPermission")
 class MainActivity : AppCompatActivity() {
 
     private lateinit var cameraController: LifecycleCameraController
+    private lateinit var activeRecording: Recording
     private lateinit var previewView: PreviewView
     private lateinit var overlayView: OverlayView
     private lateinit var executor: Executor
@@ -185,7 +187,7 @@
         videoCaptureBtn.setOnClickListener {
             // determine whether the onclick is to start recording or stop recording
             if (cameraController.isRecording) {
-                cameraController.stopRecording()
+                activeRecording.stop()
                 videoCaptureBtn.setText(R.string.start_video_capture)
                 val msg = "video stopped recording"
                 showToast(msg)
@@ -200,35 +202,29 @@
                         put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
                     }
                 }
-                val outputFileOptions = OutputFileOptions
-                    .builder(
-                        contentResolver,
-                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-                        contentValues
-                    )
+                val outputOptions = MediaStoreOutputOptions
+                    .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+                    .setContentValues(contentValues)
                     .build()
                 Log.d(TAG, "finished composing video name")
 
+                val audioConfig = AudioConfig.create(true)
+
                 // start recording
                 try {
-                    cameraController.startRecording(
-                        outputFileOptions,
-                        executor,
-                        object : OnVideoSavedCallback {
-                            override fun onVideoSaved(outputFileResults: OutputFileResults) {
-                                val msg = "Video record succeeded: " + outputFileResults.savedUri
+                    activeRecording = cameraController.startRecording(
+                        outputOptions, audioConfig, executor
+                    ) { event ->
+                        if (event is VideoRecordEvent.Finalize) {
+                            val uri = event.outputResults.outputUri
+                            if (event.error == VideoRecordEvent.Finalize.ERROR_NONE) {
+                                val msg = "Video record succeeded: $uri"
                                 showToast(msg)
-                            }
-
-                            override fun onError(
-                                videoCaptureError: Int,
-                                message: String,
-                                cause: Throwable?
-                            ) {
-                                Log.e(TAG, "Video saving failed: $message")
+                            } else {
+                                Log.e(TAG, "Video saving failed: ${event.cause}")
                             }
                         }
-                    )
+                    }
                     videoCaptureBtn.setText(R.string.stop_video_capture)
                     val msg = "video recording"
                     showToast(msg)
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 75139ec..794f5d8 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -63,6 +63,7 @@
     implementation(project(":camera:camera-mlkit-vision"))
     implementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
     implementation(project(":camera:camera-view"))
+    implementation(project(":camera:camera-video"))
     implementation(libs.guavaAndroid)
     implementation('com.google.mlkit:barcode-scanning:17.0.2')
     implementation("androidx.exifinterface:exifinterface:1.3.2")
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 0f90843..b3c9695 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -21,6 +21,7 @@
 import android.graphics.BitmapFactory
 import android.graphics.Matrix
 import android.graphics.PointF
+import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.os.Build
 import android.view.Surface
@@ -39,6 +40,7 @@
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.video.VideoRecordEvent
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_FAILED
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_FOCUSED
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_NOT_FOCUSED
@@ -443,6 +445,98 @@
         fragment.assertCanTakePicture()
     }
 
+    @Test
+    fun fragmentLaunched_cannotRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        val exception = Assert.assertThrows(IllegalStateException::class.java) {
+            fragment.assertCanRecordVideo()
+        }
+        assertThat(exception).hasMessageThat().isEqualTo("VideoCapture disabled.")
+    }
+
+    @Test
+    fun recordEnabled_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    @Test
+    fun cameraToggled_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        fragment.assertPreviewIsStreaming()
+        onView(withId(R.id.camera_toggle)).perform(click())
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    @Test
+    fun recordDisabledAndEnabledMultipleTimes_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        val times = 10
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        repeat(times) {
+            onView(withId(R.id.video_enabled)).perform(click())
+            onView(withId(R.id.video_enabled)).perform(click())
+        }
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    private fun invertAllUseCaseEnableStatusExceptPreview() {
+        onView(withId(R.id.capture_enabled)).perform(click())
+        onView(withId(R.id.analysis_enabled)).perform(click())
+        onView(withId(R.id.video_enabled)).perform(click())
+    }
+
+    private fun skipVideoRecordingTestOnCuttlefishApi29() {
+        // Skip test for b/168175357
+        Assume.assumeFalse(
+            "Cuttlefish has MediaCodec dequeInput/Output buffer fails issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+        )
+    }
+
+    private fun skipTestWithSurfaceProcessingOnCuttlefishApi30() {
+        // Skip test for b/253211491
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+        )
+    }
+
     /**
      * Calculates the 1st order moment (center of mass) of the R, G and B of the bitmap.
      */
@@ -565,6 +659,73 @@
         )
     }
 
+    /**
+     * Records a video and assert the URI exists.
+     *
+     * <p> Also cleans up the saved video afterwards.
+     */
+    private fun CameraControllerFragment.assertCanRecordVideo() {
+        // Arrange.
+        val videoSavedSemaphore = Semaphore(0)
+        val videoRecordingSemaphore = Semaphore(0)
+        var finalize: VideoRecordEvent.Finalize? = null
+
+        // Act.
+        instrumentation.runOnMainSync {
+            this.startRecording {
+                when (it) {
+                    is VideoRecordEvent.Finalize -> {
+                        finalize = it
+                        videoSavedSemaphore.release()
+                    }
+                    is VideoRecordEvent.Status -> {
+                        videoRecordingSemaphore.release()
+                    }
+                    is VideoRecordEvent.Start,
+                    is VideoRecordEvent.Pause,
+                    is VideoRecordEvent.Resume -> {
+                        // no op for this test, skip these event now.
+                    }
+                    else -> {
+                        throw IllegalStateException()
+                    }
+                }
+            }
+        }
+
+        // Wait for status event to proceed recording for a while.
+        assertThat(
+            videoRecordingSemaphore.tryAcquire(RECORDING_COUNT, TIMEOUT_SECONDS, TimeUnit.SECONDS)
+        ).isTrue()
+
+        instrumentation.runOnMainSync {
+            this.stopRecording()
+        }
+
+        // Wait for finalize event to saved file.
+        assertThat(videoSavedSemaphore.tryAcquire(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue()
+        assertThat(finalize).isNotEqualTo(null)
+        assertThat(finalize!!.hasError()).isFalse()
+
+        // Verify.
+        val uri = finalize!!.outputResults.outputUri
+        assertThat(uri).isNotEqualTo(Uri.EMPTY)
+        checkFileVideo(uri)
+
+        // Cleanup.
+        val contentResolver: ContentResolver = this.activity!!.contentResolver
+        contentResolver.delete(uri, null, null)
+    }
+
+    private fun checkFileVideo(uri: Uri) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(ApplicationProvider.getApplicationContext(), uri)
+            val hasVideo = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+            assertThat(hasVideo).isEqualTo("yes")
+        }
+    }
+
     private fun createFragmentScenario(): FragmentScenario<CameraControllerFragment> {
         return FragmentScenario.launchInContainer(
             CameraControllerFragment::class.java, null, R.style.AppTheme,
@@ -639,6 +800,7 @@
         val testCameraRule = CameraUtil.PreTestCamera()
 
         const val TIMEOUT_SECONDS = 10L
+        const val RECORDING_COUNT = 5
 
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index b7c539c..3881ff3 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -18,10 +18,12 @@
 
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
 
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Dialog;
 import android.content.ContentResolver;
@@ -33,6 +35,7 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.provider.MediaStore;
@@ -51,9 +54,11 @@
 import android.widget.Toast;
 import android.widget.ToggleButton;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
+import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.CameraSelector;
@@ -65,14 +70,16 @@
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoRecordEvent;
 import androidx.camera.view.CameraController;
 import androidx.camera.view.LifecycleCameraController;
 import androidx.camera.view.PreviewView;
 import androidx.camera.view.RotationProvider;
+import androidx.camera.view.video.AudioConfig;
 import androidx.camera.view.video.ExperimentalVideo;
-import androidx.camera.view.video.OnVideoSavedCallback;
-import androidx.camera.view.video.OutputFileOptions;
-import androidx.camera.view.video.OutputFileResults;
+import androidx.core.util.Consumer;
 import androidx.fragment.app.Fragment;
 import androidx.lifecycle.LiveData;
 
@@ -121,6 +128,21 @@
     private RotationProvider mRotationProvider;
     private int mRotation;
     private final RotationProvider.Listener mRotationListener = rotation -> mRotation = rotation;
+    @Nullable private Recording mActiveRecording = null;
+    private final Consumer<VideoRecordEvent> mVideoRecordEventListener = videoRecordEvent -> {
+        if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
+            VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) videoRecordEvent;
+            Uri uri = finalize.getOutputResults().getOutputUri();
+
+            if (finalize.getError() == ERROR_NONE) {
+                toast("Video saved to: " + uri);
+            } else {
+                String msg = "Saved uri " + uri;
+                msg += " with code (" + finalize.getError() + ")";
+                toast("Failed to save video: " + msg);
+            }
+        }
+    };
 
     // Wrapped analyzer for tests to receive callbacks.
     @Nullable
@@ -148,6 +170,21 @@
     };
 
     @NonNull
+    private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
+        String videoFileName = "video_" + System.currentTimeMillis();
+        ContentResolver resolver = requireContext().getContentResolver();
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
+        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
+        return new MediaStoreOutputOptions
+                .Builder(resolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+                .setContentValues(contentValues)
+                .build();
+    }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @NonNull
     @Override
     @OptIn(markerClass = ExperimentalVideo.class)
     public View onCreateView(
@@ -259,30 +296,7 @@
 
         view.findViewById(R.id.video_record).setOnClickListener(v -> {
             try {
-                String videoFileName = "video_" + System.currentTimeMillis();
-                ContentResolver resolver = requireContext().getContentResolver();
-                ContentValues contentValues = new ContentValues();
-                contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-                contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
-                contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
-                OutputFileOptions outputFileOptions = OutputFileOptions.builder(resolver,
-                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues).build();
-                mCameraController.startRecording(outputFileOptions, mExecutorService,
-                        new OnVideoSavedCallback() {
-                            @Override
-                            public void onVideoSaved(
-                                    @NonNull OutputFileResults outputFileResults) {
-                                toast("Video saved to: "
-                                        + outputFileResults.getSavedUri());
-                            }
-
-                            @Override
-                            public void onError(int videoCaptureError,
-                                    @NonNull String message,
-                                    @Nullable Throwable cause) {
-                                toast("Failed to save video: " + message);
-                            }
-                        });
+                startRecording(mVideoRecordEventListener);
             } catch (RuntimeException exception) {
                 toast("Failed to record video: " + exception.getMessage());
             }
@@ -290,7 +304,7 @@
         });
         view.findViewById(R.id.video_stop_recording).setOnClickListener(
                 v -> {
-                    mCameraController.stopRecording();
+                    stopRecording();
                     updateUiText();
                 });
 
@@ -636,4 +650,25 @@
                         contentValues).build();
         mCameraController.takePicture(outputFileOptions, mExecutorService, callback);
     }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @VisibleForTesting
+    @MainThread
+    @OptIn(markerClass = ExperimentalVideo.class)
+    void startRecording(Consumer<VideoRecordEvent> listener) {
+        MediaStoreOutputOptions outputOptions = getNewVideoOutputMediaStoreOptions();
+        AudioConfig audioConfig = AudioConfig.create(true);
+        mActiveRecording = mCameraController.startRecording(outputOptions, audioConfig,
+                mExecutorService, listener);
+    }
+
+    @VisibleForTesting
+    @MainThread
+    @OptIn(markerClass = ExperimentalVideo.class)
+    void stopRecording() {
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+        }
+    }
+
 }
diff --git a/collection/collection/api/current.ignore b/collection/collection/api/current.ignore
index decf837..6397290 100644
--- a/collection/collection/api/current.ignore
+++ b/collection/collection/api/current.ignore
@@ -9,28 +9,12 @@
     Method androidx.collection.SparseArrayCompat.clone has changed return type from androidx.collection.SparseArrayCompat<E!> to androidx.collection.SparseArrayCompat<E>
 
 
-RemovedInterface: androidx.collection.ArraySet:
-    Class androidx.collection.ArraySet no longer implements java.util.Collection<E>
-
-
 RemovedMethod: androidx.collection.ArraySet#ArraySet(androidx.collection.ArraySet<E>):
     Removed constructor androidx.collection.ArraySet(androidx.collection.ArraySet<E>)
 RemovedMethod: androidx.collection.ArraySet#ArraySet(java.util.Collection<E>):
     Removed constructor androidx.collection.ArraySet(java.util.Collection<E>)
-RemovedMethod: androidx.collection.ArraySet#add(E):
-    Removed method androidx.collection.ArraySet.add(E)
-RemovedMethod: androidx.collection.ArraySet#addAll(java.util.Collection<? extends E>):
-    Removed method androidx.collection.ArraySet.addAll(java.util.Collection<? extends E>)
-RemovedMethod: androidx.collection.ArraySet#clear():
-    Removed method androidx.collection.ArraySet.clear()
-RemovedMethod: androidx.collection.ArraySet#isEmpty():
-    Removed method androidx.collection.ArraySet.isEmpty()
 RemovedMethod: androidx.collection.ArraySet#size():
     Removed method androidx.collection.ArraySet.size()
-RemovedMethod: androidx.collection.ArraySet#toArray():
-    Removed method androidx.collection.ArraySet.toArray()
-RemovedMethod: androidx.collection.ArraySet#toArray(T[]):
-    Removed method androidx.collection.ArraySet.toArray(T[])
 RemovedMethod: androidx.collection.LruCache#toString():
     Removed method androidx.collection.LruCache.toString()
 RemovedMethod: androidx.collection.SimpleArrayMap#SimpleArrayMap(androidx.collection.SimpleArrayMap<K,V>):
diff --git a/collection/collection/api/restricted_current.ignore b/collection/collection/api/restricted_current.ignore
index decf837..6397290 100644
--- a/collection/collection/api/restricted_current.ignore
+++ b/collection/collection/api/restricted_current.ignore
@@ -9,28 +9,12 @@
     Method androidx.collection.SparseArrayCompat.clone has changed return type from androidx.collection.SparseArrayCompat<E!> to androidx.collection.SparseArrayCompat<E>
 
 
-RemovedInterface: androidx.collection.ArraySet:
-    Class androidx.collection.ArraySet no longer implements java.util.Collection<E>
-
-
 RemovedMethod: androidx.collection.ArraySet#ArraySet(androidx.collection.ArraySet<E>):
     Removed constructor androidx.collection.ArraySet(androidx.collection.ArraySet<E>)
 RemovedMethod: androidx.collection.ArraySet#ArraySet(java.util.Collection<E>):
     Removed constructor androidx.collection.ArraySet(java.util.Collection<E>)
-RemovedMethod: androidx.collection.ArraySet#add(E):
-    Removed method androidx.collection.ArraySet.add(E)
-RemovedMethod: androidx.collection.ArraySet#addAll(java.util.Collection<? extends E>):
-    Removed method androidx.collection.ArraySet.addAll(java.util.Collection<? extends E>)
-RemovedMethod: androidx.collection.ArraySet#clear():
-    Removed method androidx.collection.ArraySet.clear()
-RemovedMethod: androidx.collection.ArraySet#isEmpty():
-    Removed method androidx.collection.ArraySet.isEmpty()
 RemovedMethod: androidx.collection.ArraySet#size():
     Removed method androidx.collection.ArraySet.size()
-RemovedMethod: androidx.collection.ArraySet#toArray():
-    Removed method androidx.collection.ArraySet.toArray()
-RemovedMethod: androidx.collection.ArraySet#toArray(T[]):
-    Removed method androidx.collection.ArraySet.toArray(T[])
 RemovedMethod: androidx.collection.LruCache#toString():
     Removed method androidx.collection.LruCache.toString()
 RemovedMethod: androidx.collection.SimpleArrayMap#SimpleArrayMap(androidx.collection.SimpleArrayMap<K,V>):
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..096d0f6 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
@@ -96,8 +96,10 @@
             8601 to "1.3.0-rc02",
             8602 to "1.3.0",
             8603 to "1.3.1",
+            8604 to "1.3.2",
             9000 to "1.4.0-alpha01",
             9001 to "1.4.0-alpha02",
+            9100 to "1.4.0-alpha03",
         )
 
         /**
@@ -110,7 +112,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/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
new file mode 100644
index 0000000..c8a5fd5
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.foundation.text
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.properties.Delegates
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@MediumTest
+@RunWith(Parameterized::class)
+class BasicTextMinLinesTest(private val useAnnotatedString: Boolean) {
+    @get:Rule
+    val rule = createComposeRule()
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "useAnnotatedString={0}")
+        fun parameters() = arrayOf(true, false)
+    }
+
+    private val density = Density(1f)
+    private val fontSize = 20
+
+    @Test
+    fun defaultMinLines_withEmptyText() {
+        displayText("", 1) { height ->
+            assertThat(height).isEqualTo(fontSize)
+        }
+    }
+
+    @Test
+    fun minLines_greater_thanEmptyText() {
+        displayText("", 5) { height ->
+            assertThat(height).isEqualTo(fontSize * 5)
+        }
+    }
+
+    @Test
+    fun minLines_smaller_thanTextLines() {
+        displayText("Line1\nLine2", 1) { height ->
+            assertThat(height).isEqualTo(fontSize * 2)
+        }
+    }
+
+    @Test
+    fun minLines_greater_thanTextLines() {
+        displayText("Line1\nLine2", 5) { height ->
+            assertThat(height).isEqualTo(fontSize * 5)
+        }
+    }
+
+    private fun displayText(text: String, minLines: Int, verify: (Int) -> Unit) {
+        var height by Delegates.notNull<Int>()
+        val modifier = Modifier.fillMaxWidth().onSizeChanged { height = it.height }
+        val style = TextStyle(
+            fontSize = fontSize.sp,
+            fontFamily = TEST_FONT_FAMILY,
+            lineHeight = fontSize.sp
+        )
+
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides density) {
+                if (useAnnotatedString) {
+                    BasicText(
+                        text = AnnotatedString(text),
+                        modifier = modifier,
+                        style = style,
+                        minLines = minLines
+                    )
+                } else {
+                    BasicText(
+                        text = text,
+                        modifier = modifier,
+                        style = style,
+                        minLines = minLines
+                    )
+                }
+            }
+        }
+
+        rule.runOnIdle { verify(height) }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
index c278e7b..270cde8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
@@ -18,15 +18,15 @@
 
 import android.content.Context
 import android.graphics.Typeface
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.text.CoreTextField
 import androidx.compose.foundation.text.TEST_FONT
+import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.heightInLines
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFontFamilyResolver
@@ -50,8 +50,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
+import kotlin.properties.Delegates
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -64,6 +63,8 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class HeightInLinesModifierTest {
+    private val fontSize = 20
+    private val defaultTextStyle = TextStyle(fontSize = 20.sp, fontFamily = TEST_FONT_FAMILY)
 
     private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
         "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
@@ -88,66 +89,23 @@
 
     @Test
     fun minLines_shortInputText() {
-        var subjectLayout: TextLayoutResult? = null
-        var subjectHeight: Int? = null
-        var twoLineHeight: Int? = null
-        val positionedLatch = CountDownLatch(1)
-        val twoLinePositionedLatch = CountDownLatch(1)
-
-        rule.setContent {
-            HeightObservingText(
-                onGlobalHeightPositioned = {
-                    subjectHeight = it
-                    positionedLatch.countDown()
-                },
-                onTextLayoutResult = {
-                    subjectLayout = it
-                },
-                textFieldValue = TextFieldValue("abc"),
-                minLines = 2
-            )
-            HeightObservingText(
-                onGlobalHeightPositioned = {
-                    twoLineHeight = it
-                    twoLinePositionedLatch.countDown()
-                },
-                onTextLayoutResult = {},
-                textFieldValue = TextFieldValue("1\n2"),
-                minLines = 2
-            )
-        }
-        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
-
-        rule.runOnIdle {
-            assertThat(subjectLayout).isNotNull()
-            assertThat(subjectLayout!!.lineCount).isEqualTo(1)
-            assertThat(subjectHeight!!).isEqualTo(twoLineHeight)
+        setTextFieldWithMinMaxLines(
+            TextFieldValue("abc"),
+            minLines = 5
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(1)
+            assertThat(height).isGreaterThan(fontSize * 5)
         }
     }
 
     @Test
     fun maxLines_shortInputText() {
-        val (textLayoutResult, height) = setTextFieldWithMaxLines(
+        setTextFieldWithMinMaxLines(
             TextFieldValue("abc"),
             maxLines = 5
-        )
-
-        rule.runOnIdle {
-            assertThat(textLayoutResult).isNotNull()
-            assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
-            assertThat(textLayoutResult.size.height).isEqualTo(height)
-        }
-    }
-
-    @Test
-    fun maxLines_notApplied_infiniteMaxLines() {
-        val (textLayoutResult, height) =
-            setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
-
-        rule.runOnIdle {
-            assertThat(textLayoutResult).isNotNull()
-            assertThat(textLayoutResult!!.size.height).isEqualTo(height)
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(1)
+            assertThat(height).isGreaterThan(fontSize)
         }
     }
 
@@ -190,16 +148,12 @@
 
     @Test
     fun minLines_longInputText() {
-        val (textLayoutResult, height) = setTextFieldWithMaxLines(
+       setTextFieldWithMinMaxLines(
             TextFieldValue(longText),
             minLines = 2
-        )
-
-        rule.runOnIdle {
-            assertThat(textLayoutResult).isNotNull()
-            // should be in the 20s, but use this to create invariant for the next assertion
-            assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
-            assertThat(textLayoutResult.size.height).isEqualTo(height)
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isGreaterThan(2)
+            assertThat(height).isGreaterThan(fontSize * 2)
         }
     }
 
@@ -208,33 +162,21 @@
         var subjectLayout: TextLayoutResult? = null
         var subjectHeight: Int? = null
         var twoLineHeight: Int? = null
-        val positionedLatch = CountDownLatch(1)
-        val twoLinePositionedLatch = CountDownLatch(1)
 
         rule.setContent {
             HeightObservingText(
-                onGlobalHeightPositioned = {
-                    subjectHeight = it
-                    positionedLatch.countDown()
-                },
-                onTextLayoutResult = {
-                    subjectLayout = it
-                },
+                onHeightChanged = { subjectHeight = it },
+                onTextLayoutResult = { subjectLayout = it },
                 textFieldValue = TextFieldValue(longText),
                 maxLines = 2
             )
             HeightObservingText(
-                onGlobalHeightPositioned = {
-                    twoLineHeight = it
-                    twoLinePositionedLatch.countDown()
-                },
+                onHeightChanged = { twoLineHeight = it },
                 onTextLayoutResult = {},
                 textFieldValue = TextFieldValue("1\n2"),
                 maxLines = 2
             )
         }
-        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
 
         rule.runOnIdle {
             assertThat(subjectLayout).isNotNull()
@@ -275,13 +217,12 @@
                 LocalDensity provides Density(1.0f, 1f)
             ) {
                 HeightObservingText(
-                    onGlobalHeightPositioned = {
+                    onHeightChanged = {
                         heights.add(it)
                     },
-                    onTextLayoutResult = {},
                     textFieldValue = TextFieldValue(longText),
                     maxLines = 10,
-                    textStyle = TextStyle.Default.copy(
+                    textStyle = defaultTextStyle.copy(
                         fontFamily = fontFamily,
                         fontSize = 80.sp
                     )
@@ -313,61 +254,55 @@
         )
     }
 
-    private fun setTextFieldWithMaxLines(
+    private fun setTextFieldWithMinMaxLines(
         textFieldValue: TextFieldValue,
         minLines: Int = 1,
-        maxLines: Int = Int.MAX_VALUE
-    ): Pair<TextLayoutResult?, Int?> {
-        var textLayoutResult: TextLayoutResult? = null
-        var height: Int? = null
-        val positionedLatch = CountDownLatch(1)
-
+        maxLines: Int = Int.MAX_VALUE,
+        verify: (Int, TextLayoutResult) -> Unit
+    ) {
+        var height by Delegates.notNull<Int>()
+        lateinit var textLayoutResult: TextLayoutResult
         rule.setContent {
             HeightObservingText(
-                onGlobalHeightPositioned = {
-                    height = it
-                    positionedLatch.countDown()
-                },
-                onTextLayoutResult = {
-                    textLayoutResult = it
-                },
+                onHeightChanged = { height = it },
                 textFieldValue = textFieldValue,
+                onTextLayoutResult = { textLayoutResult = it },
                 minLines = minLines,
                 maxLines = maxLines
             )
         }
-        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
 
-        return Pair(textLayoutResult, height)
+        rule.runOnIdle {
+            verify(height, textLayoutResult)
+        }
     }
 
     @Composable
     private fun HeightObservingText(
-        onGlobalHeightPositioned: (Int) -> Unit,
-        onTextLayoutResult: (TextLayoutResult) -> Unit,
+        onHeightChanged: (Int) -> Unit,
         textFieldValue: TextFieldValue,
+        onTextLayoutResult: (TextLayoutResult) -> Unit = {},
         minLines: Int = 1,
         maxLines: Int = Int.MAX_VALUE,
-        textStyle: TextStyle = TextStyle.Default
+        textStyle: TextStyle = defaultTextStyle
     ) {
-        Box(
-            Modifier.onGloballyPositioned {
-                onGlobalHeightPositioned(it.size.height)
-            }
-        ) {
-            CoreTextField(
-                value = textFieldValue,
-                onValueChange = {},
-                textStyle = textStyle,
-                modifier = Modifier
-                    .requiredWidth(100.dp)
-                    .heightInLines(
-                        textStyle = textStyle,
-                        minLines = minLines,
-                        maxLines = maxLines
-                    ),
-                onTextLayout = onTextLayoutResult
-            )
-        }
+        CoreTextField(
+            value = textFieldValue,
+            onValueChange = {},
+            textStyle = textStyle,
+            onTextLayout = onTextLayoutResult,
+            modifier = Modifier
+                .onSizeChanged {
+                    onHeightChanged(it.height)
+                }
+                .requiredWidth(100.dp)
+                // we test modifier here so propagating min and max lines here instead of passing
+                // them to the CoreTextField directly
+                .heightInLines(
+                    textStyle = textStyle,
+                    minLines = minLines,
+                    maxLines = maxLines
+                )
+        )
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
new file mode 100644
index 0000000..9fc479c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.foundation.textfield
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.TEST_FONT_FAMILY
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.sp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.properties.Delegates
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class TextFieldMinMaxLinesTest {
+    private val fontSize = 20
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun minLines_smaller_thanInput() {
+        displayTextField(
+            text = "abc\nabc\nabc",
+            minLines = 1
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(3)
+            assertThat(height).isEqualTo(fontSize * 3)
+        }
+    }
+
+    @Test
+    fun minLines_greater_thanInput() {
+        displayTextField(
+            text = "abc",
+            minLines = 3
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(1)
+            assertThat(height).isEqualTo(fontSize * 3)
+        }
+    }
+
+    @Test
+    fun maxLines_smaller_thanInput() {
+        displayTextField(
+            text = "abc\nabc\nabc",
+            maxLines = 1
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(3)
+            assertThat(height).isEqualTo(fontSize)
+        }
+    }
+
+    @Test
+    fun maxLines_greater_thanInput() {
+        displayTextField(
+            text = "abc",
+            maxLines = 3
+        ) { height, textLayoutResult ->
+            assertThat(textLayoutResult.lineCount).isEqualTo(1)
+            assertThat(height).isEqualTo(fontSize)
+        }
+    }
+
+    private fun displayTextField(
+        text: String,
+        minLines: Int = 1,
+        maxLines: Int = Int.MAX_VALUE,
+        verify: (Int, TextLayoutResult) -> Unit
+    ) {
+        var height by Delegates.notNull<Int>()
+        lateinit var textLayoutResult: TextLayoutResult
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                CoreTextField(
+                    value = TextFieldValue(text),
+                    onValueChange = {},
+                    onTextLayout = { textLayoutResult = it },
+                    modifier = Modifier
+                        .onSizeChanged { height = it.height }
+                        .fillMaxWidth(),
+                    minLines = minLines,
+                    maxLines = maxLines,
+                    textStyle = TextStyle(
+                        fontSize = fontSize.sp,
+                        fontFamily = TEST_FONT_FAMILY,
+                        lineHeight = fontSize.sp
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            verify(height, textLayoutResult)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index ea0ff4f..6169203 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -18,6 +18,9 @@
 
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.calculateTargetValue
 import androidx.compose.animation.core.spring
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.rememberSplineBasedDecay
@@ -35,6 +38,7 @@
 import androidx.compose.foundation.lazy.LazyList
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -338,6 +342,11 @@
     /**
      * @param state The [PagerState] that controls the which to which this FlingBehavior will
      * be applied to.
+     * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
+     * The default behavior will result in any fling going to the next page in the direction of the
+     * fling (if the fling has enough velocity, otherwise we'll bounce back). Use
+     * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
+     * fling after scrolling is finished and fling has started.
      * @param lowVelocityAnimationSpec The animation spec used to approach the target offset. When
      * the fling velocity is not large enough. Large enough means large enough to naturally decay.
      * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
@@ -352,18 +361,140 @@
     @Composable
     fun flingBehavior(
         state: PagerState,
-        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(),
+        pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
+        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
         highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
-        snapAnimationSpec: AnimationSpec<Float> = spring(),
+        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
     ): FlingBehavior {
         val density = LocalDensity.current
-        val snapLayoutInfoProvider = SnapLayoutInfoProvider(state.lazyListState)
-        return SnapFlingBehavior(
-            snapLayoutInfoProvider,
+
+        return remember(
             lowVelocityAnimationSpec,
             highVelocityAnimationSpec,
             snapAnimationSpec,
+            pagerSnapDistance,
             density
-        )
+        ) {
+            val snapLayoutInfoProvider =
+                SnapLayoutInfoProvider(state, pagerSnapDistance, highVelocityAnimationSpec)
+            SnapFlingBehavior(
+                snapLayoutInfoProvider = snapLayoutInfoProvider,
+                lowVelocityAnimationSpec = lowVelocityAnimationSpec,
+                highVelocityAnimationSpec = highVelocityAnimationSpec,
+                snapAnimationSpec = snapAnimationSpec,
+                density = density
+            )
+        }
+    }
+}
+
+/**
+ * [PagerSnapDistance] defines the way the [Pager] will treat the distance between the current
+ * page and the page where it will settle.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PagerSnapDistance {
+
+    /** Provides a chance to change where the [Pager] fling will settle.
+     *
+     * @param startPage The current page right before the fling starts.
+     * @param suggestedTargetPage The proposed target page where this fling will stop. This target
+     * will be the page that will be correctly positioned (snapped) after naturally decaying with
+     * [velocity] using a [DecayAnimationSpec].
+     * @param velocity The initial fling velocity.
+     * @param pageSize The page size for this [Pager].
+     * @param pageSpacing The spacing used between pages.
+     *
+     * @return An updated target page where to settle. Note that this value needs to be between 0
+     * and the total count of pages in this pager. If an invalid value is passed, the pager will
+     * coerce within the valid values.
+     */
+    fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int
+    ): Int
+
+    companion object {
+        /**
+         * Limits the maximum number of pages that can be flung per fling gesture.
+         * @param pages The maximum number of extra pages that can be flung at once.
+         */
+        fun atMost(pages: Int): PagerSnapDistance {
+            require(pages >= 0) {
+                "pages should be greater than or equal to 0. You have used $pages."
+            }
+            return PagerSnapDistanceMaxPages(pages)
+        }
+    }
+}
+
+/**
+ * Limits the maximum number of pages that can be flung per fling gesture.
+ * @param pagesLimit The maximum number of extra pages that can be flung at once.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerSnapDistanceMaxPages(private val pagesLimit: Int) : PagerSnapDistance {
+    override fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int,
+    ): Int {
+        return suggestedTargetPage.coerceIn(startPage - pagesLimit, startPage + pagesLimit)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return if (other is PagerSnapDistanceMaxPages) {
+            this.pagesLimit == other.pagesLimit
+        } else {
+            false
+        }
+    }
+
+    override fun hashCode(): Int {
+        return pagesLimit.hashCode()
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun SnapLayoutInfoProvider(
+    pagerState: PagerState,
+    pagerSnapDistance: PagerSnapDistance,
+    decayAnimationSpec: DecayAnimationSpec<Float>
+): SnapLayoutInfoProvider {
+    return object : SnapLayoutInfoProvider by SnapLayoutInfoProvider(
+        lazyListState = pagerState.lazyListState,
+        positionInLayout = SnapAlignmentStartToStart
+    ) {
+        override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+            val effectivePageSize = pagerState.pageSize + pagerState.pageSpacing
+            val initialOffset = pagerState.currentPageOffset * effectivePageSize
+            val animationOffset =
+                decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+
+            val startPage = pagerState.currentPage
+            val startPageOffset = startPage * effectivePageSize
+
+            val targetOffset =
+                (startPageOffset + initialOffset + animationOffset) / effectivePageSize
+
+            val targetPage = targetOffset.toInt()
+            val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
+                startPage,
+                targetPage,
+                initialVelocity,
+                pagerState.pageSize,
+                pagerState.pageSpacing
+            ).coerceIn(0, pagerState.pageCount)
+
+            val finalOffset = (correctedTargetPage - startPage) * effectivePageSize
+
+            return (finalOffset - initialOffset)
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 2b42d2e..919b042 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -78,7 +78,7 @@
     private val pageAvailableSpace: Int
         get() = pageSize + pageSpacing
 
-    private val pageCount: Int
+    internal val pageCount: Int
         get() = lazyListState.layoutInfo.totalItemsCount
 
     private val closestPageToSnappedPosition: LazyListItemInfo?
@@ -126,9 +126,9 @@
 
     /**
      * Indicates how far the current page is to the snapped position, this will vary from
-     * [MinPageOffset] (page is offset towards the start of the layout) to [MaxPageOffset]
-     * (page is offset towards the end of the layout). This is 0.0 if the [currentPage] is in the
-     * snapped position. The value will flip once the current page changes.
+     * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
+     * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
+     * flip once the current page changes.
      */
     val currentPageOffset: Float by derivedStateOf {
         val currentPagePositionOffset = closestPageToSnappedPosition?.offset ?: 0
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index d499986..b8ded54 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -220,6 +220,7 @@
                     softWrap = softWrap,
                     fontFamilyResolver = fontFamilyResolver,
                     overflow = overflow,
+                    minLines = minLines,
                     maxLines = maxLines,
                     placeholders = placeholders
                 ),
@@ -238,6 +239,7 @@
                 softWrap = softWrap,
                 fontFamilyResolver = fontFamilyResolver,
                 overflow = overflow,
+                minLines = minLines,
                 maxLines = maxLines,
                 placeholders = placeholders,
             )
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index fb2a146..7a79167 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -36,16 +36,16 @@
          * 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")
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
index 463830d..4ace0ae 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
@@ -59,6 +59,7 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -96,6 +97,7 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -125,6 +127,7 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -166,6 +169,7 @@
             ),
             Stubs.Composable
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -231,6 +235,7 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index f91c9a1..3dbaa26 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 9001
+    const val version: Int = 9100
 }
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-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
index f3aba8d..0c5b55e 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
@@ -20,6 +20,7 @@
 
 import androidx.compose.lint.test.Stubs
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -62,6 +63,7 @@
             Stubs.Composable,
             Stubs.Modifier
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -103,6 +105,7 @@
             Stubs.Composable,
             Stubs.Modifier
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -146,6 +149,7 @@
             Stubs.Composable,
             Stubs.Modifier
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -187,6 +191,7 @@
             Stubs.Composable,
             Stubs.Modifier
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -222,6 +227,7 @@
             Stubs.Composable,
             Stubs.Modifier
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
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/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/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index de8dd50..a7d7eba 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.GenericShape
 import androidx.compose.runtime.Composable
@@ -91,15 +92,15 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import kotlin.math.ceil
+import kotlin.math.roundToInt
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.math.roundToInt
-import org.junit.Assert.assertNotNull
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -1349,4 +1350,37 @@
         assertEquals(sizePx, drawScopeWidth)
         assertEquals(sizePx, drawScopeHeight)
     }
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun removingGraphicsLayerInvalidatesParentLayer() {
+        var toggle by mutableStateOf(true)
+        val size = 100
+        rule.setContent {
+            val sizeDp = with(LocalDensity.current) { size.toDp() }
+            LazyColumn(Modifier.testTag("lazy").background(Color.Blue)) {
+                items(4) {
+                    Box(
+                        Modifier
+                            .then(if (toggle) Modifier.graphicsLayer(alpha = 0f) else Modifier)
+                            .background(Color.Red)
+                            .size(sizeDp)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+            assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+            assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+        }
+
+        rule.runOnIdle {
+            toggle = !toggle
+        }
+
+        rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+            assertEquals(Color.Red.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+            assertEquals(Color.Red.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+        }
+    }
 }
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/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
index 83a4a4f..1bfa2ff 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
@@ -37,17 +37,12 @@
     @get:Rule
     val rule = createComposeRule()
 
-    var value by mutableStateOf(1)
-    var callbackInvoked = false
-
     @Test
     fun simplyObservingValue_doesNotTriggerCallback() {
         // Arrange.
-        val observerNode = object : ObserverNode, Modifier.Node() {
-            override fun onObservedReadsChanged() {
-                callbackInvoked = true
-            }
-        }
+        val value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
         rule.setContent {
             Box(Modifier.modifierElementOf { observerNode })
         }
@@ -67,11 +62,9 @@
     @Test
     fun changeInObservedValue_triggersCallback() {
         // Arrange.
-        val observerNode = object : ObserverNode, Modifier.Node() {
-            override fun onObservedReadsChanged() {
-                callbackInvoked = true
-            }
-        }
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
         rule.setContent {
             Box(Modifier.modifierElementOf { observerNode })
         }
@@ -91,10 +84,94 @@
         }
     }
 
+    @Test(expected = IllegalStateException::class)
+    fun unusedNodeDoesNotObserve() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+
+        // Act.
+        rule.runOnIdle {
+            // Read value to observe changes.
+            observerNode.observeReads { value.toString() }
+
+            // Write to the read value to trigger onObservedReadsChanged.
+            value = 3
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isFalse()
+        }
+    }
+
+    @Test
+    fun detachedNodeCanObserveReads() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            Box(if (attached) modifierElementOf { observerNode } else Modifier)
+        }
+
+        // Act.
+        // Read value while not attached.
+        rule.runOnIdle { attached = false }
+        rule.runOnIdle { observerNode.observeReads { value.toString() } }
+        rule.runOnIdle { attached = true }
+        // Write to the read value to trigger onObservedReadsChanged.
+        rule.runOnIdle { value = 3 }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isTrue()
+        }
+    }
+
+    @Test
+    fun detachedNodeDoesNotCallOnObservedReadsChanged() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            Box(if (attached) modifierElementOf { observerNode } else Modifier)
+        }
+
+        // Act.
+        rule.runOnIdle {
+            // Read value to observe changes.
+            observerNode.observeReads { value.toString() }
+        }
+
+        rule.runOnIdle {
+            attached = false
+        }
+        // Write to the read value to trigger onObservedReadsChanged.
+        rule.runOnIdle { value = 3 }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isFalse()
+        }
+    }
+
     @ExperimentalComposeUiApi
     private inline fun <reified T : Modifier.Node> Modifier.modifierElementOf(
-        crossinline create: () -> T
+        crossinline create: () -> T,
     ): Modifier {
         return this.then(modifierElementOf(create) { name = "testNode" })
     }
+
+    class TestObserverNode(
+        private val onObserveReadsChanged: () -> Unit,
+    ) : ObserverNode, Modifier.Node() {
+        override fun onObservedReadsChanged() {
+            this.onObserveReadsChanged()
+        }
+    }
 }
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/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 6dea801..2c43580 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -191,7 +191,7 @@
             level = DeprecationLevel.HIDDEN
         )
         override val isValid: Boolean
-            get() = TODO("Not yet implemented")
+            get() = isAttached
 
         internal open fun updateCoordinator(coordinator: NodeCoordinator?) {
             this.coordinator = coordinator
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/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index 215dd4d..2b4b7ac 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2022 The Android Open Source Project
  *
@@ -302,10 +301,10 @@
 }
 
 @ExperimentalComposeUiApi
-internal fun DelegatableNode.requireLayoutNode(): LayoutNode = node.coordinator!!.layoutNode
+internal fun DelegatableNode.requireLayoutNode() = checkNotNull(node.coordinator).layoutNode
 
 @ExperimentalComposeUiApi
-internal fun DelegatableNode.requireOwner(): Owner = requireLayoutNode().owner!!
+internal fun DelegatableNode.requireOwner(): Owner = checkNotNull(requireLayoutNode().owner)
 
 /**
  * Invalidates the subtree of this layout, including layout, drawing, parent data, etc.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 9d0bed1..c56b027 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -234,7 +234,7 @@
         headToTail {
             if (!it.isAttached) {
                 it.attach()
-                if (performInvalidations) autoInvalidateNode(it)
+                if (performInvalidations) autoInvalidateInsertedNode(it)
             }
         }
     }
@@ -432,7 +432,7 @@
             // for removing nodes, we always do the autoInvalidateNode call,
             // regardless of whether or not it was a ModifierNodeElement with autoInvalidate
             // true, or a BackwardsCompatNode, etc.
-            autoInvalidateNode(node)
+            autoInvalidateRemovedNode(node)
             node.detach()
         }
         return removeNode(node)
@@ -510,24 +510,26 @@
             check(node is BackwardsCompatNode)
             node.element = next
             // we always autoInvalidate BackwardsCompatNode
-            autoInvalidateNode(node)
+            autoInvalidateUpdatedNode(node)
             return node
         }
         val updated = next.updateUnsafe(node)
-        val result = if (updated !== node) {
+        if (updated !== node) {
             // if a new instance is returned, we want to detach the old one
+            autoInvalidateRemovedNode(node)
             node.detach()
-            replaceNode(node, updated)
+            val result = replaceNode(node, updated)
+            autoInvalidateInsertedNode(updated)
+            return result
         } else {
             // the node was updated. we are done.
-            updated
+            if (next.autoInvalidate) {
+                // the modifier element is labeled as "auto invalidate", which means that since the
+                // node was updated, we need to invalidate everything relevant to it
+                autoInvalidateUpdatedNode(updated)
+            }
+            return updated
         }
-        if (next.autoInvalidate) {
-            // the modifier element is labeled as "auto invalidate", which means that since the
-            // node was updated, we need to invalidate everything relevant to it
-            autoInvalidateNode(result)
-        }
-        return result
     }
 
     // TRAVERSAL
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4f8022f..04fef20 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -156,10 +156,29 @@
     return mask
 }
 
+private const val Updated = 0
+private const val Inserted = 1
+private const val Removed = 2
+
 @OptIn(ExperimentalComposeUiApi::class)
-internal fun autoInvalidateNode(node: Modifier.Node) {
+internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
+@OptIn(ExperimentalComposeUiApi::class)
+private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
     if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
         node.invalidateMeasurements()
+        if (phase == Removed) {
+            val coordinator = node.requireCoordinator(Nodes.Layout)
+            val layer = coordinator.layer
+            if (layer != null) {
+                coordinator.onLayerBlockUpdated(null)
+            }
+        }
     }
     if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
         node.requireLayoutNode().invalidateMeasurements()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
index 5b4aa14..2450ada 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
@@ -36,7 +36,7 @@
     @ExperimentalComposeUiApi
     companion object {
         internal val OnObserveReadsChanged: (ObserverNode) -> Unit = {
-            it.onObservedReadsChanged()
+            if (it.node.isAttached) it.onObservedReadsChanged()
         }
     }
 }
@@ -47,8 +47,8 @@
 @ExperimentalComposeUiApi
 fun <T> T.observeReads(block: () -> Unit) where T : Modifier.Node, T : ObserverNode {
     requireOwner().snapshotObserver.observeReads(
-        this,
-        ObserverNode.OnObserveReadsChanged,
-        block
+        target = this,
+        onChanged = ObserverNode.OnObserveReadsChanged,
+        block = block
     )
 }
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/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
index f735ace..2082a7a 100644
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
@@ -58,7 +58,6 @@
 import kotlinx.coroutines.test.runTest
 import kotlinx.coroutines.withContext
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -244,7 +243,6 @@
         }
     }
 
-    @Ignore("b/244352517")
     @Test
     fun testReadFromNonExistentFile() = runTest {
         val nonExistentFile = tempFolder.newFile()
@@ -372,7 +370,7 @@
             dataStore.updateData { awaitCancellation() }
         }
 
-        dsScope.launch(Dispatchers.Unconfined) {
+        val started = dsScope.async(Dispatchers.Unconfined) {
             dataStore.updateData {
                 latch.await()
                 it.inc()
@@ -387,6 +385,11 @@
 
         assertThat(awaitingCancellation.isCancelled).isTrue()
         assertThat(notStarted.isCancelled).isTrue()
+
+        // wait for coroutine to complete to prevent it from outliving the test, which is flaky
+        latch.complete(Unit)
+        started.await()
+        assertThat(dataStore.data.first()).isEqualTo(1)
     }
 
     @Test
diff --git a/datastore/datastore/build.gradle b/datastore/datastore/build.gradle
index dd9b2bb..305a0d6 100644
--- a/datastore/datastore/build.gradle
+++ b/datastore/datastore/build.gradle
@@ -14,48 +14,72 @@
  * limitations under the License.
  */
 
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import androidx.build.KmpPlatformsKt
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("kotlin-android")
 }
 
+def enableNative = KmpPlatformsKt.enableNative(project)
+
 android {
-    sourceSets {
-        androidTest {
-            // Shared TestingSerializer between test and androidTest
-            kotlin.srcDirs += test.kotlin.srcDirs
-        }
-    }
     namespace "androidx.datastore.datastore"
 }
 
-dependencies {
-    api(libs.kotlinStdlib)
-    api(libs.kotlinCoroutinesCore)
-    api("androidx.annotation:annotation:1.2.0")
-    api(project(":datastore:datastore-core"))
-    api(project(":datastore:datastore-core-okio"))
+androidXMultiplatform {
+    android()
 
-    testImplementation(libs.junit)
-    testImplementation(libs.kotlinCoroutinesTest)
-    testImplementation(libs.truth)
-    testImplementation(project(":internal-testutils-truth"))
+    sourceSets {
+        androidMain {
+            dependencies {
+                api(libs.kotlinStdlib)
+                api(libs.kotlinCoroutinesCore)
+                api("androidx.annotation:annotation:1.2.0")
+                api(project(":datastore:datastore-core"))
+                api(project(":datastore:datastore-core-okio"))
+            }
 
-    androidTestImplementation(libs.junit)
-    androidTestImplementation(libs.kotlinCoroutinesTest)
-    androidTestImplementation(libs.truth)
-    androidTestImplementation(project(":internal-testutils-truth"))
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testCore)
+        }
+        androidAndroidTest {
+            dependencies {
+                implementation(libs.junit)
+                implementation(libs.kotlinCoroutinesTest)
+                implementation(libs.truth)
+                implementation(project(":internal-testutils-truth"))
+
+                implementation(libs.junit)
+                implementation(libs.kotlinCoroutinesTest)
+                implementation(libs.truth)
+                implementation(project(":internal-testutils-truth"))
+                implementation(libs.testRunner)
+                implementation(libs.testCore)
+            }
+
+        }
+        targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget).configureEach {
+            binaries.all {
+                binaryOptions["memoryModel"] = "experimental"
+            }
+        }
+        targets.all { target ->
+            if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+                target.compilations["main"].defaultSourceSet {
+                    dependsOn(nativeMain)
+                }
+                target.compilations["test"].defaultSourceSet {
+                    dependsOn(nativeTest)
+                }
+            }
+        }
+    }
 }
 
 androidx {
     name = "Android DataStore"
-    publish = Publish.SNAPSHOT_AND_RELEASE
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.DATASTORE
     inceptionYear = "2020"
     description = "Android DataStore - contains the underlying store used by each serialization " +
diff --git a/datastore/datastore/src/androidTest/AndroidManifest.xml b/datastore/datastore/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore/src/androidTest/AndroidManifest.xml
rename to datastore/datastore/src/androidAndroidTest/AndroidManifest.xml
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreDelegateTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreDelegateTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreFileTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreFileTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
diff --git a/datastore/datastore/src/main/AndroidManifest.xml b/datastore/datastore/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore/src/main/AndroidManifest.xml
rename to datastore/datastore/src/androidMain/AndroidManifest.xml
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreDelegate.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreDelegate.kt
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFile.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreFile.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/DataStoreFile.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreFile.kt
diff --git a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/migrations/SharedPreferencesMigration.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/migrations/SharedPreferencesMigration.kt
diff --git a/development/referenceDocs/switcher.py b/development/referenceDocs/switcher.py
index 0bb494d..63195ae 100755
--- a/development/referenceDocs/switcher.py
+++ b/development/referenceDocs/switcher.py
@@ -79,10 +79,9 @@
     if (java):
       file_path = doc[len(java_ref_root)+1:]
       stub = doc.replace(java_source_abs_path, kotlin_source_abs_path)
-      if (both):
-        slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% setvar page_path %}_page_path_{% endsetvar %}\\n{% setvar can_switch %}1{% endsetvar %}\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
-      else:
-        slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
+      # Always add the switcher for java files, switch to the package summary if
+      # the page itself doesn't exist in kotlin
+      slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% setvar page_path %}_page_path_{% endsetvar %}\\n{% setvar can_switch %}1{% endsetvar %}\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
     else:
       file_path = doc[len(kotlin_ref_root)+1:]
       stub = doc.replace(kotlin_source_abs_path, java_source_abs_path)
@@ -92,8 +91,13 @@
         slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% include \"reference\/_kotlin_switcher2.md\" %}",doc)
 
     os.system(slug1)
-    if (both):
-      page_path_slug = "sed -i 's/_page_path_/{}/' {}".format(file_path.replace("/","\/"),doc)
+    if both or java:
+      if both:
+        page_path = file_path
+      else:
+        page_path = os.path.join(os.path.dirname(file_path), "package-summary.html")
+
+      page_path_slug = "sed -i 's/_page_path_/{}/' {}".format(page_path.replace("/","\/"),doc)
       os.system(page_path_slug)
 
 
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 292d6fbb..187a9d4 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 # Get versions
-AGP_VERSION=${1:-8.0.0-alpha05}
-STUDIO_VERSION_STRING=${2:-"Android Studio Flamingo (2022.2.1) Canary 5"}
+AGP_VERSION=${1:-8.0.0-alpha07}
+STUDIO_VERSION_STRING=${2:-"Android Studio Flamingo (2022.2.1) Canary 7"}
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
 STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
 STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -20,6 +20,7 @@
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint:$LINT_VERSION,"
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint-tests:$LINT_VERSION,"
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint-gradle:$LINT_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="com.android.tools:ninepatch:$LINT_VERSION,"
 
 # Update studio_versions.properties
 sed -i "s/androidGradlePlugin = .*/androidGradlePlugin = \"$AGP_VERSION\"/g" gradle/libs.versions.toml
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 0ad099d..5df412d 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -29,10 +29,10 @@
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.2.0-beta01")
-    docs("androidx.benchmark:benchmark-common:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha06")
+    docs("androidx.benchmark:benchmark-common:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha07")
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
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 4a9d32c..9679844 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
@@ -152,7 +152,7 @@
     }
 
     override fun getItemCount(): Int {
-        return (emojiGridColumns * emojiGridRows).toInt()
+        return flattenSource.size
     }
 
     override fun getItemViewType(position: Int): Int {
@@ -169,11 +169,11 @@
         return flattenSource
     }
 
-    fun getParentWidth(parent: ViewGroup): Int {
+    private fun getParentWidth(parent: ViewGroup): Int {
         return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
     }
 
-    internal fun updateEmojis(emojis: List<List<ItemViewData>>) {
+    fun updateEmojis(emojis: List<List<ItemViewData>>) {
         flattenSource = ItemViewDataFlatList(
             emojis,
             emojiGridColumns
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/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 55aadae..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
@@ -21,7 +21,6 @@
 import android.os.Trace
 import android.util.AttributeSet
 import android.widget.FrameLayout
-import androidx.recyclerview.widget.GridLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.CoroutineScope
@@ -93,7 +92,7 @@
     ): EmojiPickerBodyAdapter {
         val categoryNames = mutableListOf<String>()
         val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
-        for (i in 0 until categorizedEmojiData.size) {
+        for (i in categorizedEmojiData.indices) {
             categoryNames.add(categorizedEmojiData[i].categoryName)
             categorizedEmojis.add(
                 categorizedEmojiData[i].emojiDataList.toMutableList()
@@ -151,12 +150,6 @@
 
         // 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
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..f1bb622 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -22,10 +22,12 @@
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/emoji_picker_header"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/emoji_picker_header_height"/>
+        android:layout_height="@dimen/emoji_picker_header_height"
+        android:paddingEnd="@dimen/emoji_picker_header_padding"
+        android:paddingStart="@dimen/emoji_picker_header_padding" />
 
-    <androidx.recyclerview.widget.RecyclerView
+    <androidx.emoji2.emojipicker.EmojiPickerBodyView
         android:id="@+id/emoji_picker_body"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+        android:layout_height="match_parent" />
 </LinearLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index 5ab47ec..cc79c54 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -23,4 +23,5 @@
     <dimen name="emoji_picker_header_icon_underline_width">28dp</dimen>
     <dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
     <dimen name="emoji_picker_header_height">50dp</dimen>
+    <dimen name="emoji_picker_header_padding">8dp</dimen>
 </resources>
\ No newline at end of file
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/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 07513fd..0546b9e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,19 +2,19 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "8.0.0-alpha05"
+androidGradlePlugin = "8.0.0-alpha07"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.0.0-alpha05"
+androidLint = "31.0.0-alpha07"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2022.2.1.5"
+androidStudio = "2022.2.1.7"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
 androidLintMin = "30.0.4"
 androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.5.0"
+androidxTestRunner = "1.5.1"
 androidxTestRules = "1.5.0"
 androidxTestMonitor = "1.6.0"
 androidxTestCore = "1.5.0"
@@ -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"
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/libraryversions.toml b/libraryversions.toml
index 20d5b80..c5f8065 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -14,15 +14,15 @@
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.5.0-alpha02"
 BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.3.0-alpha01"
+CAMERA = "1.3.0-alpha02"
 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_MATERIAL3 = "1.1.0-alpha02"
+COMPOSE = "1.4.0-alpha03"
+COMPOSE_COMPILER = "1.4.0-alpha02"
+COMPOSE_MATERIAL3 = "1.1.0-alpha03"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
 CONSTRAINTLAYOUT = "2.2.0-alpha05"
 CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha05"
@@ -60,7 +60,7 @@
 FUTURES = "1.2.0-alpha01"
 GLANCE = "1.0.0-alpha06"
 GLANCE_TEMPLATE = "1.0.0-alpha01"
-GRAPHICS = "1.0.0-alpha02"
+GRAPHICS = "1.0.0-alpha03"
 GRIDLAYOUT = "1.1.0-alpha01"
 HEALTH_CONNECT = "1.0.0-alpha08"
 HEALTH_SERVICES_CLIENT = "1.0.0-beta02"
@@ -83,7 +83,7 @@
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-alpha01"
 MEDIA2 = "1.3.0-alpha01"
-MEDIAROUTER = "1.4.0-alpha01"
+MEDIAROUTER = "1.4.0-alpha02"
 METRICS = "1.0.0-alpha04"
 NAVIGATION = "2.6.0-alpha04"
 PAGING = "3.2.0-alpha04"
@@ -93,7 +93,7 @@
 PREFERENCE = "1.3.0-alpha01"
 PRINT = "1.1.0-beta01"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha01"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha02"
 PRIVACYSANDBOX_UI = "1.0.0-alpha01"
 PROFILEINSTALLER = "1.3.0-alpha02"
 RECOMMENDATION = "1.1.0-alpha01"
@@ -125,7 +125,7 @@
 TRACING = "1.2.0-alpha02"
 TRACING_PERFETTO = "1.0.0-alpha07"
 TRANSITION = "1.5.0-alpha01"
-TV = "1.0.0-alpha02"
+TV = "1.0.0-alpha03"
 TVPROVIDER = "1.1.0-alpha02"
 VECTORDRAWABLE = "1.2.0-beta02"
 VECTORDRAWABLE_ANIMATED = "1.2.0-beta01"
@@ -141,12 +141,12 @@
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
 WEAR_TILES = "1.2.0-alpha01"
-WEAR_WATCHFACE = "1.2.0-alpha04"
-WEBKIT = "1.6.0-alpha03"
-WINDOW = "1.1.0-alpha04"
+WEAR_WATCHFACE = "1.2.0-alpha05"
+WEBKIT = "1.6.0-alpha04"
+WINDOW = "1.1.0-alpha05"
 WINDOW_EXTENSIONS = "1.1.0-alpha02"
 WINDOW_SIDECAR = "1.0.0-rc01"
-WORK = "2.8.0-beta02"
+WORK = "2.9.0-alpha01"
 
 [groups]
 ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
diff --git a/lifecycle/lifecycle-extensions/build.gradle b/lifecycle/lifecycle-extensions/build.gradle
index cf246c3..92c8382 100644
--- a/lifecycle/lifecycle-extensions/build.gradle
+++ b/lifecycle/lifecycle-extensions/build.gradle
@@ -45,6 +45,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.espressoCore)
+    androidTestImplementation(libs.multidex)
     androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
     androidTestImplementation(project(":internal-testutils-runtime"))
 }
@@ -60,5 +61,8 @@
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled true
+    }
     namespace "androidx.lifecycle.extensions"
 }
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/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
index 07806af..5733f15 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.build.lint
 
+import com.android.tools.lint.checks.infrastructure.TestMode
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -121,7 +122,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -199,7 +206,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -278,7 +291,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -371,7 +390,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -405,7 +430,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -443,7 +474,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -478,7 +515,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 57222fb..33cb3fe 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -26,6 +26,9 @@
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
+    defaultConfig {
+        multiDexEnabled true
+    }
     namespace "androidx.navigation.common"
 }
 
@@ -56,6 +59,7 @@
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
     androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.multidex)
 
     lintPublish(project(':navigation:navigation-common-lint'))
 }
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
index 2619c96..b6215a9 100644
--- a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
@@ -17,6 +17,7 @@
 package androidx.navigation.runtime.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -89,6 +90,7 @@
             """
             )
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257336973
             .run()
             .expect("""
 res/navigation/nav_main.xml:17: Warning: Do not attach a <deeplink> to an <activity> destination. Attach the deeplink directly to the second activity or the start destination of a nav host in the second activity instead. [DeepLinkInActivityDestination]
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..9d3f1bb 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
 
@@ -94,6 +95,15 @@
     fun getDeclaredType(type: XTypeElement, vararg types: XType): XType
 
     /**
+     * Returns an [XType] representing a wildcard type.
+     *
+     * In Java source, this represents types like `?`, `? extends T`, and `? super T`.
+     *
+     * In Kotlin source, this represents types like `*`, `out T`, and `in T`.
+     */
+    fun getWildcardType(consumerSuper: XType? = null, producerExtends: XType? = null): XType
+
+    /**
      * Return an [XArrayType] that has [type] as the [XArrayType.componentType].
      */
     fun getArrayType(type: XType): XArrayType
@@ -119,7 +129,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/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index da6d0de..c9c5393 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -139,11 +139,6 @@
             check(it is JavacType)
             it.typeMirror
         }.toTypedArray()
-        check(
-            types.all {
-                it is JavacType
-            }
-        )
         return wrap<JavacDeclaredType>(
             typeMirror = typeUtils.getDeclaredType(type.element, *args),
             // type elements cannot have nullability hence we don't synthesize anything here
@@ -152,6 +147,20 @@
         )
     }
 
+    override fun getWildcardType(consumerSuper: XType?, producerExtends: XType?): XType {
+        check(consumerSuper == null || producerExtends == null) {
+            "Cannot supply both super and extends bounds."
+        }
+        return wrap(
+            typeMirror = typeUtils.getWildcardType(
+                (producerExtends as? JavacType)?.typeMirror,
+                (consumerSuper as? JavacType)?.typeMirror,
+            ),
+            kotlinType = null,
+            elementNullability = null
+        )
+    }
+
     fun wrapTypeElement(element: TypeElement) = typeElementStore[element]
 
     /**
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
index 48ddf0a..87f05bf 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.InternalXAnnotationValue
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
@@ -93,10 +94,26 @@
                     !is List<*> -> listOf(result)
                     else -> result
                 }.map {
-                    KspAnnotationValue(env, this, valueType.componentType, valueArgument) { it }
+                    KspAnnotationValue(env, this, valueType.componentType, valueArgument) {
+                        convertValueToType(it, valueType.componentType)
+                    }
                 }
             }
-            else -> result
+            else -> convertValueToType(result, valueType)
         }
     }
 }
+
+private fun convertValueToType(value: Any?, valueType: XType): Any? {
+    // Unlike Javac, KSP does not convert the value to the type declared on the annotation class's
+    // annotation value automatically so we have to do that conversion manually here.
+    return when (valueType.asTypeName()) {
+        XTypeName.PRIMITIVE_BYTE -> (value as Number).toByte()
+        XTypeName.PRIMITIVE_SHORT -> (value as Number).toShort()
+        XTypeName.PRIMITIVE_INT -> (value as Number).toInt()
+        XTypeName.PRIMITIVE_LONG -> (value as Number).toLong()
+        XTypeName.PRIMITIVE_FLOAT -> (value as Number).toFloat()
+        XTypeName.PRIMITIVE_DOUBLE -> (value as Number).toDouble()
+        else -> value
+    }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index d58625a..002a36a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -144,7 +144,11 @@
             }
             resolver.getTypeArgument(
                 argType.ksType.createTypeReference(),
-                variance = Variance.INVARIANT
+                variance = if (argType is KspTypeArgumentType) {
+                    argType.typeArg.variance
+                } else {
+                    Variance.INVARIANT
+                }
             )
         }
         return wrap(
@@ -153,6 +157,31 @@
         )
     }
 
+    override fun getWildcardType(consumerSuper: XType?, producerExtends: XType?): XType {
+        check(consumerSuper == null || producerExtends == null) {
+            "Cannot supply both super and extends bounds."
+        }
+        return wrap(
+            ksTypeArgument = if (consumerSuper != null) {
+                resolver.getTypeArgument(
+                    typeRef = (consumerSuper as KspType).ksType.createTypeReference(),
+                    variance = Variance.CONTRAVARIANT
+                )
+            } else if (producerExtends != null) {
+                resolver.getTypeArgument(
+                    typeRef = (producerExtends as KspType).ksType.createTypeReference(),
+                    variance = Variance.COVARIANT
+                )
+            } else {
+                // This returns the type "out Any?", which should be equivalent to "*"
+                resolver.getTypeArgument(
+                    typeRef = resolver.builtIns.anyType.makeNullable().createTypeReference(),
+                    variance = Variance.COVARIANT
+                )
+            }
+        )
+    }
+
     override fun getArrayType(type: XType): KspArrayType {
         check(type is KspType)
         return arrayTypeFactory.createWithComponentType(type)
@@ -184,7 +213,7 @@
         ksType = typeReference.resolve()
     )
 
-    fun wrap(ksTypeParam: KSTypeParameter, ksTypeArgument: KSTypeArgument): KspType {
+    fun wrap(ksTypeArgument: KSTypeArgument): KspType {
         val typeRef = ksTypeArgument.type
         if (typeRef != null && ksTypeArgument.variance == Variance.INVARIANT) {
             // fully resolved type argument, return regular type.
@@ -196,7 +225,6 @@
         return KspTypeArgumentType(
             env = this,
             typeArg = ksTypeArgument,
-            typeParam = ksTypeParam,
             jvmTypeResolver = null
         )
     }
@@ -231,7 +259,6 @@
                     ksType.createTypeReference(),
                     declaration.variance
                 ),
-                typeParam = declaration,
                 jvmTypeResolver = null
             )
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 3acfe4a..133b002 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -175,9 +175,7 @@
         if (env.resolver.isJavaRawType(ksType)) {
             emptyList()
         } else {
-            ksType.arguments.mapIndexed { index, arg ->
-                env.wrap(ksType.declaration.typeParameters[index], arg)
-            }
+            ksType.arguments.map { env.wrap(it) }
         }
     }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index f7471ae..668554d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -19,7 +19,6 @@
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import com.google.devtools.ksp.symbol.KSTypeArgument
-import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
 import com.squareup.kotlinpoet.javapoet.JTypeName
 import com.squareup.kotlinpoet.javapoet.KTypeName
@@ -30,7 +29,6 @@
  */
 internal class KspTypeArgumentType(
     env: KspProcessingEnv,
-    val typeParam: KSTypeParameter,
     val typeArg: KSTypeArgument,
     jvmTypeResolver: KspJvmTypeResolver?
 ) : KspType(
@@ -68,7 +66,6 @@
     override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
         return KspTypeArgumentType(
             env = env,
-            typeParam = typeParam,
             typeArg = DelegatingTypeArg(
                 original = typeArg,
                 type = ksType.withNullability(nullability).createTypeReference()
@@ -80,7 +77,6 @@
     override fun copyWithJvmTypeResolver(jvmTypeResolver: KspJvmTypeResolver): KspType {
         return KspTypeArgumentType(
             env = env,
-            typeParam = typeParam,
             typeArg = typeArg,
             jvmTypeResolver = jvmTypeResolver
         )
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/XAnnotationValueTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
index 8c01a28..3d05e0b 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
@@ -200,9 +200,9 @@
                     int[] intVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    intParam = 1,
-                    intArrayParam = {3, 5, 7},
-                    intVarArgsParam = {9, 11, 13}
+                    intParam = (short) 1,
+                    intArrayParam = {(byte) 3, (short) 5, 7},
+                    intVarArgsParam = {(byte) 9, (short) 11, 13}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -290,9 +290,9 @@
                     short[] shortVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    shortParam = (short) 1,
-                    shortArrayParam = {(short) 3, (short) 5, (short) 7},
-                    shortVarArgsParam = {(short) 9, (short) 11, (short) 13}
+                    shortParam = (byte) 1,
+                    shortArrayParam = {(byte) 3, (short) 5, 7},
+                    shortVarArgsParam = {(byte) 9, (short) 11, 13}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -380,9 +380,9 @@
                     long[] longVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    longParam = 1L,
-                    longArrayParam = {3L, 5L, 7L},
-                    longVarArgsParam = {9L, 11L, 13L}
+                    longParam = (byte) 1,
+                    longArrayParam = {(short) 3, (int) 5, 7L},
+                    longVarArgsParam = {(short) 9, (int) 11, 13L}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -470,9 +470,9 @@
                     float[] floatVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    floatParam = 1.1F,
-                    floatArrayParam = {3.1F, 5.1F, 7.1F},
-                    floatVarArgsParam = {9.1F, 11.1F, 13.1F}
+                    floatParam = (byte) 1,
+                    floatArrayParam = {(short) 3, 5.1F, 7.1F},
+                    floatVarArgsParam = {9, 11.1F, 13.1F}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -487,9 +487,9 @@
                     vararg val floatVarArgsParam: Float,
                 )
                 @MyAnnotation(
-                    floatParam = 1.1F,
-                    floatArrayParam = [3.1F, 5.1F, 7.1F],
-                    floatVarArgsParam = [9.1F, 11.1F, 13.1F],
+                    floatParam = 1F,
+                    floatArrayParam = [3F, 5.1F, 7.1F],
+                    floatVarArgsParam = [9F, 11.1F, 13.1F],
                 )
                 class MyClass
                 """.trimIndent()
@@ -530,20 +530,20 @@
             assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
                 .isEqualTo("""
                     @test.MyAnnotation(
-                        floatParam = 1.1f,
-                        floatArrayParam = {3.1f, 5.1f, 7.1f},
-                        floatVarArgsParam = {9.1f, 11.1f, 13.1f}
+                        floatParam = 1.0f,
+                        floatArrayParam = {3.0f, 5.1f, 7.1f},
+                        floatVarArgsParam = {9.0f, 11.1f, 13.1f}
                     )
                     """.removeWhiteSpace())
 
             val floatParam = annotation.getAnnotationValue("floatParam")
-            checkSingleValue(floatParam, 1.1F)
+            checkSingleValue(floatParam, 1.0F)
 
             val floatArrayParam = annotation.getAnnotationValue("floatArrayParam")
-            checkListValues(floatArrayParam, 3.1F, 5.1F, 7.1F)
+            checkListValues(floatArrayParam, 3.0F, 5.1F, 7.1F)
 
             val floatVarArgsParam = annotation.getAnnotationValue("floatVarArgsParam")
-            checkListValues(floatVarArgsParam, 9.1F, 11.1F, 13.1F)
+            checkListValues(floatVarArgsParam, 9.0F, 11.1F, 13.1F)
         }
     }
 
@@ -560,9 +560,9 @@
                     double[] doubleVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    doubleParam = 1.1,
-                    doubleArrayParam = {3.1, 5.1, 7.1},
-                    doubleVarArgsParam = {9.1, 11.1, 13.1}
+                    doubleParam = (byte) 1,
+                    doubleArrayParam = {(short) 3, 5.1F, 7.1},
+                    doubleVarArgsParam = {9, 11.1F, 13.1}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -577,9 +577,9 @@
                     vararg val doubleVarArgsParam: Double,
                 )
                 @MyAnnotation(
-                    doubleParam = 1.1,
-                    doubleArrayParam = [3.1, 5.1, 7.1],
-                    doubleVarArgsParam = [9.1, 11.1, 13.1],
+                    doubleParam = 1.0,
+                    doubleArrayParam = [3.0, 5.1, 7.1],
+                    doubleVarArgsParam = [9.0, 11.1, 13.1],
                 )
                 class MyClass
                 """.trimIndent()
@@ -615,25 +615,57 @@
             val annotation = invocation.processingEnv.requireTypeElement("test.MyClass")
                 .getAllAnnotations()
                 .single { it.qualifiedName == "test.MyAnnotation" }
+            annotation.getAnnotationValue("doubleParam").value
+            annotation.getAnnotationValue("doubleArrayParam").value
+            annotation.getAnnotationValue("doubleVarArgsParam").value
 
-            // Compare the AnnotationSpec string ignoring whitespace
-            assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
-                .isEqualTo("""
-                    @test.MyAnnotation(
-                        doubleParam = 1.1,
-                        doubleArrayParam = {3.1, 5.1, 7.1},
-                        doubleVarArgsParam = {9.1, 11.1, 13.1}
+            // The java source allows an interesting corner case where you can use a float,
+            // e.g. 5.1F, in place of a double and the value returned is converted to a double.
+            // Note that the kotlin source doesn't even allow this case so we've separated them
+            // into two separate checks below.
+            if (sourceKind == SourceKind.JAVA) {
+                // Compare the AnnotationSpec string ignoring whitespace
+                assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
+                    .isEqualTo(
+                        """
+                        @test.MyAnnotation(
+                            doubleParam = 1.0,
+                            doubleArrayParam = {3.0, 5.099999904632568, 7.1},
+                            doubleVarArgsParam = {9.0, 11.100000381469727, 13.1}
+                        )
+                        """.removeWhiteSpace()
                     )
-                    """.removeWhiteSpace())
 
-            val doubleParam = annotation.getAnnotationValue("doubleParam")
-            checkSingleValue(doubleParam, 1.1)
+                val doubleParam = annotation.getAnnotationValue("doubleParam")
+                checkSingleValue(doubleParam, 1.0)
 
-            val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
-            checkListValues(doubleArrayParam, 3.1, 5.1, 7.1)
+                val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
+                checkListValues(doubleArrayParam, 3.0, 5.099999904632568, 7.1)
 
-            val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
-            checkListValues(doubleVarArgsParam, 9.1, 11.1, 13.1)
+                val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
+                checkListValues(doubleVarArgsParam, 9.0, 11.100000381469727, 13.1)
+            } else {
+                // Compare the AnnotationSpec string ignoring whitespace
+                assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
+                    .isEqualTo(
+                        """
+                        @test.MyAnnotation(
+                            doubleParam = 1.0,
+                            doubleArrayParam = {3.0, 5.1, 7.1},
+                            doubleVarArgsParam = {9.0, 11.1, 13.1}
+                        )
+                        """.removeWhiteSpace()
+                    )
+
+                val doubleParam = annotation.getAnnotationValue("doubleParam")
+                checkSingleValue(doubleParam, 1.0)
+
+                val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
+                checkListValues(doubleArrayParam, 3.0, 5.1, 7.1)
+
+                val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
+                checkListValues(doubleVarArgsParam, 9.0, 11.1, 13.1)
+            }
         }
     }
 
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/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 4e5c8b2..54f581f 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -24,6 +24,7 @@
 import androidx.room.compiler.processing.util.asJClassName
 import androidx.room.compiler.processing.util.asKClassName
 import androidx.room.compiler.processing.util.dumpToString
+import androidx.room.compiler.processing.util.getDeclaredField
 import androidx.room.compiler.processing.util.getDeclaredMethodByJvmName
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethodByJvmName
@@ -1307,4 +1308,73 @@
             result.hasErrorContaining("Unresolved reference")
         }
     }
+
+    @Test
+    fun getWildcardType() {
+        fun XTestInvocation.checkType() {
+            val usageElement = processingEnv.requireTypeElement("test.Usage")
+            val fooElement = processingEnv.requireTypeElement("test.Foo")
+            val barType = processingEnv.requireType("test.Bar")
+
+            // Test a manually constructed Foo<Bar>
+            val fooBarType = processingEnv.getDeclaredType(fooElement, barType)
+            val fooBarUsageType = usageElement.getDeclaredField("fooBar").type
+            assertThat(fooBarType.asTypeName()).isEqualTo(fooBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<? extends Bar>
+            val fooExtendsBarType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType(producerExtends = barType)
+            )
+            val fooExtendsBarUsageType = usageElement.getDeclaredField("fooExtendsBar").type
+            assertThat(fooExtendsBarType.asTypeName())
+                .isEqualTo(fooExtendsBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<? super Bar>
+            val fooSuperBarType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType(consumerSuper = barType)
+            )
+            val fooSuperBarUsageType = usageElement.getDeclaredField("fooSuperBar").type
+            assertThat(fooSuperBarType.asTypeName()).isEqualTo(fooSuperBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<?>
+            val fooUnboundedType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType()
+            )
+            val fooUnboundedUsageType = usageElement.getDeclaredField("fooUnbounded").type
+            assertThat(fooUnboundedType.asTypeName()).isEqualTo(fooUnboundedUsageType.asTypeName())
+        }
+
+        runProcessorTest(listOf(Source.java(
+            "test.Foo",
+            """
+            package test;
+            class Usage {
+              Foo<?> fooUnbounded;
+              Foo<Bar> fooBar;
+              Foo<? extends Bar> fooExtendsBar;
+              Foo<? super Bar> fooSuperBar;
+            }
+            interface Foo<T> {}
+            interface Bar {}
+            """.trimIndent()
+        ))) { it.checkType() }
+
+        runProcessorTest(listOf(Source.kotlin(
+            "test.Usage.kt",
+            """
+            package test
+            class Usage {
+              val fooUnbounded: Foo<*> = TODO()
+              val fooBar: Foo<Bar> = TODO()
+              val fooExtendsBar: Foo<out Bar> = TODO()
+              val fooSuperBar: Foo<in Bar> = TODO()
+            }
+            interface Foo<T>
+            interface Bar
+            """.trimIndent()
+        ))) { it.checkType() }
+    }
 }
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/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index 64af3dd..6a1dc5d 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -687,7 +687,7 @@
         pinchArea.setGestureMargin(1_000);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
 
@@ -697,7 +697,7 @@
         pinchArea.setGestureMargin(1);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
@@ -715,7 +715,7 @@
         pinchArea.setGestureMargins(1, 1, 1_000, 1_000);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
 
@@ -725,7 +725,7 @@
         pinchArea.setGestureMargins(1, 1, 1, 1);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index ee12f2d..c38e665 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -635,6 +635,8 @@
         UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/scale_factor").text("1.0f"));
 
+        assertTrue(pinchArea.pinchOut(0, 10));
+        assertFalse(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         assertTrue(pinchArea.pinchOut(100, 10));
         assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
@@ -655,6 +657,8 @@
         UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/scale_factor").text("1.0f"));
 
+        assertTrue(pinchArea.pinchIn(0, 10));
+        assertFalse(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         assertTrue(pinchArea.pinchIn(100, 10));
         assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
@@ -674,6 +678,10 @@
         assertUiObjectNotFound(() -> noNode.pinchIn(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchOut(-1, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchOut(101, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchIn(-1, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchIn(101, 10));
     }
 
     @Test
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/UiAutomatorTestCase.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
index 914f9b3..c3e7bf1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
@@ -81,7 +81,7 @@
         if (monkeyVal != null) {
             // only if the monkey key is specified, we alter the state of monkey
             // else we should leave things as they are.
-            getUiDevice().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal));
+            getUiDevice().getUiAutomation().setRunAsMonkey(Boolean.parseBoolean(monkeyVal));
         }
     }
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 974fb8f..451f493 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -964,8 +964,9 @@
      * @throws UiObjectNotFoundException
      */
     public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException {
-        // make value between 1 and 100
-        percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+        if (percent < 0 || percent > 100) {
+            throw new IllegalArgumentException("Percent must be between 0 and 100");
+        }
         float percentage = percent / 100f;
 
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
@@ -1001,8 +1002,9 @@
      * @throws UiObjectNotFoundException
      */
     public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException {
-        // make value between 1 and 100
-        percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent;
+        if (percent < 0 || percent > 100) {
+            throw new IllegalArgumentException("Percent must be between 0 and 100");
+        }
         float percentage = percent / 100f;
 
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
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..e652c7e 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
@@ -855,12 +855,12 @@
         // matched attributes - now check for matching instance number
         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) {
             currentSelectorInstance =
-                    (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
+                    (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
         }
 
         // instance is required. Add count if not already counting
         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) {
-            currentSelectorCounter = (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
+            currentSelectorCounter = (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
         }
 
         // Verify
@@ -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/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/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/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index 16787a1..c4933a3 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
@@ -16,16 +16,9 @@
 
 package androidx.tv.material.carousel
 
-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.clickable
 import androidx.compose.foundation.focusable
-import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -33,9 +26,8 @@
 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
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -46,7 +38,6 @@
 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.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
@@ -58,34 +49,37 @@
 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.ComposeContentTestRule
 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
 
+private const val delayBetweenSlides = 2500L
+private const val animationTime = 900L
+private const val overlayRenderWaitTime = 1500L
+
 @OptIn(ExperimentalTvMaterialApi::class)
 class CarouselTest {
-
-    private val delayBetweenSlides = 2500L
-    private val animationTime = 900L
-    private val overlayRenderWaitTime = 1500L
-
     @get:Rule
     val rule = createComposeRule()
 
     @Test
     fun carousel_autoScrolls() {
-
         rule.setContent {
-            Content()
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -101,9 +95,10 @@
 
     @Test
     fun carousel_onFocus_stopsScroll() {
-
         rule.setContent {
-            Content()
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -122,15 +117,12 @@
 
     @Test
     fun carousel_onUserTriggeredPause_stopsScroll() {
-        var carouselState: CarouselState?
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) { carouselState?.pauseAutoScroll(it) }
-                })
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) { carouselState.pauseAutoScroll(it) }
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -145,18 +137,15 @@
 
     @Test
     fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
-        var carouselState: CarouselState?
         var pauseHandle: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        pauseHandle = carouselState?.pauseAutoScroll(it)
-                    }
-                })
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    pauseHandle = carouselState.pauseAutoScroll(it)
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -183,24 +172,21 @@
 
     @Test
     fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
-        var carouselState: CarouselState?
         var pauseHandle1: ScrollPauseHandle? = null
         var pauseHandle2: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        if (pauseHandle1 == null) {
-                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
-                        }
-                        if (pauseHandle2 == null) {
-                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
-                        }
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
                     }
-                })
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -233,24 +219,21 @@
 
     @Test
     fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
-        var carouselState: CarouselState?
         var pauseHandle1: ScrollPauseHandle? = null
-        var pauseHandle2: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        if (pauseHandle1 == null) {
-                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
-                        }
-                        if (pauseHandle2 == null) {
-                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
-                        }
+            val carouselState = remember { CarouselState() }
+            var pauseHandle2: ScrollPauseHandle? = null
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
                     }
-                })
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -278,7 +261,12 @@
     @Test
     fun carousel_outOfFocus_resumesScroll() {
         rule.setContent {
-            Content()
+            Column {
+                SampleCarousel {
+                    BasicText(text = "Text ${it + 1}")
+                }
+                BasicText(text = "Card", modifier = Modifier.focusable())
+            }
         }
 
         rule.onNodeWithText("Text 1")
@@ -297,7 +285,9 @@
     @Test
     fun carousel_pagerIndicatorDisplayed() {
         rule.setContent {
-            Content()
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
         }
 
         rule.onNodeWithTag("indicator").assertIsDisplayed()
@@ -306,7 +296,14 @@
     @Test
     fun carousel_withAnimatedContent_successfulTransition() {
         rule.setContent {
-            AnimatedContent()
+            SampleCarousel {
+                SampleCarouselSlide(index = it) {
+                    Column {
+                        BasicText(text = "Text ${it + 1}")
+                        BasicText(text = "PLAY")
+                    }
+                }
+            }
         }
 
         rule.onNodeWithText("Text 1").assertDoesNotExist()
@@ -318,11 +315,12 @@
         rule.onNodeWithText("PLAY").assertIsDisplayed()
     }
 
-    @FlakyTest(bugId = 246336782)
     @Test
     fun carousel_withAnimatedContent_successfulFocusIn() {
         rule.setContent {
-            AnimatedContent()
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -334,228 +332,98 @@
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
-        rule.onNodeWithText("PLAY").assertIsDisplayed()
-        rule.onNodeWithText("PLAY").assertIsFocused()
+        rule.onNodeWithText("Play 0").assertIsDisplayed()
+        rule.onNodeWithText("Play 0").assertIsFocused()
     }
 
     @Test
-    fun carousel_manualScrolling_ltr() {
-        rule.setContent {
-            Content {
-                TestButton("Button ${it + 1}")
-            }
-        }
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_manualScrolling_rtl() {
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Content {
-                    TestButton("Button ${it + 1}")
-                }
-            }
-        }
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-
-    private fun performKeyPress(keyCode: Int, count: Int = 1) {
-        for (i in 1..count) {
-            InstrumentationRegistry
-                .getInstrumentation()
-                .sendKeyDownUpSync(keyCode)
-        }
-    }
-
-    @Composable
-    fun Content(
-        carouselState: CarouselState = remember { CarouselState() },
-        slideCount: Int = 3,
-        content: @Composable (index: Int) -> Unit = { BasicText(text = "Text ${it + 1}") }
-    ) {
-        LazyColumn {
-            item {
-                Carousel(
-                    modifier = Modifier.fillMaxSize(),
-                    carouselState = carouselState,
-                    slideCount = slideCount,
-                    timeToDisplaySlideMillis = delayBetweenSlides,
-                    carouselIndicator = {
-                        CarouselDefaults.Indicator(modifier = Modifier
-                            .align(Alignment.BottomEnd)
-                            .padding(16.dp)
-                            .testTag("indicator"),
-                            carouselState = carouselState,
-                            slideCount = slideCount)
-                    },
-                    content = content
-                )
-            }
-            item {
-                Box(modifier = Modifier.focusable()
-                ) {
-                    BasicText(
-                        text = "Card",
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .padding(12.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun AnimatedContent(carouselState: CarouselState = remember { CarouselState() }) {
-        LazyColumn {
-            item {
-                Carousel(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .testTag("pager"),
-                    slideCount = 3,
-                    timeToDisplaySlideMillis = delayBetweenSlides,
-                    carouselState = carouselState
-                ) { Frame(text = "Text ${it + 1}") }
-            }
-            item {
-                Box(modifier = Modifier.focusable()
-                ) {
-                    BasicText(
-                        text = "Card",
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .padding(12.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun Frame(text: String) {
+    fun carousel_scrollToRegainFocus_checkBringIntoView() {
         val focusRequester = FocusRequester()
-        CarouselItem(
-            overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
-            background = {}) {
-            Column(modifier = Modifier
-                .onFocusChanged {
-                    if (it.isFocused) {
-                        focusRequester.requestFocus()
-                    }
+        rule.setContent {
+            LazyColumn {
+                items(3) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = Modifier
+                            .focusRequester(if (it == 0) focusRequester else FocusRequester.Default)
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .border(2.dp, if (isFocused) Color.Red else Color.Black)
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
                 }
-                .focusable()) {
-                BasicText(text = text)
-                Row(modifier = Modifier
-                    .horizontalScroll(rememberScrollState())
-                    .onFocusChanged {
-                        if (it.isFocused) {
-                            focusRequester.requestFocus()
+                item {
+                    Carousel(
+                        modifier = Modifier
+                            .height(500.dp)
+                            .fillMaxWidth()
+                            .testTag("featured-carousel")
+                            .border(2.dp, Color.Black),
+                        carouselState = remember { CarouselState() },
+                        slideCount = 3,
+                        timeToDisplaySlideMillis = delayBetweenSlides
+                    ) {
+                        SampleCarouselSlide(
+                            index = it,
+                            overlayRenderWaitTime = overlayRenderWaitTime,
+                        ) {
+                            Box {
+                                Column(modifier = Modifier.align(Alignment.BottomStart)) {
+                                    BasicText(text = "carousel-frame")
+                                    Row {
+                                        SampleButton(text = "PLAY")
+                                    }
+                                }
+                            }
                         }
                     }
-                    .focusable()) {
-                    TestButton(text = "PLAY", focusRequester)
+                }
+                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() }
 
-    @Composable
-    fun TestButton(text: String, focusRequester: FocusRequester? = null) {
-        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
-                )
-            )
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
 
-        val baseModifier =
-            if (focusRequester == null) Modifier else Modifier.focusRequester(focusRequester)
+        // 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(rule, "featured-carousel")).isTrue()
 
-        Box(
-            modifier = baseModifier
-                .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
-                    }
-                }
-                .clickable(onClick = {})) {
-            BasicText(text = text)
-        }
+        // 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(rule, "featured-carousel")).isTrue()
     }
 
     @Test
@@ -567,4 +435,188 @@
 
         rule.onNodeWithTag(testTag).assertExists()
     }
+
+    @Test
+    fun carousel_manualScrolling_ltr() {
+        rule.setContent {
+            SampleCarousel { index ->
+                SampleButton("Button ${index + 1}")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_manualScrolling_rtl() {
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalLayoutDirection provides LayoutDirection.Rtl
+            ) {
+                SampleCarousel {
+                    SampleButton("Button ${it + 1}")
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun SampleCarousel(
+    carouselState: CarouselState = remember { CarouselState() },
+    slideCount: Int = 3,
+    timeToDisplaySlideMillis: Long = delayBetweenSlides,
+    content: @Composable (index: Int) -> Unit
+) {
+    Carousel(
+        modifier = Modifier
+            .padding(5.dp)
+            .fillMaxWidth()
+            .height(50.dp)
+            .testTag("pager"),
+        carouselState = carouselState,
+        slideCount = slideCount,
+        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        carouselIndicator = {
+            CarouselDefaults.Indicator(
+                modifier = Modifier
+                    .align(Alignment.BottomEnd)
+                    .padding(16.dp)
+                    .testTag("indicator"),
+                carouselState = carouselState,
+                slideCount = slideCount
+            )
+        },
+        content = content,
+    )
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun SampleCarouselSlide(
+    index: Int,
+    overlayRenderWaitTime: Long = CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    content: (@Composable () -> Unit) = { SampleButton("Play $index") },
+) {
+    CarouselItem(
+        overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
+        background = {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Red)
+                    .border(2.dp, Color.Blue)
+            )
+        },
+        overlay = content
+    )
+}
+
+@Composable
+private fun SampleButton(text: String = "Play") {
+    var isFocused by remember { mutableStateOf(false) }
+    BasicText(
+        text = text,
+        modifier = Modifier
+            .size(100.dp, 20.dp)
+            .background(Color.Yellow)
+            .onFocusChanged { isFocused = it.isFocused }
+            .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
+            .focusable(),
+    )
+}
+
+private fun checkNodeCompletelyVisible(
+    rule: ComposeContentTestRule,
+    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
+            .getInstrumentation()
+            .sendKeyDownUpSync(keyCode)
+    }
 }
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/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/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
index ebe6b1b..d781ba1 100644
--- a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
+++ b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.resolveAsTypeface
 import androidx.compose.ui.text.style.TextOverflow
 import kotlin.math.roundToInt
 
@@ -90,12 +91,12 @@
         val fontFamilyResolver = LocalFontFamilyResolver.current
         typeFace = remember(fontFamily, fontWeight, fontStyle, fontSynthesis, fontFamilyResolver) {
             derivedStateOf {
-                fontFamilyResolver.resolve(
+                fontFamilyResolver.resolveAsTypeface(
                     fontFamily,
                     fontWeight ?: FontWeight.Normal,
                     fontStyle ?: FontStyle.Normal,
                     fontSynthesis ?: FontSynthesis.All
-                ).value as Typeface
+                ).value
             }
         }
         updateTypeFace()
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index 4ea531b..e681cd5 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -106,6 +106,15 @@
 
     @Test
     fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: ScalingLazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -121,7 +130,7 @@
                 autoCentering = null
             ) {
                 items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
+                    Box(Modifier.requiredSize(itemSize))
                 }
             }
             PositionIndicator(
@@ -444,6 +453,15 @@
 
     @Test
     fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: LazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -458,7 +476,7 @@
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
             ) {
                 items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
+                    Box(Modifier.requiredSize(itemSize))
                 }
             }
             PositionIndicator(
@@ -759,6 +777,15 @@
 
     @Test
     fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: ScrollState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -772,9 +799,9 @@
                     .verticalScroll(state = state),
                 verticalArrangement = Arrangement.spacedBy(itemSpacingDp)
             ) {
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSize))
+                Box(Modifier.requiredSize(itemSize))
+                Box(Modifier.requiredSize(itemSize))
             }
             PositionIndicator(
                 state = positionIndicatorState,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index 7d15403..ecd6bde 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -462,63 +462,63 @@
         ) {
             Box(
                 modifier = Modifier
-                    .fillMaxSize()
-                    .drawWithContent {
-                        // We need to invert reverseDirection when the screen is round and we are on
-                        // the left.
+                .fillMaxSize()
+                .drawWithContent {
+                    // We need to invert reverseDirection when the screen is round and we are on
+                    // the left.
                         val actualReverseDirection =
                             if (isScreenRound && !indicatorOnTheRight) {
-                                !reverseDirection
-                            } else {
-                                reverseDirection
-                            }
+                        !reverseDirection
+                    } else {
+                        reverseDirection
+                    }
 
-                        val indicatorPosition = if (actualReverseDirection) {
-                            1 - animatedDisplayState.value.position
-                        } else {
-                            animatedDisplayState.value.position
-                        }
+                    val indicatorPosition = if (actualReverseDirection) {
+                        1 - animatedDisplayState.value.position
+                    } else {
+                        animatedDisplayState.value.position
+                    }
 
-                        val indicatorWidthPx = indicatorWidth.toPx()
+                    val indicatorWidthPx = indicatorWidth.toPx()
 
-                        // We want position = 0 be the indicator aligned at the top of its area and
-                        // position = 1 be aligned at the bottom of the area.
+                    // We want position = 0 be the indicator aligned at the top of its area and
+                    // position = 1 be aligned at the bottom of the area.
                         val indicatorStart =
                             indicatorPosition * (1 - animatedDisplayState.value.size)
 
                         val diameter = max(containerSize.width, containerSize.height)
 
-                        val paddingHorizontalPx = paddingHorizontal.toPx()
-                        if (isScreenRound) {
-                            val usableHalf = diameter / 2f - paddingHorizontalPx
-                            val sweepDegrees =
-                                (2 * asin((indicatorHeight.toPx() / 2) / usableHalf)).toDegrees()
+                    val paddingHorizontalPx = paddingHorizontal.toPx()
+                    if (isScreenRound) {
+                        val usableHalf = diameter / 2f - paddingHorizontalPx
+                        val sweepDegrees =
+                            (2 * asin((indicatorHeight.toPx() / 2) / usableHalf)).toDegrees()
 
-                            drawCurvedIndicator(
-                                color,
-                                background,
-                                paddingHorizontalPx,
-                                indicatorOnTheRight,
-                                sweepDegrees,
-                                indicatorWidthPx,
-                                indicatorStart,
-                                animatedDisplayState.value.size,
-                                highlightAlpha.value
-                            )
-                        } else {
-                            drawStraightIndicator(
-                                color,
-                                background,
-                                paddingHorizontalPx,
-                                indicatorOnTheRight,
-                                indicatorWidthPx,
-                                indicatorHeightPx = indicatorHeight.toPx(),
-                                indicatorStart,
-                                animatedDisplayState.value.size,
-                                highlightAlpha.value
-                            )
-                        }
+                        drawCurvedIndicator(
+                            color,
+                            background,
+                            paddingHorizontalPx,
+                            indicatorOnTheRight,
+                            sweepDegrees,
+                            indicatorWidthPx,
+                            indicatorStart,
+                            animatedDisplayState.value.size,
+                            highlightAlpha.value
+                        )
+                    } else {
+                        drawStraightIndicator(
+                            color,
+                            background,
+                            paddingHorizontalPx,
+                            indicatorOnTheRight,
+                            indicatorWidthPx,
+                            indicatorHeightPx = indicatorHeight.toPx(),
+                            indicatorStart,
+                            animatedDisplayState.value.size,
+                            highlightAlpha.value
+                        )
                     }
+                }
             )
         }
     }
@@ -730,8 +730,10 @@
         // of list item offset.
         val lastItemEndOffset = lastItem.startOffset(state.anchorType.value!!) + lastItem.size
         val viewportEndOffset = state.viewportHeightPx.value!! / 2f
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         val lastItemVisibleFraction =
-            (1f - ((lastItemEndOffset - viewportEndOffset) / lastItem.size)).coerceAtMost(1f)
+            (1f - ((lastItemEndOffset - viewportEndOffset) /
+                lastItem.size.coerceAtLeast(1))).coerceAtMost(1f)
 
         return lastItem.index.toFloat() + lastItemVisibleFraction
     }
@@ -751,8 +753,10 @@
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
         val firstItemStartOffset = firstItem.startOffset(state.anchorType.value!!)
         val viewportStartOffset = - (state.viewportHeightPx.value!! / 2f)
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         val firstItemInvisibleFraction =
-            ((viewportStartOffset - firstItemStartOffset) / firstItem.size).coerceAtLeast(0f)
+            ((viewportStartOffset - firstItemStartOffset) /
+                firstItem.size.coerceAtLeast(1)).coerceAtLeast(0f)
 
         return firstItem.index.toFloat() + firstItemInvisibleFraction
     }
@@ -823,18 +827,22 @@
     private fun decimalLastItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val lastItem = state.layoutInfo.visibleItemsInfo.last()
+        // Coerce item sizes to at least 1 to avoid divide by zero for zero height items
         val lastItemVisibleSize =
-            (state.layoutInfo.viewportEndOffset - lastItem.offset).coerceAtMost(lastItem.size)
+            (state.layoutInfo.viewportEndOffset - lastItem.offset)
+                .coerceAtMost(lastItem.size).coerceAtLeast(1)
         return lastItem.index.toFloat() +
-            lastItemVisibleSize.toFloat() / lastItem.size.toFloat()
+            lastItemVisibleSize.toFloat() / lastItem.size.coerceAtLeast(1).toFloat()
     }
 
     private fun decimalFirstItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
         val firstItemOffset = firstItem.offset - state.layoutInfo.viewportStartOffset
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         return firstItem.index.toFloat() -
-            firstItemOffset.coerceAtMost(0).toFloat() / firstItem.size.toFloat()
+            firstItemOffset.coerceAtMost(0).toFloat() /
+            firstItem.size.coerceAtLeast(1).toFloat()
     }
 }
 
diff --git a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
index 0162522..593789a 100644
--- a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
+++ b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
@@ -19,6 +19,9 @@
         <service android:name="androidx.wear.watchface.client.test.WatchFaceControlTestService"/>
         <service android:name="androidx.wear.watchface.client.test.TestLifeCycleWatchFaceService"/>
         <service android:name="androidx.wear.watchface.client.test.TestNopCanvasWatchFaceService"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceA"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceB"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceC"/>
         <service
             android:name="androidx.wear.watchface.client.test.OutdatedWatchFaceControlTestService">
             <meta-data android:name="androidx.wear.watchface.xml_version" android:value="99999" />
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt
new file mode 100644
index 0000000..864dba7
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.watchface.client.test
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+open class ObservableService(private val latch: CountDownLatch) : Service() {
+    private val binder: IBinder = LocalBinder()
+
+    inner class LocalBinder : Binder() {
+        val service: ObservableService
+            get() = this@ObservableService
+    }
+
+    override fun onBind(intent: Intent?) = binder
+
+    override fun onCreate() {
+        super.onCreate()
+        latch.countDown()
+        this.stopSelf()
+    }
+}
+
+class ObservableServiceA : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceA::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
+
+class ObservableServiceB : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceB::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
+
+class ObservableServiceC : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceC::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
index 0d06e96..53b3eb5 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
@@ -131,6 +131,16 @@
         )
         return watchFace
     }
+
+    companion object {
+        var systemTimeMillis = 1000000000L
+    }
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = systemTimeMillis
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
 }
 
 internal class TestExampleOpenGLBackgroundInitWatchFaceService(
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index a10788a..dc02bd9 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -31,6 +31,7 @@
 import android.os.Looper
 import android.view.Surface
 import android.view.SurfaceHolder
+import androidx.annotation.CallSuper
 import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -43,6 +44,7 @@
 import androidx.wear.watchface.ContentDescriptionLabel
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.TapType
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceColors
 import androidx.wear.watchface.WatchFaceExperimental
@@ -66,6 +68,8 @@
 import androidx.wear.watchface.complications.data.LongTextComplicationData
 import androidx.wear.watchface.complications.data.PlainComplicationText
 import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import androidx.wear.watchface.complications.data.toApiComplicationData
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
@@ -166,7 +170,8 @@
     }
 
     @After
-    fun tearDown() {
+    @CallSuper
+    open fun tearDown() {
         // Interactive instances are not currently shut down when all instances go away. E.g. WCS
         // crashing does not cause the watch face to stop. So we need to shut down explicitly.
         if (this::engine.isInitialized) {
@@ -236,7 +241,52 @@
         }
         return value!!
     }
+
+    /**
+     * Updates the complications for [interactiveInstance] and waits until they have been applied.
+     */
+    protected fun updateComplicationsBlocking(
+        interactiveInstance: InteractiveWatchFaceClient,
+        slotIdToComplicationData: Map<Int, ComplicationData>
+    ) {
+        val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
+        var slot: ComplicationSlot
+
+        runBlocking {
+            slot = engine.deferredWatchFaceImpl.await()
+                .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
+        }
+
+        val updateCountDownLatch = CountDownLatch(1)
+        handlerCoroutineScope.launch {
+            slot.complicationData.collect { updateCountDownLatch.countDown() }
+        }
+
+        interactiveInstance.updateComplicationData(slotIdToComplicationData)
+        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
+    }
+
+    protected fun tapOnComplication(
+        interactiveInstance: InteractiveWatchFaceClient,
+        slotId: Int
+    ) {
+        val leftClickX = interactiveInstance.complicationSlotsState[slotId]!!.bounds.centerX()
+        val leftClickY = interactiveInstance.complicationSlotsState[slotId]!!.bounds.centerY()
+
+        interactiveInstance.sendTouchEvent(leftClickX, leftClickY, TapType.DOWN)
+        interactiveInstance.sendTouchEvent(leftClickX, leftClickY, TapType.UP)
+    }
 }
+
+fun rangedValueComplicationBuilder() =
+    RangedValueComplicationData.Builder(
+        value = 50.0f,
+        min = 10.0f,
+        max = 100.0f,
+        ComplicationText.EMPTY
+    )
+        .setText(PlainComplicationText.Builder("Battery").build())
+
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 @RequiresApi(Build.VERSION_CODES.O_MR1)
@@ -245,6 +295,14 @@
     private val exampleCanvasAnalogWatchFaceComponentName =
         componentOf<ExampleCanvasAnalogWatchFaceService>()
 
+    @After
+    override fun tearDown() {
+        super.tearDown()
+        ObservableServiceA.reset()
+        ObservableServiceB.reset()
+        ObservableServiceC.reset()
+    }
+
     @Test
     fun complicationProviderDefaults() {
         val wallpaperService = TestComplicationProviderDefaultsWatchFaceService(
@@ -413,36 +471,11 @@
     fun updateComplicationData() {
         val interactiveInstance = getOrCreateTestSubject()
 
-        // Under the hood updateComplicationData is a oneway aidl method so we need to perform some
-        // additional synchronization to ensure it's side effects have been applied before
-        // inspecting complicationSlotsState otherwise we risk test flakes.
-        val updateCountDownLatch = CountDownLatch(1)
-        var leftComplicationSlot: ComplicationSlot
-
-        runBlocking {
-            leftComplicationSlot = engine.deferredWatchFaceImpl.await()
-                .complicationSlotsManager.complicationSlots[
-                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-            ]!!
-        }
-
-        handlerCoroutineScope.launch {
-            leftComplicationSlot.complicationData.collect {
-                updateCountDownLatch.countDown()
-            }
-        }
-
-        interactiveInstance.updateComplicationData(
+        updateComplicationsBlocking(
+            interactiveInstance,
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
-                    RangedValueComplicationData.Builder(
-                        50.0f,
-                        10.0f,
-                        100.0f,
-                        ComplicationText.EMPTY
-                    )
-                        .setText(PlainComplicationText.Builder("Battery").build())
-                        .build(),
+                    rangedValueComplicationBuilder().build(),
                 EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
                     LongTextComplicationData.Builder(
                         PlainComplicationText.Builder("Test").build(),
@@ -450,7 +483,6 @@
                     ).build()
             )
         )
-        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
 
         assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2)
 
@@ -1092,6 +1124,107 @@
 
         assertThat(lastDisconnectReason).isEqualTo(DisconnectReasons.ENGINE_DETACHED)
     }
+
+    @Test
+    fun tapComplication() {
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(
+            context,
+            surfaceHolder
+        )
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
+        updateComplicationsBlocking(
+            interactiveInstance,
+            mapOf(
+                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+                    rangedValueComplicationBuilder()
+                        .setTapAction(ObservableServiceA.createPendingIntent(context))
+                        .build()
+            )
+        )
+
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(
+            ObservableServiceA.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS)
+        )
+    }
+
+    @Test
+    fun tapTimelineComplication() {
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(
+            context,
+            surfaceHolder
+        )
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
+        val watchFaceImpl = runBlocking { engine.deferredWatchFaceImpl.await() }
+
+        // Create a timeline complication with three phases, each with their own tap actions leading
+        // to ObservableServiceA, ObservableServiceB & ObservableServiceC getting bound.
+        val timelineComplication = rangedValueComplicationBuilder()
+            .setTapAction(ObservableServiceA.createPendingIntent(context))
+            .build()
+            .asWireComplicationData()
+
+        timelineComplication.setTimelineEntryCollection(
+            listOf(
+                ShortTextComplicationData.Builder(
+                    PlainComplicationText.Builder("B").build(),
+                    ComplicationText.EMPTY
+                )
+                    .setTapAction(ObservableServiceB.createPendingIntent(context))
+                    .build()
+                    .asWireComplicationData().apply {
+                        timelineStartEpochSecond =
+                            10 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                        timelineEndEpochSecond =
+                            20 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                    },
+                ShortTextComplicationData.Builder(
+                    PlainComplicationText.Builder("C").build(),
+                    ComplicationText.EMPTY
+                )
+                    .setTapAction(ObservableServiceC.createPendingIntent(context))
+                    .build()
+                    .asWireComplicationData().apply {
+                        timelineStartEpochSecond =
+                            20 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                        timelineEndEpochSecond =
+                            90 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                    }
+            )
+        )
+
+        updateComplicationsBlocking(
+            interactiveInstance,
+            mapOf(
+                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+                    timelineComplication.toApiComplicationData()
+            )
+        )
+
+        // A tap should initially lead to TapTargetServiceA getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceA.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+
+        // Simulate the passage of time and force timeline entry selection by drawing.
+        TestExampleCanvasAnalogWatchFaceService.systemTimeMillis += 15 * 1000
+        watchFaceImpl.onDraw()
+
+        // A tap should now lead to TapTargetServiceB getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceB.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+
+        // Simulate the passage of time and force timeline entry selection by drawing.
+        TestExampleCanvasAnalogWatchFaceService.systemTimeMillis += 20 * 1000
+        watchFaceImpl.onDraw()
+
+        // A tap should now lead to TapTargetServiceC getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceC.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+    }
 }
 
 @RunWith(AndroidJUnit4::class)
@@ -1277,7 +1410,8 @@
         val wallpaperService =
             TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2)
 
-        val interactiveInstance = getOrCreateTestSubject(wallpaperService,
+        val interactiveInstance = getOrCreateTestSubject(
+            wallpaperService,
             complications = emptyMap()
         )
 
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 02dc741..34de840 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -1119,10 +1119,12 @@
                 !fields.containsKey(FIELD_PLACEHOLDER_TYPE)
             ) {
                 null
-            } else ComplicationData(
-                fields.getInt(FIELD_PLACEHOLDER_TYPE),
-                fields.getBundle(FIELD_PLACEHOLDER_FIELDS)!!
-            )
+            } else {
+                ComplicationData(
+                    fields.getInt(FIELD_PLACEHOLDER_TYPE),
+                    fields.getBundle(FIELD_PLACEHOLDER_FIELDS)!!
+                )
+            }
         }
 
     /** Returns the bytes of the proto layout. */
diff --git a/wear/watchface/watchface-style/api/current.txt b/wear/watchface/watchface-style/api/current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/current.txt
+++ b/wear/watchface/watchface-style/api/current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 24ce1c1..1d98429 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -19,9 +19,11 @@
 import android.content.res.Resources
 import android.content.res.XmlResourceParser
 import android.graphics.drawable.Icon
+import android.os.Build
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.complications.IllegalNodeException
 import androidx.wear.watchface.complications.iterate
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
 import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -422,8 +424,10 @@
  *
  * @param userStyleSettings The user configurable style categories associated with this watch face.
  * Empty if the watch face doesn't support user styling. Note we allow at most one
- * [UserStyleSetting.ComplicationSlotsUserStyleSetting] and one
- * [UserStyleSetting.CustomValueUserStyleSetting] in the list.
+ * [UserStyleSetting.CustomValueUserStyleSetting] in the list. Prior to android T ot most one
+ * [UserStyleSetting.ComplicationSlotsUserStyleSetting] is allowed, however from android T it's
+ * possible with hierarchical styles for there to be more than one, but at most one can be active at
+ * any given time.
  */
 public class UserStyleSchema constructor(
     userStyleSettings: List<UserStyleSetting>
@@ -521,9 +525,12 @@
             }
         }
 
-        // This requirement makes it easier to implement companion editors.
-        require(complicationSlotsUserStyleSettingCount <= 1) {
-            "At most only one ComplicationSlotsUserStyleSetting is allowed"
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            validateComplicationSettings(rootUserStyleSettings, null)
+        } else {
+            require(complicationSlotsUserStyleSettingCount <= 1) {
+               "Prior to Android T, at most only one ComplicationSlotsUserStyleSetting is allowed"
+            }
         }
 
         // There's a hard limit to how big Schema + UserStyle can be and since this data is sent
@@ -535,6 +542,28 @@
         }
     }
 
+    private fun validateComplicationSettings(
+        settings: Collection<UserStyleSetting>,
+        initialPrevSetting: UserStyleSetting.ComplicationSlotsUserStyleSetting?
+    ) {
+        var prevSetting = initialPrevSetting
+        for (setting in settings) {
+            if (setting is UserStyleSetting.ComplicationSlotsUserStyleSetting) {
+                require(prevSetting == null) {
+                    "From Android T multiple ComplicationSlotsUserStyleSettings are allowed, but" +
+                        " at most one can be active for any permutation of UserStyle. Note: " +
+                        "$setting and $prevSetting"
+                }
+                prevSetting = setting
+            }
+        }
+        for (setting in settings) {
+            for (option in setting.options) {
+                validateComplicationSettings(option.childSettings, prevSetting)
+            }
+        }
+    }
+
     /** @hide */
     @Suppress("Deprecation") // userStyleSettings
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -638,6 +667,40 @@
     private class NullOutputStream : OutputStream() {
         override fun write(value: Int) {}
     }
+
+    private fun findActiveComplicationSetting(
+        settings: Collection<UserStyleSetting>,
+        userStyle: UserStyle
+    ): UserStyleSetting.ComplicationSlotsUserStyleSetting? {
+        for (setting in settings) {
+            if (setting is UserStyleSetting.ComplicationSlotsUserStyleSetting) {
+                return setting
+            }
+            findActiveComplicationSetting(userStyle[setting]!!.childSettings, userStyle)?.let {
+                return it
+            }
+        }
+        return null
+    }
+
+    /**
+     * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
+     * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
+     * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
+     * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
+     * it returns `null`.
+     *
+     * @param userStyle The [UserStyle] for which the function will search for the selected
+     * [ComplicationSlotsOption], if any.
+     * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+     * otherwise.
+     */
+    public fun findComplicationSlotsOptionForUserStyle(
+        userStyle: UserStyle
+    ): ComplicationSlotsOption? =
+        findActiveComplicationSetting(rootUserStyleSettings, userStyle)?.let {
+            userStyle[it] as ComplicationSlotsOption
+        }
 }
 
 /**
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 6da2687..4691492 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -878,8 +878,11 @@
      * The ComplicationsManager listens for style changes with this setting and when a
      * [ComplicationSlotsOption] is selected the overrides are automatically applied. Note its
      * suggested that the default [ComplicationSlotOverlay] (the first entry in the list) does
-     * not apply any overrides. Only a single [ComplicationSlotsUserStyleSetting] is permitted in
-     * the [UserStyleSchema].
+     * not apply any overrides.
+     *
+     * From android T multiple [ComplicationSlotsUserStyleSetting] are allowed in a style hierarchy
+     * as long as at  most one is active for any permutation of [UserStyle]. Prior to android T only
+     * a single ComplicationSlotsUserStyleSetting was allowed.
      *
      * Not to be confused with complication data source selection.
      */
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 6b96dcf..8afe47f 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -20,6 +20,9 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
@@ -32,6 +35,7 @@
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
 
 private val redStyleOption =
     ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
@@ -121,6 +125,7 @@
 private val optionTrue = BooleanUserStyleSetting.BooleanOption.TRUE
 private val optionFalse = BooleanUserStyleSetting.BooleanOption.FALSE
 
+@Config(minSdk = Build.VERSION_CODES.TIRAMISU)
 @RunWith(StyleTestRunner::class)
 class CurrentUserStyleRepositoryTest {
 
@@ -736,6 +741,307 @@
         assertThat(watchHandLengthStyleSetting.hasParent).isTrue()
         assertThat(watchHandStyleSetting.hasParent).isTrue()
     }
+
+    @Test
+    fun invalid_multiple_ComplicationSlotsUserStyleSettings_same_level() {
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1, complicationSetting2)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null,
+            childSettings = listOf(complicationSetting2)
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            UserStyleSchema(
+                listOf(
+                    ListUserStyleSetting(
+                        UserStyleSetting.Id("a123"),
+                        displayName = "A123",
+                        description = "A123",
+                        icon = null,
+                        listOf(optionA1, optionA2),
+                        WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+                    ),
+                    complicationSetting1,
+                    complicationSetting2
+                )
+            )
+        }
+    }
+
+    @Test
+    fun invalid_multiple_ComplicationSlotsUserStyleSettings_different_levels() {
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            UserStyleSchema(
+                listOf(
+                    ListUserStyleSetting(
+                        UserStyleSetting.Id("a123"),
+                        displayName = "A123",
+                        description = "A123",
+                        icon = null,
+                        listOf(optionA1, optionA2),
+                        WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+                    ),
+                    complicationSetting1,
+                    complicationSetting2
+                )
+            )
+        }
+    }
+
+    @Test
+    @Suppress("deprecation")
+    fun multiple_ComplicationSlotsUserStyleSettings() {
+        // The code below constructs the following hierarchy:
+        //
+        //                                  rootABChoice
+        //          rootOptionA   ---------/            \---------  rootOptionB
+        //               |                                               |
+        //          a123Choice                                       b12Choice
+        //         /    |     \                                      /      \
+        // optionA1  optionA2  optionA3                        optionB1    optionB2
+        //   |          |                                         |
+        //   |      complicationSetting2                 complicationSetting1
+        // complicationSetting1
+
+        val leftComplicationID = 101
+        val rightComplicationID = 102
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val noComplications = ComplicationSlotsOption(
+            Option.Id("NO_COMPLICATIONS"),
+            displayName = "None",
+            icon = null,
+            listOf(
+                ComplicationSlotOverlay(leftComplicationID, enabled = false),
+                ComplicationSlotOverlay(rightComplicationID, enabled = false)
+            )
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications, noComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val leftComplication = ComplicationSlotsOption(
+            Option.Id("LEFT_COMPLICATION"),
+            displayName = "Left",
+            icon = null,
+            listOf(ComplicationSlotOverlay(rightComplicationID, enabled = false))
+        )
+        val rightComplication = ComplicationSlotsOption(
+            Option.Id("RIGHT_COMPLICATION"),
+            displayName = "Right",
+            icon = null,
+            listOf(ComplicationSlotOverlay(leftComplicationID, enabled = false))
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftComplication, rightComplication),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val normal = ComplicationSlotsOption(
+            Option.Id("Normal"),
+            displayName = "Normal",
+            icon = null,
+            emptyList()
+        )
+        val traversal = ComplicationSlotsOption(
+            Option.Id("Traversal"),
+            displayName = "Traversal",
+            icon = null,
+            listOf(
+                ComplicationSlotOverlay(leftComplicationID, accessibilityTraversalIndex = 3),
+                ComplicationSlotOverlay(rightComplicationID, accessibilityTraversalIndex = 2)
+            )
+        )
+        val complicationSetting3 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting3"),
+            displayName = "Traversal Order",
+            description = "Traversal Order",
+            icon = null,
+            complicationConfig = listOf(normal, traversal),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null,
+            childSettings = listOf(complicationSetting2)
+        )
+        val optionA3 =
+            ListUserStyleSetting.ListOption(Option.Id("a3_style"), "A3", icon = null)
+
+        val a123Choice = ListUserStyleSetting(
+            UserStyleSetting.Id("a123"),
+            displayName = "A123",
+            description = "A123",
+            icon = null,
+            listOf(optionA1, optionA2, optionA3),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val optionB1 = ListUserStyleSetting.ListOption(
+            Option.Id("b1_style"),
+            displayName = "B1",
+            icon = null,
+            childSettings = listOf(complicationSetting3)
+        )
+        val optionB2 =
+            ListUserStyleSetting.ListOption(Option.Id("b2_style"), "B2", icon = null)
+
+        val b12Choice = ListUserStyleSetting(
+            UserStyleSetting.Id("b12"),
+            displayName = "B12",
+            "B12",
+            icon = null,
+            listOf(optionB1, optionB2),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val rootOptionA = ListUserStyleSetting.ListOption(
+            Option.Id("a_style"),
+            displayName = "A",
+            icon = null,
+            childSettings = listOf(a123Choice)
+        )
+        val rootOptionB = ListUserStyleSetting.ListOption(
+            Option.Id("b_style"),
+            displayName = "B",
+            icon = null,
+            childSettings = listOf(b12Choice)
+        )
+
+        val rootABChoice = ListUserStyleSetting(
+            UserStyleSetting.Id("root_ab"),
+            displayName = "AB",
+            description = "AB",
+            icon = null,
+            listOf(rootOptionA, rootOptionB),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val schema = UserStyleSchema(
+            listOf(
+                rootABChoice,
+                a123Choice,
+                b12Choice,
+                complicationSetting1,
+                complicationSetting2,
+                complicationSetting3
+            )
+        )
+
+        val userStyleMap = mutableMapOf(
+            rootABChoice to rootOptionA,
+            a123Choice to optionA1,
+            b12Choice to optionB1,
+            complicationSetting1 to leftAndRightComplications,
+            complicationSetting2 to rightComplication,
+            complicationSetting3 to traversal
+        )
+
+        // Test various userStyleMap permutations to ensure the correct ComplicationSlotsOption is
+        // returned.
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(leftAndRightComplications)
+
+        userStyleMap[a123Choice] = optionA2
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(rightComplication)
+
+        userStyleMap[a123Choice] = optionA3
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isNull()
+
+        userStyleMap[rootABChoice] = rootOptionB
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(traversal)
+
+        userStyleMap[b12Choice] = optionB2
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isNull()
+    }
 }
 
 @RunWith(StyleTestRunner::class)
diff --git a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
index 45a6c9b3..1ae8d16 100644
--- a/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
+++ b/wear/watchface/watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleHierarchicalStyleWatchFaceService.kt
@@ -16,14 +16,18 @@
 
 package androidx.wear.watchface.samples
 
+import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.Rect
+import android.graphics.RectF
 import android.graphics.drawable.Icon
 import android.view.SurfaceHolder
 import androidx.annotation.Px
+import androidx.wear.watchface.CanvasComplicationFactory
 import androidx.wear.watchface.CanvasType
+import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
@@ -32,18 +36,24 @@
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
 import androidx.wear.watchface.WatchState
+import androidx.wear.watchface.complications.ComplicationSlotBounds
+import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy
+import androidx.wear.watchface.complications.SystemDataSources
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.rendering.CanvasComplicationDrawable
 import androidx.wear.watchface.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
 import androidx.wear.watchface.style.WatchFaceLayer
 import java.time.ZonedDateTime
 import kotlin.math.cos
 import kotlin.math.sin
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 open class ExampleHierarchicalStyleWatchFaceService : WatchFaceService() {
 
@@ -52,7 +62,7 @@
             UserStyleSetting.Option.Id("12_style"),
             resources,
             R.string.digital_clock_style_12,
-            icon = null
+            Icon.createWithResource(this, R.drawable.red_style)
         )
     }
 
@@ -61,7 +71,48 @@
             UserStyleSetting.Option.Id("24_style"),
             resources,
             R.string.digital_clock_style_24,
-            icon = null
+            Icon.createWithResource(this, R.drawable.red_style)
+        )
+    }
+
+    @Suppress("Deprecation")
+    private val digitalComplicationSettings by lazy {
+        ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("DigitalComplications"),
+            resources,
+            R.string.digital_complications_setting,
+            R.string.digital_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("On"),
+                    resources,
+                    R.string.digital_complication_on_screen_name,
+                    Icon.createWithResource(this, R.drawable.on),
+                    listOf(
+                        ComplicationSlotOverlay(
+                            COMPLICATION1_ID,
+                            enabled = true,
+                            complicationSlotBounds =
+                                ComplicationSlotBounds(RectF(0.1f, 0.4f, 0.3f, 0.6f))
+                        ),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    ),
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Off"),
+                    resources,
+                    R.string.digital_complication_off_screen_name,
+                    Icon.createWithResource(this, R.drawable.off),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
         )
     }
 
@@ -120,31 +171,63 @@
         )
     }
 
-    internal val watchHandLengthStyleSetting by lazy {
-        DoubleRangeUserStyleSetting(
-            UserStyleSetting.Id(WATCH_HAND_LENGTH_STYLE_SETTING),
+    internal val drawHoursSetting by lazy {
+        UserStyleSetting.BooleanUserStyleSetting(
+            UserStyleSetting.Id(HOURS_STYLE_SETTING),
             resources,
-            R.string.watchface_hand_length_setting,
-            R.string.watchface_hand_length_setting_description,
-            null,
-            0.25,
-            1.0,
-            listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY),
-            0.75
+            R.string.watchface_draw_hours_setting,
+            R.string.watchface_draw_hours_setting_description,
+            icon = null,
+            listOf(WatchFaceLayer.BASE),
+            defaultValue = true,
+            watchFaceEditorData = null
         )
     }
 
-    internal val hoursDrawFreqStyleSetting by lazy {
-        LongRangeUserStyleSetting(
-            UserStyleSetting.Id(HOURS_DRAW_FREQ_STYLE_SETTING),
+    @Suppress("Deprecation")
+    private val analogComplicationSettings by lazy {
+        ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("AnalogComplications"),
             resources,
-            R.string.watchface_draw_hours_freq_setting,
-            R.string.watchface_draw_hours_freq_setting_description,
-            null,
-            HOURS_DRAW_FREQ_MIN,
-            HOURS_DRAW_FREQ_MAX,
-            listOf(WatchFaceLayer.BASE),
-            HOURS_DRAW_FREQ_DEFAULT
+            R.string.watchface_complications_setting,
+            R.string.watchface_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("One"),
+                    resources,
+                    R.string.analog_complication_one_screen_name,
+                    Icon.createWithResource(this, R.drawable.one),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Two"),
+                    resources,
+                    R.string.analog_complication_two_screen_name,
+                    Icon.createWithResource(this, R.drawable.two),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Three"),
+                    resources,
+                    R.string.analog_complication_three_screen_name,
+                    Icon.createWithResource(this, R.drawable.three),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = true)
+                    )
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
         )
     }
 
@@ -153,8 +236,12 @@
             UserStyleSetting.Option.Id("digital"),
             resources,
             R.string.style_digital_watch,
-            icon = null,
-            childSettings = listOf(digitalClockStyleSetting, colorStyleSetting)
+            icon = Icon.createWithResource(this, R.drawable.d),
+            childSettings = listOf(
+                digitalClockStyleSetting,
+                colorStyleSetting,
+                digitalComplicationSettings
+            )
         )
     }
 
@@ -163,8 +250,12 @@
             UserStyleSetting.Option.Id("analog"),
             resources,
             R.string.style_analog_watch,
-            icon = null,
-            childSettings = listOf(watchHandLengthStyleSetting, hoursDrawFreqStyleSetting)
+            icon = Icon.createWithResource(this, R.drawable.a),
+            childSettings = listOf(
+                colorStyleSetting,
+                drawHoursSetting,
+                analogComplicationSettings
+            )
         )
     }
 
@@ -185,11 +276,97 @@
             watchFaceType,
             digitalClockStyleSetting,
             colorStyleSetting,
-            watchHandLengthStyleSetting,
-            hoursDrawFreqStyleSetting
+            drawHoursSetting,
+            digitalComplicationSettings,
+            analogComplicationSettings
         )
     )
 
+    private val watchFaceStyle by lazy {
+        WatchFaceColorStyle.create(this, "red_style")
+    }
+
+    public override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        val canvasComplicationFactory =
+            CanvasComplicationFactory { watchState, listener ->
+                CanvasComplicationDrawable(
+                    watchFaceStyle.getDrawable(this@ExampleHierarchicalStyleWatchFaceService)!!,
+                    watchState,
+                    listener
+                )
+            }
+
+        val complicationOne = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION1_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_WATCH_BATTERY,
+                ComplicationType.RANGED_VALUE
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.1f, 0.8f, 0.3f))
+        ).setNameResourceId(R.string.hierarchical_complication1_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication1_screen_reader_name
+            ).build()
+
+        val complicationTwo = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION2_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_TIME_AND_DATE,
+                ComplicationType.SHORT_TEXT
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
+        ).setNameResourceId(R.string.hierarchical_complication2_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication2_screen_reader_name
+            ).build()
+
+        val complicationThree = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION3_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
+                ComplicationType.SHORT_TEXT
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.7f, 0.8f, 0.9f))
+        ).setNameResourceId(R.string.hierarchical_complication3_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication3_screen_reader_name
+            ).build()
+
+        return ComplicationSlotsManager(
+            listOf(complicationOne, complicationTwo, complicationThree),
+            currentUserStyleRepository
+        )
+    }
+
     override suspend fun createWatchFace(
         surfaceHolder: SurfaceHolder,
         watchState: WatchState,
@@ -206,6 +383,32 @@
             16L
         ) {
             val renderer = ExampleHierarchicalStyleWatchFaceRenderer()
+            val context: Context = this@ExampleHierarchicalStyleWatchFaceService
+
+            init {
+                CoroutineScope(Dispatchers.Main.immediate).launch {
+                    currentUserStyleRepository.userStyle.collect { userStyle ->
+                        for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                            (complication.renderer as CanvasComplicationDrawable).drawable =
+                                when (userStyle[colorStyleSetting]) {
+                                    redStyle ->
+                                        WatchFaceColorStyle.create(context, "red_style")
+                                            .getDrawable(context)!!
+                                    greenStyle ->
+                                        WatchFaceColorStyle.create(context, "green_style")
+                                            .getDrawable(context)!!
+                                    blueStyle ->
+                                        WatchFaceColorStyle.create(context, "blue_style")
+                                             .getDrawable(context)!!
+                                    else -> throw IllegalArgumentException(
+                                        "Unrecognized colorStyleSetting "
+                                    )
+                                }
+                        }
+                    }
+                }
+            }
+
             override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
                 val currentStyle = currentUserStyleRepository.userStyle.value
                 when (currentStyle[watchFaceType]) {
@@ -232,10 +435,8 @@
                         bounds,
                         zonedDateTime,
                         renderParameters,
-                        (currentStyle[hoursDrawFreqStyleSetting]!!
-                            as LongRangeOption).value.toInt(),
-                        (currentStyle[watchHandLengthStyleSetting]!!
-                            as DoubleRangeOption).value.toFloat()
+                        (currentStyle[drawHoursSetting]!!
+                            as UserStyleSetting.BooleanUserStyleSetting.BooleanOption).value,
                     )
 
                     else -> {
@@ -245,6 +446,12 @@
                         )
                     }
                 }
+
+                for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                    if (complication.enabled) {
+                        complication.render(canvas, zonedDateTime, renderParameters)
+                    }
+                }
             }
 
             override fun renderHighlightLayer(
@@ -252,7 +459,11 @@
                 bounds: Rect,
                 zonedDateTime: ZonedDateTime
             ) {
-                // Nothing to do.
+                for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                    if (complication.enabled) {
+                        complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+                    }
+                }
             }
         }
     )
@@ -352,8 +563,7 @@
             bounds: Rect,
             zonedDateTime: ZonedDateTime,
             renderParameters: RenderParameters,
-            hoursDrawFreq: Int,
-            watchHandLength: Float
+            drawHourPips: Boolean
         ) {
             val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
 
@@ -362,8 +572,8 @@
 
             paint.color = Color.WHITE
             paint.textSize = 20.0f
-            if (isActive) {
-                for (i in 12 downTo 1 step hoursDrawFreq) {
+            if (isActive && drawHourPips) {
+                for (i in 12 downTo 1 step 3) {
                     val rot = i.toFloat() / 12.0f * 2.0f * Math.PI
                     val dx = sin(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
                     val dy = -cos(rot).toFloat() * NUMBER_RADIUS_FRACTION * bounds.width().toFloat()
@@ -386,8 +596,8 @@
             val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
             val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
 
-            val hourXRadius = bounds.width() * watchHandLength * 0.35f
-            val hourYRadius = bounds.height() * watchHandLength * 0.35f
+            val hourXRadius = bounds.width() * 0.3f
+            val hourYRadius = bounds.height() * 0.3f
 
             paint.strokeWidth = if (isActive) 8f else 5f
             canvas.drawLine(
@@ -398,8 +608,8 @@
                 paint
             )
 
-            val minuteXRadius = bounds.width() * watchHandLength * 0.499f
-            val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+            val minuteXRadius = bounds.width() * 0.4f
+            val minuteYRadius = bounds.height() * 0.4f
 
             paint.strokeWidth = if (isActive) 4f else 2.5f
             canvas.drawLine(
@@ -418,11 +628,7 @@
         private const val GREEN_STYLE = "green_style"
         private const val BLUE_STYLE = "blue_style"
 
-        private const val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
-        private const val HOURS_DRAW_FREQ_STYLE_SETTING = "hours_draw_freq_style_setting"
-        private const val HOURS_DRAW_FREQ_MIN = 1L
-        private const val HOURS_DRAW_FREQ_MAX = 4L
-        private const val HOURS_DRAW_FREQ_DEFAULT = 3L
+        private const val HOURS_STYLE_SETTING = "hours_style_setting"
         private const val NUMBER_RADIUS_FRACTION = 0.45f
 
         private val timeText = charArrayOf('1', '0', ':', '0', '9')
@@ -439,5 +645,9 @@
 
         @Px
         private val TEXT_PADDING = 12
+
+        const val COMPLICATION1_ID = 101
+        const val COMPLICATION2_ID = 102
+        const val COMPLICATION3_ID = 103
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp
new file mode 100644
index 0000000..c837655
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp
new file mode 100644
index 0000000..f47294e
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp
new file mode 100644
index 0000000..e2db0da
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp
new file mode 100644
index 0000000..91e5649
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp
new file mode 100644
index 0000000..8a3622b
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp
new file mode 100644
index 0000000..0c5ffc1
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp
new file mode 100644
index 0000000..ceaa89a
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/values/strings.xml b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
index a5d1d53..de9835e 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -25,7 +25,7 @@
     <string name="gl_background_init_watch_face_name"
         translatable="false">Background Init Watchface</string>
     <string name="hierarchical_watch_face_name"
-        translatable="false">Example Hierarchical Watchface</string>
+        translatable="false">Hierarchical Watchface (Needs T)</string>
 
     <!-- Name of watchface style [CHAR LIMIT=20] -->
     <string name="red_style_name">Red Style</string>
@@ -116,6 +116,14 @@
     hours labeling [CHAR LIMIT=20] -->
     <string name="watchface_draw_hours_freq_setting_description">Labeling frequency</string>
 
+    <!-- Menu option to select a widget that lets us configure whether to show hour pips
+    [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting">Hour pips</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure whether to show
+    hour pips [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting_description">Display on/off</string>
+
     <!-- Name of the left complication for use visually in the companion editor. [CHAR LIMIT=20] -->
     <string name="left_complication_screen_name">Left</string>
 
@@ -177,4 +185,55 @@
 
     <!-- Sub title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
     <string name="clock_type_description">Select analog or digital</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting">Complication</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting_description">On or off</string>
+
+    <!-- List entry, enabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_on_screen_name">On</string>
+
+    <!-- List entry, disabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_off_screen_name">Off</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_one_screen_name">One</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_two_screen_name">Two</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_three_screen_name">Three</string>
+
+    <!-- Name of a complication at the top of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication1_screen_name">Top</string>
+
+    <!-- Name of a complication at the top of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication1_screen_reader_name">Top complication</string>
+
+    <!-- Name of a complication at the middle of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication2_screen_name">Middle</string>
+
+    <!-- Name of a complication at the middle of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication2_screen_reader_name">Middle complication</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication3_screen_name">Bottom</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication3_screen_reader_name">Bottom complication</string>
+
 </resources>
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 4ad2be0..b422068 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -42,7 +42,6 @@
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import java.time.Instant
 
@@ -152,31 +151,47 @@
         }
     }
 
+    private fun applyInitialComplicationConfig() {
+        for ((id, complication) in complicationSlots) {
+            val initialConfig = initialComplicationConfigs[id]!!
+            complication.complicationSlotBounds = initialConfig.complicationSlotBounds
+            complication.enabled = initialConfig.enabled
+            complication.accessibilityTraversalIndex = initialConfig.accessibilityTraversalIndex
+            complication.nameResourceId = initialConfig.nameResourceId
+            complication.screenReaderNameResourceId = initialConfig.screenReaderNameResourceId
+        }
+        onComplicationsUpdated()
+    }
+
     /** @hide */
     @WorkerThread
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @Suppress("Deprecation") // userStyleSettings
     public fun listenForStyleChanges(coroutineScope: CoroutineScope) {
-        val complicationsStyleCategory =
-            currentUserStyleRepository.schema.userStyleSettings.firstOrNull {
-                it is ComplicationSlotsUserStyleSetting
-            } ?: return
-
-        var previousOption: ComplicationSlotsOption = currentUserStyleRepository.userStyle.value[
-            complicationsStyleCategory
-        ]!! as ComplicationSlotsOption
+        var previousOption =
+            currentUserStyleRepository.schema.findComplicationSlotsOptionForUserStyle(
+                currentUserStyleRepository.userStyle.value
+            )
 
         // Apply the initial settings on the worker thread.
-        applyComplicationSlotsStyleCategoryOption(previousOption)
+        previousOption?.let {
+            applyComplicationSlotsStyleCategoryOption(it)
+        }
 
         // Add a listener so we can track changes and automatically apply them on the UIThread
         coroutineScope.launch {
-            currentUserStyleRepository.userStyle.collect { userStyle ->
+            currentUserStyleRepository.userStyle.collect {
                 val newlySelectedOption =
-                    userStyle[complicationsStyleCategory]!! as ComplicationSlotsOption
+                    currentUserStyleRepository.schema.findComplicationSlotsOptionForUserStyle(
+                        currentUserStyleRepository.userStyle.value
+                    )
+
                 if (previousOption != newlySelectedOption) {
                     previousOption = newlySelectedOption
-                    applyComplicationSlotsStyleCategoryOption(newlySelectedOption)
+                    if (newlySelectedOption == null) {
+                        applyInitialComplicationConfig()
+                    } else {
+                        applyComplicationSlotsStyleCategoryOption(newlySelectedOption)
+                    }
                 }
             }
         }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 0b6d480..40c59a8 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -934,9 +934,8 @@
         }
     }
 
-    /** @hide */
     @UiThread
-    internal fun onDraw() {
+    fun onDraw() {
         val startTime = getZonedDateTime()
         val startInstant = startTime.toInstant()
         val startTimeMillis = systemTimeProvider.getSystemTimeMillis()
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 92c05ee..9a10e9e3 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -808,31 +808,30 @@
     ) = TraceEvent(
         "WatchFaceService.writeComplicationCache"
     ).use {
-        try {
-            val stream = ByteArrayOutputStream()
-            val objectOutputStream = ObjectOutputStream(stream)
-            objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
-            for (slot in complicationSlotsManager.complicationSlots) {
-                objectOutputStream.writeInt(slot.key)
-                objectOutputStream.writeObject(
-                    if ((slot.value.complicationData.value.persistencePolicy and
-                            ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
-                    ) {
-                        NoDataComplicationData().asWireComplicationData()
-                    } else {
-                        slot.value.complicationData.value.asWireComplicationData()
-                    }
-                )
-            }
-            objectOutputStream.close()
-            val byteArray = stream.toByteArray()
-
-            // File IO can be slow so perform the write from a background thread.
-            getBackgroundThreadHandler().post {
+        // File IO can be slow so perform the write from a background thread.
+        getBackgroundThreadHandler().post {
+            try {
+                val stream = ByteArrayOutputStream()
+                val objectOutputStream = ObjectOutputStream(stream)
+                objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+                for (slot in complicationSlotsManager.complicationSlots) {
+                    objectOutputStream.writeInt(slot.key)
+                    objectOutputStream.writeObject(
+                        if ((slot.value.complicationData.value.persistencePolicy and
+                              ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+                        ) {
+                            NoDataComplicationData().asWireComplicationData()
+                        } else {
+                            slot.value.complicationData.value.asWireComplicationData()
+                        }
+                    )
+                }
+                objectOutputStream.close()
+                val byteArray = stream.toByteArray()
                 writeComplicationDataCacheByteArray(context, fileName, byteArray)
+            } catch (e: Exception) {
+                Log.w(TAG, "Failed to write to complication cache due to exception", e)
             }
-        } catch (e: Exception) {
-            Log.w(TAG, "Failed to write to complication cache due to exception", e)
         }
     }
 
@@ -2489,7 +2488,7 @@
                 "The estimated wire size of the supplied UserStyleSchemas for watch face " +
                     "$packageName is too big at $estimatedBytes bytes. UserStyleSchemas get sent " +
                     "to the companion over bluetooth and should be as small as possible for this " +
-                    "to be performant."
+                    "to be performant. The maximum size is " + MAX_REASONABLE_SCHEMA_WIRE_SIZE_BYTES
             }
         }
 
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 0bdb170..97ca016 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -409,6 +409,17 @@
         ),
         affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
     )
+    private val complicationsStyleSetting2 = ComplicationSlotsUserStyleSetting(
+        UserStyleSetting.Id("complications_style_setting2"),
+        "AllComplicationSlots",
+        "Number and position",
+        icon = null,
+        complicationConfig = listOf(
+            leftOnlyComplicationsOption,
+            rightOnlyComplicationsOption
+        ),
+        affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
+    )
 
     private lateinit var renderer: TestRenderer
     private lateinit var complicationSlotsManager: ComplicationSlotsManager
@@ -2701,6 +2712,79 @@
     }
 
     @Test
+    fun hierarchical_complicationsStyleSetting() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            return
+        }
+
+        val option1 = ListUserStyleSetting.ListOption(
+            Option.Id("1"),
+            displayName = "1",
+            icon = null,
+            childSettings = listOf(complicationsStyleSetting)
+        )
+        val option2 = ListUserStyleSetting.ListOption(
+            Option.Id("2"),
+            displayName = "2",
+            icon = null,
+            childSettings = listOf(complicationsStyleSetting2)
+        )
+        val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "3", icon = null)
+        val choice = ListUserStyleSetting(
+            UserStyleSetting.Id("123"),
+            displayName = "123",
+            description = "123",
+            icon = null,
+            listOf(option1, option2, option3),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        initEngine(
+            WatchFaceType.DIGITAL,
+            listOf(leftComplication, rightComplication),
+            UserStyleSchema(listOf(choice, complicationsStyleSetting, complicationsStyleSetting2)),
+            apiVersion = 4
+        )
+
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option1,
+                    complicationsStyleSetting to noComplicationsOption, // Active
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption
+                )
+            )
+        )
+        assertFalse(leftComplication.enabled)
+        assertFalse(rightComplication.enabled)
+
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option2,
+                    complicationsStyleSetting to noComplicationsOption,
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption // Active
+                )
+            )
+        )
+        assertFalse(leftComplication.enabled)
+        assertTrue(rightComplication.enabled)
+
+        // By default all complications are active if no complicationsStyleSetting applies.
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option3,
+                    complicationsStyleSetting to noComplicationsOption,
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption
+                )
+            )
+        )
+        assertTrue(leftComplication.enabled)
+        assertTrue(rightComplication.enabled)
+    }
+
+    @Test
     public fun observeComplicationData() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
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;
+    }
+}
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index 2952118..9c79acd 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -18,6 +18,18 @@
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
+
+        <service android:name="androidx.window.sample.TestIme"
+            android:label="@string/test_ime"
+            android:permission="android.permission.BIND_INPUT_METHOD"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+            <meta-data android:name="android.view.im"
+                android:resource="@xml/method"/>
+        </service>
+
         <activity android:name=".demos.WindowDemosActivity"
             android:exported="true"
             android:label="@string/windowManagerDemos">
@@ -172,6 +184,13 @@
             android:taskAffinity="androidx.window.sample.split_pip">
         </activity>
 
+        <!-- The demo app that shows various IME-related use cases -->
+
+        <activity android:name=".ImeActivity"
+            android:exported="false"
+            android:configChanges="orientation|screenSize|screenLayout|screenSize"
+            android:label="@string/ime"/>
+
         <!-- ActivityEmbedding Initializer -->
 
         <provider android:name="androidx.startup.InitializationProvider"
diff --git a/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
new file mode 100644
index 0000000..5ee6536
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
@@ -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.window.sample
+
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Demo app that shows various IME-related features.
+ */
+class ImeActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_ime)
+
+        findViewById<Button>(R.id.ime_button_settings).apply {
+            setOnClickListener {
+                val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
+                startActivity(intent)
+            }
+        }
+
+        findViewById<Button>(R.id.ime_button_switch_default).apply {
+            setOnClickListener {
+                val imm = getSystemService(InputMethodManager::class.java)
+                imm.showInputMethodPicker()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt b/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
new file mode 100644
index 0000000..d337e11
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.window.sample
+
+import android.inputmethodservice.InputMethodService
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+
+/**
+ * A test IME that currently provides a minimal UI containing a "Close" button. To use this, go to
+ * "Settings > System > Languages & Input > On-screen keyboard" and enable "Test IME". Remember you
+ * may still need to switch to this IME after the default on-screen keyboard pops up.
+ */
+internal class TestIme : InputMethodService() {
+
+    override fun onCreateInputView(): View {
+        return layoutInflater.inflate(R.layout.test_ime, null).apply {
+            findViewById<Button>(R.id.button_close).setOnClickListener {
+                requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS)
+            }
+        }
+    }
+
+    override fun onEvaluateFullscreenMode(): Boolean {
+        return false
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
index 6b39067..dd2f206 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
@@ -21,6 +21,7 @@
 import androidx.recyclerview.widget.RecyclerView
 import androidx.window.sample.DisplayFeaturesConfigChangeActivity
 import androidx.window.sample.DisplayFeaturesNoConfigChangeActivity
+import androidx.window.sample.ImeActivity
 import androidx.window.sample.PresentationActivity
 import androidx.window.sample.R
 import androidx.window.sample.R.string.display_features_config_change
@@ -63,6 +64,11 @@
                 buttonTitle = getString(R.string.presentation),
                 description = getString(R.string.presentation_demo_description),
                 clazz = PresentationActivity::class.java
+            ),
+            DemoItem(
+                buttonTitle = getString(R.string.ime),
+                description = getString(R.string.ime_demo_description),
+                clazz = ImeActivity::class.java
             )
         )
         val recyclerView = findViewById<RecyclerView>(R.id.demo_recycler_view)
diff --git a/window/window-samples/src/main/res/layout/activity_ime.xml b/window/window-samples/src/main/res/layout/activity_ime.xml
new file mode 100644
index 0000000..5947c94
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/activity_ime.xml
@@ -0,0 +1,48 @@
+<?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.
+  -->
+
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_demo_reminder"/>
+
+    <Button
+        android:id="@+id/ime_button_settings"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_button_settings"/>
+
+    <Button
+        android:id="@+id/ime_button_switch_default"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_button_switch_default"/>
+
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="16dp"/>
+
+    <EditText
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:hint="@string/window_metrics_ime_hint"/>
+
+</androidx.appcompat.widget.LinearLayoutCompat>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/test_ime.xml b/window/window-samples/src/main/res/layout/test_ime.xml
new file mode 100644
index 0000000..feda1d7
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/test_ime.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Button
+        android:id="@+id/button_close"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/test_ime_button_close"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index 5faaae6..45e3de2 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -51,4 +51,12 @@
     <string name="occlusion_is_none">Occlusion is none</string>
     <string name="window_metrics">Window metrics</string>
     <string name="window_metrics_description">Demo of using WindowMetrics API with activity handling rotations.</string>
+    <string name="test_ime">Test IME</string>
+    <string name="test_ime_button_close">Close Test IME</string>
+    <string name="window_metrics_ime_hint">Tap to open IME</string>
+    <string name="ime">IME</string>
+    <string name="ime_demo_description">Demo of using various APIs from within IME.</string>
+    <string name="ime_demo_reminder">Reminder: To use the Test IME bundled with this application, remember to enable it in System Settings.</string>
+    <string name="ime_button_settings">System IME Settings</string>
+    <string name="ime_button_switch_default">Switch default IME</string>
 </resources>
diff --git a/window/window-samples/src/main/res/xml/method.xml b/window/window-samples/src/main/res/xml/method.xml
new file mode 100644
index 0000000..6d61dd9
--- /dev/null
+++ b/window/window-samples/src/main/res/xml/method.xml
@@ -0,0 +1,19 @@
+<?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.
+  -->
+
+<!-- Note that this file is required even if it contains nothing. -->
+<input-method>
+</input-method>
\ No newline at end of file
diff --git a/work/work-gcm/api/2.8.0-beta03.txt b/work/work-gcm/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-gcm/api/res-2.8.0-beta03.txt b/work/work-gcm/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-gcm/api/res-2.8.0-beta03.txt
diff --git a/work/work-gcm/api/restricted_2.8.0-beta03.txt b/work/work-gcm/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-multiprocess/api/2.8.0-beta03.txt b/work/work-multiprocess/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-multiprocess/api/res-2.8.0-beta03.txt b/work/work-multiprocess/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-multiprocess/api/res-2.8.0-beta03.txt
diff --git a/work/work-multiprocess/api/restricted_2.8.0-beta03.txt b/work/work-multiprocess/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/2.8.0-beta03.txt b/work/work-runtime-ktx/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/res-2.8.0-beta03.txt b/work/work-runtime-ktx/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime-ktx/api/res-2.8.0-beta03.txt
diff --git a/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt b/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime/api/2.8.0-beta03.txt b/work/work-runtime/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-runtime/api/res-2.8.0-beta03.txt b/work/work-runtime/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime/api/res-2.8.0-beta03.txt
diff --git a/work/work-runtime/api/restricted_2.8.0-beta03.txt b/work/work-runtime/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/2.8.0-beta03.txt b/work/work-rxjava2/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/res-2.8.0-beta03.txt b/work/work-rxjava2/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava2/api/res-2.8.0-beta03.txt
diff --git a/work/work-rxjava2/api/restricted_2.8.0-beta03.txt b/work/work-rxjava2/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/2.8.0-beta03.txt b/work/work-rxjava3/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/res-2.8.0-beta03.txt b/work/work-rxjava3/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava3/api/res-2.8.0-beta03.txt
diff --git a/work/work-rxjava3/api/restricted_2.8.0-beta03.txt b/work/work-rxjava3/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-testing/api/2.8.0-beta03.txt b/work/work-testing/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/work-testing/api/res-2.8.0-beta03.txt b/work/work-testing/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-testing/api/res-2.8.0-beta03.txt
diff --git a/work/work-testing/api/restricted_2.8.0-beta03.txt b/work/work-testing/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+