Merge "Stop placing text xml configs in distribution directory" into androidx-main
diff --git a/annotation/annotation-experimental/src/main/java/androidx/annotation/RequiresOptIn.kt b/annotation/annotation-experimental/src/main/java/androidx/annotation/RequiresOptIn.kt
index 345dab2..9cde13b 100644
--- a/annotation/annotation-experimental/src/main/java/androidx/annotation/RequiresOptIn.kt
+++ b/annotation/annotation-experimental/src/main/java/androidx/annotation/RequiresOptIn.kt
@@ -27,43 +27,47 @@
  * by using [OptIn] or by being annotated with that marker themselves, effectively causing
  * further propagation of that opt-in aspect.
  *
- * Marker example:
+ * ```
+ * // Marker definition
  *
- *     @Retention(CLASS)
- *     @Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
- *     @RequiresOptIn(level = Level.ERROR)
- *     public @interface ExperimentalDateTime {}
+ * @Retention(CLASS)
+ * @Target({TYPE, METHOD, CONSTRUCTOR, FIELD, PACKAGE})
+ * @RequiresOptIn(level = Level.ERROR)
+ * public @interface ExperimentalDateTime {}
  *
- *     @ExperimentalDateTime
- *     public class DateProvider {
- *       // ...
- *     }
+ * @ExperimentalDateTime
+ * public class DateProvider {
+ *   // ...
+ * }
  *
- * Client example:
+ * // Client code
  *
- *     int getYear() {
- *       DateProvider provider; // Error: DateProvider is experimental
- *       // ...
- *     }
+ * int getYear() {
+ *   DateProvider provider; // Error: DateProvider is experimental
+ *   // ...
+ * }
  *
- *     @ExperimentalDateTime
- *     Date getDate() {
- *       DateProvider provider; // OK: the function is marked as experimental
- *       // ...
- *     }
+ * @ExperimentalDateTime
+ * Date getDate() {
+ *   DateProvider provider; // OK: the function is marked as experimental
+ *   // ...
+ * }
  *
- *     void displayDate() {
- *       System.out.println(getDate()); // Error: getDate() is experimental, acceptance is required
- *     }
+ * void displayDate() {
+ *   System.out.println(getDate()); // Error: getDate() is experimental, acceptance is required
+ * }
+ * ```
  *
  * To configure project-wide opt-in, specify the `opt-in` option value in `lint.xml` as a
  * comma-delimited list of opted-in annotations:
  *
- *     <lint>
- *       <issue id="$issueId">
- *         <option name="opt-in" value="com.foo.ExperimentalBarAnnotation" />
- *       </issue>
- *     </lint>
+ * ```
+ * <lint>
+ *   <issue id="$issueId">
+ *     <option name="opt-in" value="com.foo.ExperimentalBarAnnotation" />
+ *   </issue>
+ * </lint>
+ * ```
  */
 @Retention(AnnotationRetention.BINARY)
 @Target(AnnotationTarget.ANNOTATION_CLASS)
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index 7f11e4d..ea61343 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -20,8 +20,8 @@
     implementation("androidx.core:core-ktx:1.8.0")
     implementation(libs.kotlinStdlib)
 
-    implementation("androidx.emoji2:emoji2:1.2.0-rc01")
-    implementation("androidx.emoji2:emoji2-views-helper:1.2.0-rc01")
+    implementation("androidx.emoji2:emoji2:1.2.0")
+    implementation("androidx.emoji2:emoji2-views-helper:1.2.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.cursoradapter:cursoradapter:1.0.0")
     api("androidx.activity:activity:1.6.0")
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 02e32f9..c9e4a8b 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -50,17 +50,6 @@
     androidTestImplementation(libs.protobufLite)
 }
 
-// Create a jar for use by appsearch-compiler:test
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-    project.tasks.create(name: "jar${suffix}", type: Jar) {
-        dependsOn variant.javaCompileProvider.get()
-        from variant.javaCompileProvider.get().destinationDirectory
-        destinationDirectory.set(new File(project.buildDir, "libJar"))
-    }
-}
-
 androidx {
     name = 'AndroidX AppSearch'
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/appsearch/compiler/build.gradle b/appsearch/compiler/build.gradle
index 486ae37..3aee826 100644
--- a/appsearch/compiler/build.gradle
+++ b/appsearch/compiler/build.gradle
@@ -21,6 +21,8 @@
     id('java-library')
 }
 
+androidx.enableAarAsJarForJvmTest()
+
 dependencies {
     api('androidx.annotation:annotation:1.1.0')
     api(libs.jsr250)
@@ -30,19 +32,10 @@
     implementation(libs.javapoet)
 
     // For testing, add in the compiled classes from appsearch to get access to annotations.
-    testImplementation fileTree(
-            dir: provider {
-                // Wrapping in a provider as a workaround as we access buildDir before this project is configured
-                // Replace with AGP API once it is added b/228109260
-                "${new File(project(":appsearch:appsearch").buildDir, "libJar")}"
-            },
-            include : "*.jar"
-    )
+    testAarAsJar(project(":appsearch:appsearch"))
     testImplementation(libs.googleCompileTesting)
 }
 
-tasks.findByName('compileJava').dependsOn(":appsearch:appsearch:jarRelease")
-
 androidx {
     name = 'AndroidX AppSearch Compiler'
     type = LibraryType.ANNOTATION_PROCESSOR
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/BuildJars.kt b/buildSrc-tests/src/test/kotlin/androidx/build/BuildJars.kt
index 2962064..78b604d 100644
--- a/buildSrc-tests/src/test/kotlin/androidx/build/BuildJars.kt
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/BuildJars.kt
@@ -17,7 +17,6 @@
 package androidx.build
 
 import java.io.File
-import org.gradle.api.GradleException
 
 /**
  * The self-built jars that we need to successfully apply the plugin
@@ -34,14 +33,6 @@
     private val jetpadIntegrationJar = findJar("jetpad-integration")
 
     fun classpathEntries(): String {
-        // b/239026887: Sometimes, we suspect, the jars are still being written when we run our
-        //              first test.  Hopefully, b/239066130 will allow us to drop these checks
-
-        waitForFileToExist(privateJar)
-        waitForFileToExist(pluginsJar)
-        waitForFileToExist(publicJar)
-        waitForFileToExist(jetpadIntegrationJar)
-
         return """|// Needed for androidx extension
                   |classpath(project.files("${privateJar.path}"))
                   |
@@ -55,19 +46,4 @@
                   |classpath(project.files("${jetpadIntegrationJar.path}"))
                   |""".trimMargin()
     }
-
-    private fun waitForFileToExist(
-        file: File,
-        millisToWait: Long = 5000,
-        waitStepMillis: Long = 50
-    ) {
-        val startStamp = System.currentTimeMillis()
-        val deadline = startStamp + millisToWait
-        while (!file.exists()) {
-            if (System.currentTimeMillis() > deadline) {
-                throw GradleException("${file.path} not found (even after $millisToWait ms)")
-            }
-            Thread.sleep(waitStepMillis)
-        }
-    }
 }
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index c12c696..ac3da1b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -17,6 +17,7 @@
 package androidx.build
 
 import androidx.build.checkapi.shouldConfigureApiTasks
+import androidx.build.jvmtest.configureAarAsJarForJvmTest
 import com.android.build.gradle.internal.crash.afterEvaluate
 import groovy.lang.Closure
 import org.gradle.api.GradleException
@@ -248,6 +249,10 @@
         return licenses
     }
 
+    fun enableAarAsJarForJvmTest() {
+        configureAarAsJarForJvmTest(project)
+    }
+
     companion object {
         const val DEFAULT_UNSPECIFIED_VERSION = "unspecified"
     }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
index 7675b95..ca38466 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -205,9 +205,6 @@
         // Broken in 7.0.0-alpha15 due to b/180408990
         disable.add("RestrictedApi")
 
-        // Broken in 7.0.0-alpha15 due to b/187508590
-        disable.add("InvalidPackage")
-
         // Reenable after upgradingto 7.1.0-beta01
         disable.add("SupportAnnotationUsage")
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 6d71deb..ea04292 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -686,7 +686,7 @@
  * Location of the library metadata JSON file that's used by Dackka, represented as a [RegularFile]
  */
 private fun getMetadataRegularFile(project: Project): Provider<RegularFile> =
-    project.layout.buildDirectory.file("SampleLibraryMetadata.json")
+    project.layout.buildDirectory.file("AndroidXLibraryMetadata.json")
 
 private const val DOCLAVA_DEPENDENCY = "com.android:doclava:1.0.6"
 
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/jvmtest/AarDependencyForJvmTest.kt b/buildSrc/private/src/main/kotlin/androidx/build/jvmtest/AarDependencyForJvmTest.kt
new file mode 100644
index 0000000..d9f45ec
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/jvmtest/AarDependencyForJvmTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.build.jvmtest
+
+import androidx.build.transform.ExtractClassesJarTransform
+import androidx.build.transform.IdentityTransform
+import com.android.build.api.attributes.BuildTypeAttr
+import org.gradle.api.Project
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.attributes.Usage
+
+/**
+ * Creates `testAarAsJar` configuration that can be used for JVM tests that need to Android library
+ * classes on the classpath.
+ */
+fun configureAarAsJarForJvmTest(project: Project) {
+    val testAarsAsJars = project.configurations.create("testAarAsJar") {
+        it.isTransitive = false
+        it.isCanBeConsumed = false
+        it.isCanBeResolved = true
+        it.attributes.attribute(
+            BuildTypeAttr.ATTRIBUTE,
+            project.objects.named(BuildTypeAttr::class.java, "release")
+        )
+        it.attributes.attribute(
+            Usage.USAGE_ATTRIBUTE,
+            project.objects.named(Usage::class.java, Usage.JAVA_API)
+        )
+    }
+    val artifactType = Attribute.of("artifactType", String::class.java)
+    project.dependencies.registerTransform(IdentityTransform::class.java) { spec ->
+        spec.from.attribute(artifactType, "jar")
+        spec.to.attribute(artifactType, "aarAsJar")
+    }
+
+    project.dependencies.registerTransform(ExtractClassesJarTransform::class.java) { spec ->
+        spec.from.attribute(artifactType, "aar")
+        spec.to.attribute(artifactType, "aarAsJar")
+    }
+
+    val aarAsJar = testAarsAsJars.incoming.artifactView { viewConfiguration ->
+        viewConfiguration.attributes.attribute(artifactType, "aarAsJar")
+    }.files
+    project.configurations.getByName("testImplementation").dependencies.add(
+        project.dependencies.create(aarAsJar)
+    )
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/transform/ExtractClassesJarTransform.kt b/buildSrc/private/src/main/kotlin/androidx/build/transform/ExtractClassesJarTransform.kt
new file mode 100644
index 0000000..cfeff81
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/transform/ExtractClassesJarTransform.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.build.transform
+
+import com.google.common.io.Files
+import java.util.zip.ZipInputStream
+import org.gradle.api.artifacts.transform.InputArtifact
+import org.gradle.api.artifacts.transform.TransformAction
+import org.gradle.api.artifacts.transform.TransformOutputs
+import org.gradle.api.artifacts.transform.TransformParameters
+import org.gradle.api.file.FileSystemLocation
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.work.DisableCachingByDefault
+
+@DisableCachingByDefault
+abstract class ExtractClassesJarTransform : TransformAction<TransformParameters.None> {
+    @get:PathSensitive(PathSensitivity.NAME_ONLY)
+    @get:InputArtifact
+    abstract val primaryInput: Provider<FileSystemLocation>
+
+    override fun transform(outputs: TransformOutputs) {
+        val inputFile = primaryInput.get().asFile
+        val outputFile = outputs.file("${inputFile.nameWithoutExtension}.jar")
+        ZipInputStream(inputFile.inputStream().buffered()).use { zipInputStream ->
+            while (true) {
+                val entry = zipInputStream.nextEntry ?: break
+                if (entry.name != "classes.jar") continue
+                Files.asByteSink(outputFile).writeFrom(zipInputStream)
+                break
+            }
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/transform/IdentityTransform.kt b/buildSrc/private/src/main/kotlin/androidx/build/transform/IdentityTransform.kt
new file mode 100644
index 0000000..d662963
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/transform/IdentityTransform.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.build.transform
+
+import org.gradle.api.artifacts.transform.InputArtifact
+import org.gradle.api.artifacts.transform.TransformAction
+import org.gradle.api.artifacts.transform.TransformOutputs
+import org.gradle.api.artifacts.transform.TransformParameters
+import org.gradle.api.file.FileSystemLocation
+import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.work.DisableCachingByDefault
+
+@DisableCachingByDefault
+abstract class IdentityTransform : TransformAction<TransformParameters.None> {
+    @get:PathSensitive(PathSensitivity.ABSOLUTE)
+    @get:InputArtifact
+    abstract val inputArtifact: Provider<FileSystemLocation>
+
+    override fun transform(transformOutputs: TransformOutputs) {
+        val input = inputArtifact.get().asFile
+        when {
+            input.isDirectory -> transformOutputs.dir(input)
+            input.isFile -> transformOutputs.file(input)
+            else -> throw IllegalArgumentException(
+                "File/directory does not exist: ${input.absolutePath}")
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 9a5cf7b..7f95975 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
 package androidx.camera.camera2.pipe.integration
 
 import android.content.Context
@@ -43,6 +45,7 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth
 import com.google.common.util.concurrent.ListenableFuture
+import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.asExecutor
@@ -56,9 +59,7 @@
 import org.junit.Test
 import org.junit.rules.TestRule
 import org.junit.runner.RunWith
-import java.util.concurrent.TimeUnit
 
-@RequiresApi(21)
 private const val DEFAULT_LENS_FACING_SELECTOR = CameraSelector.LENS_FACING_BACK
 
 @LargeTest
@@ -75,30 +76,28 @@
 
     private var cameraControl: CameraControlAdapter? = null
     private var camera: CameraUseCaseAdapter? = null
-    private val testDeferrableSurface = TestDeferrableSurface()
-    private val fakeUseCase = FakeTestUseCase(
-        FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig
-    ).apply {
-        setupSessionConfig(
-            SessionConfig.Builder().also { sessionConfigBuilder ->
-                sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
-                sessionConfigBuilder.addSurface(testDeferrableSurface)
-            }
-        )
-    }
+    private lateinit var testDeferrableSurface: TestDeferrableSurface
+    private lateinit var fakeUseCase: FakeTestUseCase
 
     @Before
     fun setUp() = runBlocking {
         Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING_SELECTOR))
+        testDeferrableSurface = TestDeferrableSurface()
+        fakeUseCase = FakeTestUseCase(
+            FakeUseCaseConfig.Builder().setTargetName("UseCase").useCaseConfig
+        ).apply {
+            setupSessionConfig(SessionConfig.Builder().also { sessionConfigBuilder ->
+                sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+                sessionConfigBuilder.addSurface(testDeferrableSurface)
+            })
+        }
 
         val context: Context = ApplicationProvider.getApplicationContext()
         CameraXUtil.initialize(
-            context,
-            CameraPipeConfig.defaultConfig()
+            context, CameraPipeConfig.defaultConfig()
         )
         camera = CameraUtil.createCameraUseCaseAdapter(
-            context,
-            CameraSelector.Builder().requireLensFacing(
+            context, CameraSelector.Builder().requireLensFacing(
                 DEFAULT_LENS_FACING_SELECTOR
             ).build()
         ).apply {
@@ -113,7 +112,9 @@
     @After
     fun tearDown() {
         camera?.detachUseCases()
-        testDeferrableSurface.close()
+        if (this::testDeferrableSurface.isInitialized) {
+            testDeferrableSurface.close()
+        }
         CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
     }
 
@@ -124,8 +125,7 @@
         val deferred = CompletableDeferred<CameraCaptureResult>()
         val tagKey = "TestTagBundleKey"
         val tagValue = "testing"
-        val captureConfig = CaptureConfig.Builder()
-            .apply {
+        val captureConfig = CaptureConfig.Builder().apply {
                 templateType = CameraDevice.TEMPLATE_PREVIEW
                 addTag(tagKey, tagValue)
                 addSurface(testDeferrableSurface)
@@ -158,39 +158,36 @@
             }!!.tagBundle.getTag(tagKey)
         ).isEqualTo(tagValue)
     }
-}
 
-@RequiresApi(21)
-private class FakeTestUseCase(
-    config: FakeUseCaseConfig,
-) : FakeUseCase(config) {
+    private class FakeTestUseCase(
+        config: FakeUseCaseConfig,
+    ) : FakeUseCase(config) {
 
-    fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
-        updateSessionConfig(sessionConfigBuilder.build())
-        notifyActive()
-    }
-}
-
-@RequiresApi(21)
-private class TestDeferrableSurface : DeferrableSurface() {
-    init {
-        terminationFuture.addListener(
-            { cleanUp() },
-            Dispatchers.IO.asExecutor()
-        )
+        fun setupSessionConfig(sessionConfigBuilder: SessionConfig.Builder) {
+            updateSessionConfig(sessionConfigBuilder.build())
+            notifyActive()
+        }
     }
 
-    private val surfaceTexture = SurfaceTexture(0).also {
-        it.setDefaultBufferSize(640, 480)
-    }
-    val testSurface = Surface(surfaceTexture)
+    private class TestDeferrableSurface : DeferrableSurface() {
+        init {
+            terminationFuture.addListener(
+                { cleanUp() }, Dispatchers.IO.asExecutor()
+            )
+        }
 
-    override fun provideSurface(): ListenableFuture<Surface> {
-        return Futures.immediateFuture(testSurface)
-    }
+        private val surfaceTexture = SurfaceTexture(0).also {
+            it.setDefaultBufferSize(640, 480)
+        }
+        val testSurface = Surface(surfaceTexture)
 
-    fun cleanUp() {
-        testSurface.release()
-        surfaceTexture.release()
+        override fun provideSurface(): ListenableFuture<Surface> {
+            return Futures.immediateFuture(testSurface)
+        }
+
+        fun cleanUp() {
+            testSurface.release()
+            surfaceTexture.release()
+        }
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
index 1463c47..38dc5a0 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/UseCaseSurfaceManagerDeviceTest.kt
@@ -46,7 +46,6 @@
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.IdlingResource
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
@@ -149,7 +148,6 @@
         assertThat(cameraClosedUsageCount).isEqualTo(0)
     }
 
-    @FlakyTest(bugId = 246598371)
     @Test
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.M)
     fun disconnectOpenedCameraGraph_deferrableSurfaceUsageCountTest() = runBlocking {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 35f2468..69710ad 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -37,6 +37,7 @@
 import androidx.camera.core.impl.CameraCaptureCallback
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
@@ -112,6 +113,11 @@
         return camcorderProfileProviderAdapter
     }
 
+    override fun getTimebase(): Timebase {
+        Log.warn { "TODO: getTimebase are not yet supported." }
+        return Timebase.UPTIME
+    }
+
     override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
 
     override fun getCameraQuirks(): Quirks {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 68252bd2..956ff2f 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -205,7 +205,8 @@
     }
 
     private fun shouldAddRepeatingUseCase(runningUseCases: Set<UseCase>): Boolean {
-        return runningUseCases.only { it is ImageCapture }
+        return !attachedUseCases.contains(meteringRepeating) &&
+            runningUseCases.only { it is ImageCapture }
     }
 
     private fun addRepeatingUseCase() {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/CaptureRequestOptions.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/CaptureRequestOptions.kt
index 071ba05..05cda0a 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/CaptureRequestOptions.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/interop/CaptureRequestOptions.kt
@@ -42,7 +42,7 @@
      * Returns a value for the given [CaptureRequest.Key] or null if it hasn't been set.
      *
      * @param key            The key to retrieve.
-     * @param <ValueT>       The type of the value.
+     * @param ValueT         The type of the value.
      * @return The stored value or null if the value does not exist in this
      * configuration.
      */
@@ -58,7 +58,7 @@
      *
      * @param key            The key to retrieve.
      * @param valueIfMissing The value to return if this configuration option has not been set.
-     * @param <ValueT>       The type of the value.
+     * @param ValueT         The type of the value.
      * @return The stored value or `valueIfMissing` if the value does not exist in this
      * configuration.
      *
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 445fdfe..cb4f1cd 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -17,6 +17,8 @@
 package androidx.camera.camera2.internal;
 
 import static android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING;
+import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME;
+import static android.hardware.camera2.CameraMetadata.SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN;
 
 import static androidx.camera.camera2.internal.ZslUtil.isCapabilitySupported;
 
@@ -49,6 +51,7 @@
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
 import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LiveData;
@@ -371,6 +374,21 @@
         return mCamera2CamcorderProfileProvider;
     }
 
+    @NonNull
+    @Override
+    public Timebase getTimebase() {
+        Integer timeSource = mCameraCharacteristicsCompat.get(
+                CameraCharacteristics.SENSOR_INFO_TIMESTAMP_SOURCE);
+        Preconditions.checkNotNull(timeSource);
+        switch (timeSource) {
+            case SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME:
+                return Timebase.REALTIME;
+            case SENSOR_INFO_TIMESTAMP_SOURCE_UNKNOWN:
+            default:
+                return Timebase.UPTIME;
+        }
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index d35e1d0..3f580a9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -76,6 +76,10 @@
     @NonNull
     CamcorderProfileProvider getCamcorderProfileProvider();
 
+    /** Returns the {@link Timebase} of frame output by this camera. */
+    @NonNull
+    Timebase getTimebase();
+
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/Timebase.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/Timebase.java
new file mode 100644
index 0000000..f22a26d
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/Timebase.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 androidx.camera.core.impl;
+
+import android.os.SystemClock;
+
+import androidx.annotation.RequiresApi;
+
+/**
+ * The time base enum.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public enum Timebase {
+    /**
+     * System time since boot, not counting time spent in deep sleep.
+     *
+     * @see SystemClock#uptimeMillis()
+     * @see System#nanoTime()
+     */
+    UPTIME,
+
+    /**
+     * System time since boot, including time spent in sleep.
+     *
+     * @see SystemClock#elapsedRealtime()
+     * @see SystemClock#elapsedRealtimeNanos()
+     */
+    REALTIME,
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
index f649315..75679d4 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/TransformUtils.java
@@ -26,6 +26,8 @@
 import androidx.annotation.RequiresApi;
 import androidx.core.util.Preconditions;
 
+import java.util.Locale;
+
 /**
  * Utility class for transform.
  *
@@ -53,6 +55,11 @@
         return new Size(rect.width(), rect.height());
     }
 
+    /** Returns a formatted string for a Rect. */
+    @NonNull
+    public static String rectToString(@NonNull Rect rect) {
+        return String.format(Locale.US, "%s(%dx%d)", rect, rect.width(), rect.height());
+    }
 
     /**
      * Transforms size to a {@link Rect} with zero left and top.
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 8d5d98d..c2dba9a 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -35,6 +35,7 @@
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue;
 import androidx.camera.core.impl.Quirk;
 import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.internal.ImmutableZoomState;
 import androidx.core.util.Preconditions;
@@ -71,6 +72,8 @@
     @NonNull
     private final List<Quirk> mCameraQuirks = new ArrayList<>();
 
+    private Timebase mTimebase = Timebase.UPTIME;
+
     public FakeCameraInfoInternal() {
         this(/*sensorRotation=*/ 0, /*lensFacing=*/ CameraSelector.LENS_FACING_BACK);
     }
@@ -169,6 +172,12 @@
                 mCamcorderProfileProvider;
     }
 
+    @NonNull
+    @Override
+    public Timebase getTimebase() {
+        return mTimebase;
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
@@ -220,6 +229,11 @@
         mCamcorderProfileProvider = Preconditions.checkNotNull(camcorderProfileProvider);
     }
 
+    /** Set the timebase for testing */
+    public void setTimebase(@NonNull Timebase timebase) {
+        mTimebase = timebase;
+    }
+
     /** Set the isPrivateReprocessingSupported flag for testing */
     public void setPrivateReprocessingSupported(boolean supported) {
         mIsPrivateReprocessingSupported = supported;
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolverTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolverTest.kt
index c9f6998..e7834a3 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolverTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolverTest.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.Timebase
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -62,6 +63,7 @@
     private val context: Context = ApplicationProvider.getApplicationContext()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
     private val defaultAudioSpec = AudioSpec.builder().build()
+    private val timebase = Timebase.UPTIME
 
     private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
     private lateinit var videoCapabilities: VideoCapabilities
@@ -103,6 +105,7 @@
             val config = AudioEncoderConfigCamcorderProfileResolver(
                 it.audioCodecMimeType!!,
                 it.requiredAudioProfile,
+                timebase,
                 defaultAudioSpec,
                 sourceSettings,
                 it
@@ -125,6 +128,7 @@
             AudioEncoderConfigCamcorderProfileResolver(
                 profile.audioCodecMimeType!!,
                 profile.requiredAudioProfile,
+                timebase,
                 defaultAudioSpec,
                 defaultSourceSettings,
                 profile
@@ -137,6 +141,7 @@
         val higherChannelCountConfig = AudioEncoderConfigCamcorderProfileResolver(
             profile.audioCodecMimeType!!,
             profile.requiredAudioProfile,
+            timebase,
             defaultAudioSpec,
             higherChannelCountSourceSettings,
             profile
@@ -155,6 +160,7 @@
             AudioEncoderConfigCamcorderProfileResolver(
                 profile.audioCodecMimeType!!,
                 profile.requiredAudioProfile,
+                timebase,
                 defaultAudioSpec,
                 defaultSourceSettings,
                 profile
@@ -167,6 +173,7 @@
         val higherSampleRateConfig = AudioEncoderConfigCamcorderProfileResolver(
             profile.audioCodecMimeType!!,
             profile.requiredAudioProfile,
+            timebase,
             defaultAudioSpec,
             higherSampleRateSourceSettings,
             profile
@@ -196,6 +203,7 @@
             AudioEncoderConfigCamcorderProfileResolver(
                 profile.audioCodecMimeType!!,
                 profile.requiredAudioProfile,
+                timebase,
                 higherAudioSpec,
                 defaultSourceSettings,
                 profile
@@ -206,6 +214,7 @@
             AudioEncoderConfigCamcorderProfileResolver(
                 profile.audioCodecMimeType!!,
                 profile.requiredAudioProfile,
+                timebase,
                 lowerAudioSpec,
                 defaultSourceSettings,
                 profile
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolverTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolverTest.kt
index 4545f43..36a483a 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolverTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolverTest.kt
@@ -18,6 +18,7 @@
 
 import android.media.MediaCodecInfo
 import android.util.Range
+import androidx.camera.core.impl.Timebase
 import androidx.camera.video.AudioSpec
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
@@ -34,6 +35,7 @@
     companion object {
         const val MIME_TYPE = "audio/mp4a-latm"
         const val ENCODER_PROFILE = MediaCodecInfo.CodecProfileLevel.AACObjectLC
+        val TIMEBASE = Timebase.UPTIME
     }
 
     private val defaultAudioSpec = AudioSpec.builder().build()
@@ -45,6 +47,7 @@
         val resolvedAudioConfig = AudioEncoderConfigDefaultResolver(
             MIME_TYPE,
             ENCODER_PROFILE,
+            TIMEBASE,
             defaultAudioSpec,
             defaultAudioSourceSettings
         ).get()
@@ -64,6 +67,7 @@
             AudioEncoderConfigDefaultResolver(
                 MIME_TYPE,
                 ENCODER_PROFILE,
+                TIMEBASE,
                 defaultAudioSpec,
                 defaultAudioSourceSettings
             ).get()
@@ -75,6 +79,7 @@
         val higherChannelCountConfig = AudioEncoderConfigDefaultResolver(
             MIME_TYPE,
             ENCODER_PROFILE,
+            TIMEBASE,
             defaultAudioSpec,
             higherChannelCountSourceSettings
         ).get()
@@ -89,6 +94,7 @@
             AudioEncoderConfigDefaultResolver(
                 MIME_TYPE,
                 ENCODER_PROFILE,
+                TIMEBASE,
                 defaultAudioSpec,
                 defaultAudioSourceSettings
             ).get()
@@ -100,6 +106,7 @@
         val higherSampleRateConfig = AudioEncoderConfigDefaultResolver(
             MIME_TYPE,
             ENCODER_PROFILE,
+            TIMEBASE,
             defaultAudioSpec,
             higherSampleRateSourceSettings
         ).get()
@@ -113,6 +120,7 @@
             AudioEncoderConfigDefaultResolver(
                 MIME_TYPE,
                 ENCODER_PROFILE,
+                TIMEBASE,
                 defaultAudioSpec,
                 defaultAudioSourceSettings
             ).get()
@@ -131,6 +139,7 @@
             AudioEncoderConfigDefaultResolver(
                 MIME_TYPE,
                 ENCODER_PROFILE,
+                TIMEBASE,
                 higherAudioSpec,
                 defaultAudioSourceSettings
             ).get().bitrate
@@ -140,6 +149,7 @@
             AudioEncoderConfigDefaultResolver(
                 MIME_TYPE,
                 ENCODER_PROFILE,
+                TIMEBASE,
                 lowerAudioSpec,
                 defaultAudioSourceSettings
             ).get().bitrate
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolverTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolverTest.kt
index bbd1c21..e38ddf6 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolverTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolverTest.kt
@@ -23,6 +23,7 @@
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.Timebase
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -61,6 +62,7 @@
     private val context: Context = ApplicationProvider.getApplicationContext()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
     private val defaultVideoSpec = VideoSpec.builder().build()
+    private val timebase = Timebase.UPTIME
 
     private lateinit var cameraUseCaseAdapter: CameraUseCaseAdapter
     private lateinit var videoCapabilities: VideoCapabilities
@@ -99,6 +101,7 @@
         supportedProfiles.forEach {
             val config = VideoEncoderConfigCamcorderProfileResolver(
                 it.videoCodecMimeType!!,
+                timebase,
                 defaultVideoSpec,
                 Size(it.videoFrameWidth, it.videoFrameHeight),
                 it,
@@ -119,6 +122,7 @@
 
         val defaultBitrate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
@@ -131,6 +135,7 @@
         assertThat(
             VideoEncoderConfigCamcorderProfileResolver(
                 profile.videoCodecMimeType!!,
+                timebase,
                 defaultVideoSpec,
                 increasedSurfaceSize,
                 profile,
@@ -141,6 +146,7 @@
         assertThat(
             VideoEncoderConfigCamcorderProfileResolver(
                 profile.videoCodecMimeType!!,
+                timebase,
                 defaultVideoSpec,
                 decreasedSurfaceSize,
                 profile,
@@ -156,6 +162,7 @@
 
         val defaultBitrate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
@@ -174,6 +181,7 @@
         assertThat(
             VideoEncoderConfigCamcorderProfileResolver(
                 profile.videoCodecMimeType!!,
+                timebase,
                 higherVideoSpec,
                 surfaceSize,
                 profile,
@@ -184,6 +192,7 @@
         assertThat(
             VideoEncoderConfigCamcorderProfileResolver(
                 profile.videoCodecMimeType!!,
+                timebase,
                 lowerVideoSpec,
                 surfaceSize,
                 profile,
@@ -203,6 +212,7 @@
 
         val clampedDownFrameRate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
@@ -211,6 +221,7 @@
 
         val clampedUpFrameRate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
@@ -231,6 +242,7 @@
 
         val resolvedFrameRate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
@@ -251,6 +263,7 @@
 
         val resolvedBitrate = VideoEncoderConfigCamcorderProfileResolver(
             profile.videoCodecMimeType!!,
+            timebase,
             defaultVideoSpec,
             surfaceSize,
             profile,
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolverTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolverTest.kt
index b9be879..066adfd 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolverTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolverTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.video.internal.config
 
 import android.util.Range
+import androidx.camera.core.impl.Timebase
 import androidx.camera.testing.CamcorderProfileUtil
 import androidx.camera.video.VideoSpec
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -33,7 +34,7 @@
 
     companion object {
         const val MIME_TYPE = "video/avc"
-
+        val TIMEBASE = Timebase.UPTIME
         const val FRAME_RATE_30 = 30
         const val FRAME_RATE_45 = 45
         const val FRAME_RATE_60 = 60
@@ -52,6 +53,7 @@
         val configSupplierCif =
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 defaultVideoSpec,
                 surfaceSizeCif,
                 expectedFrameRateRange
@@ -59,6 +61,7 @@
         val configSupplier720p =
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 defaultVideoSpec,
                 surfaceSize720p,
                 expectedFrameRateRange
@@ -66,6 +69,7 @@
         val configSupplier1080p =
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 defaultVideoSpec,
                 surfaceSize1080p,
                 expectedFrameRateRange
@@ -98,6 +102,7 @@
         val defaultConfig =
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 defaultVideoSpec,
                 surfaceSize720p,
                 /*expectedFrameRateRange=*/null
@@ -116,6 +121,7 @@
         assertThat(
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 higherVideoSpec,
                 surfaceSize720p,
                 /*expectedFrameRateRange=*/null
@@ -125,6 +131,7 @@
         assertThat(
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 lowerVideoSpec,
                 surfaceSize720p,
                 /*expectedFrameRateRange=*/null
@@ -142,6 +149,7 @@
         assertThat(
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 videoSpec,
                 size,
                 /*expectedFrameRateRange=*/null
@@ -164,6 +172,7 @@
         assertThat(
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 videoSpec,
                 size,
                 expectedFrameRateRange
@@ -188,6 +197,7 @@
         assertThat(
             VideoEncoderConfigDefaultResolver(
                 MIME_TYPE,
+                TIMEBASE,
                 videoSpec,
                 size,
                 expectedFrameRateRange
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImplTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImplTest.kt
new file mode 100644
index 0000000..55508d5
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImplTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.video.internal.encoder
+
+import android.media.MediaCodecInfo
+import androidx.camera.core.impl.Timebase
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class AudioEncoderInfoImplTest {
+
+    companion object {
+        private const val MIME_TYPE = "audio/mp4a-latm"
+        private const val ENCODER_PROFILE = MediaCodecInfo.CodecProfileLevel.AACObjectLC
+        private const val BIT_RATE = 64000
+        private const val SAMPLE_RATE = 44100
+        private const val CHANNEL_COUNT = 1
+        private val TIMEBASE = Timebase.UPTIME
+    }
+
+    private lateinit var encoderConfig: AudioEncoderConfig
+
+    @Before
+    fun setup() {
+        encoderConfig = AudioEncoderConfig.builder()
+            .setMimeType(MIME_TYPE)
+            .setInputTimebase(TIMEBASE)
+            .setProfile(ENCODER_PROFILE)
+            .setBitrate(BIT_RATE)
+            .setSampleRate(SAMPLE_RATE)
+            .setChannelCount(CHANNEL_COUNT)
+            .build()
+    }
+
+    @Test
+    fun canCreateEncoderInfoFromConfig() {
+        // No exception is thrown
+        AudioEncoderInfoImpl.from(encoderConfig)
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
index ed8ef9f..bf7c596 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/AudioEncoderTest.kt
@@ -17,6 +17,7 @@
 
 import android.media.MediaCodecInfo
 import androidx.camera.core.impl.Observable.Observer
+import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.video.internal.BufferProvider
 import androidx.camera.video.internal.BufferProvider.State
@@ -60,6 +61,7 @@
     companion object {
         private const val MIME_TYPE = "audio/mp4a-latm"
         private const val ENCODER_PROFILE = MediaCodecInfo.CodecProfileLevel.AACObjectLC
+        private val INPUT_TIMEBASE = Timebase.UPTIME
         private const val BIT_RATE = 64000
         private const val SAMPLE_RATE = 44100
         private const val CHANNEL_COUNT = 1
@@ -83,6 +85,7 @@
             AudioEncoderConfig.builder()
                 .setMimeType(MIME_TYPE)
                 .setProfile(ENCODER_PROFILE)
+                .setInputTimebase(INPUT_TIMEBASE)
                 .setBitrate(BIT_RATE)
                 .setSampleRate(SAMPLE_RATE)
                 .setChannelCount(CHANNEL_COUNT)
@@ -106,6 +109,11 @@
     }
 
     @Test
+    fun canGetEncoderInfo() {
+        assertThat(encoder.encoderInfo).isNotNull()
+    }
+
+    @Test
     fun discardInputBufferBeforeStart() {
         // Arrange.
         fakeAudioLoop.start()
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImplTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImplTest.kt
new file mode 100644
index 0000000..66784db
--- /dev/null
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImplTest.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.video.internal.encoder
+
+import android.media.MediaCodecInfo
+import android.media.MediaFormat
+import android.util.Size
+import androidx.camera.core.impl.Timebase
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class VideoEncoderInfoImplTest {
+
+    companion object {
+        private const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC
+        private const val BIT_RATE = 10 * 1024 * 1024 // 10M
+        private const val FRAME_RATE = 30
+        private const val I_FRAME_INTERVAL = 1
+        private const val WIDTH = 640
+        private const val HEIGHT = 480
+        private val TIMEBASE = Timebase.UPTIME
+    }
+
+    private lateinit var encoderConfig: VideoEncoderConfig
+
+    @Before
+    fun setup() {
+        encoderConfig = VideoEncoderConfig.builder()
+            .setBitrate(BIT_RATE)
+            .setColorFormat(MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
+            .setFrameRate(FRAME_RATE)
+            .setIFrameInterval(I_FRAME_INTERVAL)
+            .setMimeType(MIME_TYPE)
+            .setResolution(Size(WIDTH, HEIGHT))
+            .setInputTimebase(TIMEBASE)
+            .build()
+    }
+
+    @Test
+    fun canCreateEncoderInfoFromConfig() {
+        // No exception is thrown
+        VideoEncoderInfoImpl.from(encoderConfig)
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
index d20a2f9..0bcc6f3 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/encoder/VideoEncoderTest.kt
@@ -33,6 +33,7 @@
 import androidx.camera.core.Preview.SurfaceProvider
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.Timebase
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
@@ -99,6 +100,8 @@
             arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
             arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
         )
+
+        private val INPUT_TIMEBASE = Timebase.UPTIME
     }
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
@@ -178,6 +181,11 @@
     }
 
     @Test
+    fun canGetEncoderInfo() {
+        assertThat(videoEncoder.encoderInfo).isNotNull()
+    }
+
+    @Test
     fun canRestartVideoEncoder() {
         // Arrange.
         videoEncoder.start()
@@ -350,6 +358,7 @@
         assumeTrue(resolution != null)
 
         videoEncoderConfig = VideoEncoderConfig.builder()
+            .setInputTimebase(INPUT_TIMEBASE)
             .setBitrate(BIT_RATE)
             .setColorFormat(MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
             .setFrameRate(FRAME_RATE)
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/workaround/EncoderFinderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/workaround/EncoderFinderTest.kt
index dd4c463..751a221 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/workaround/EncoderFinderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/workaround/EncoderFinderTest.kt
@@ -24,6 +24,7 @@
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.Timebase
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.LabTestRule
@@ -71,6 +72,9 @@
             arrayOf(CameraSelector.DEFAULT_BACK_CAMERA, CameraSelector.DEFAULT_FRONT_CAMERA)
 
         @JvmStatic
+        private val timebase = Timebase.UPTIME
+
+        @JvmStatic
         private val quality = arrayOf(
             Quality.SD,
             Quality.HD,
@@ -127,6 +131,7 @@
 
         val mediaFormat = VideoEncoderConfigCamcorderProfileResolver(
             camcorderProfileVideoMime,
+            timebase,
             videoSpec,
             resolution!!,
             camcorderProfile,
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java b/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
index aedcfc7..fb5f735 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/MediaSpec.java
@@ -66,8 +66,10 @@
     public @interface OutputFormat {
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
     @NonNull
-    static String outputFormatToAudioMime(@OutputFormat int outputFormat) {
+    public static String outputFormatToAudioMime(@OutputFormat int outputFormat) {
         switch (outputFormat) {
             case MediaSpec.OUTPUT_FORMAT_WEBM:
                 return AUDIO_ENCODER_MIME_WEBM_DEFAULT;
@@ -80,7 +82,9 @@
         }
     }
 
-    static int outputFormatToAudioProfile(@OutputFormat int outputFormat) {
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
+    public static int outputFormatToAudioProfile(@OutputFormat int outputFormat) {
         String audioMime = outputFormatToAudioMime(outputFormat);
         if (Objects.equals(audioMime, MediaFormat.MIMETYPE_AUDIO_AAC)) {
             return AAC_DEFAULT_PROFILE;
@@ -89,8 +93,10 @@
         return EncoderConfig.CODEC_PROFILE_NONE;
     }
 
+    /** @hide */
+    @RestrictTo(Scope.LIBRARY)
     @NonNull
-    static String outputFormatToVideoMime(@OutputFormat int outputFormat) {
+    public static String outputFormatToVideoMime(@OutputFormat int outputFormat) {
         switch (outputFormat) {
             case MediaSpec.OUTPUT_FORMAT_WEBM:
                 return VIDEO_ENCODER_MIME_WEBM_DEFAULT;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index 5a2bde4..d85ba2e 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -26,6 +26,11 @@
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_UNKNOWN;
 import static androidx.camera.video.VideoRecordEvent.Finalize.VideoRecordError;
 import static androidx.camera.video.internal.DebugUtils.readableUs;
+import static androidx.camera.video.internal.config.AudioConfigUtil.resolveAudioEncoderConfig;
+import static androidx.camera.video.internal.config.AudioConfigUtil.resolveAudioMimeInfo;
+import static androidx.camera.video.internal.config.AudioConfigUtil.resolveAudioSourceSettings;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoEncoderConfig;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoMimeInfo;
 
 import android.Manifest;
 import android.annotation.SuppressLint;
@@ -40,7 +45,6 @@
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
 import android.util.Pair;
-import android.util.Range;
 import android.util.Size;
 import android.view.Surface;
 
@@ -58,6 +62,7 @@
 import androidx.camera.core.impl.MutableStateObservable;
 import androidx.camera.core.impl.Observable;
 import androidx.camera.core.impl.StateObservable;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.annotation.ExecutedBy;
 import androidx.camera.core.impl.utils.CloseGuardHelper;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
@@ -73,13 +78,7 @@
 import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
-import androidx.camera.video.internal.config.AudioEncoderConfigCamcorderProfileResolver;
-import androidx.camera.video.internal.config.AudioEncoderConfigDefaultResolver;
-import androidx.camera.video.internal.config.AudioSourceSettingsCamcorderProfileResolver;
-import androidx.camera.video.internal.config.AudioSourceSettingsDefaultResolver;
 import androidx.camera.video.internal.config.MimeInfo;
-import androidx.camera.video.internal.config.VideoEncoderConfigCamcorderProfileResolver;
-import androidx.camera.video.internal.config.VideoEncoderConfigDefaultResolver;
 import androidx.camera.video.internal.encoder.AudioEncoderConfig;
 import androidx.camera.video.internal.encoder.BufferCopiedEncodedData;
 import androidx.camera.video.internal.encoder.EncodeException;
@@ -96,7 +95,6 @@
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.util.Consumer;
 import androidx.core.util.Preconditions;
-import androidx.core.util.Supplier;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -344,6 +342,8 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     SurfaceRequest mSurfaceRequest;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+    Timebase mVideoSourceTimebase;
+    @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     Surface mLatestSurface = null;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     Surface mActiveSurface = null;
@@ -416,6 +416,13 @@
 
     @Override
     public void onSurfaceRequested(@NonNull SurfaceRequest request) {
+        onSurfaceRequested(request, Timebase.UPTIME);
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @Override
+    public void onSurfaceRequested(@NonNull SurfaceRequest request, @NonNull Timebase timebase) {
         synchronized (mLock) {
             Logger.d(TAG, "Surface is requested in state: " + mState + ", Current surface: "
                     + mStreamId);
@@ -430,7 +437,8 @@
                     // Fall-through
                 case INITIALIZING:
                     mSequentialExecutor.execute(
-                            () -> initializeInternal(mSurfaceRequest = request));
+                            () -> initializeInternal(mSurfaceRequest = request,
+                                    mVideoSourceTimebase = timebase));
                     break;
                 case IDLING:
                     // Fall-through
@@ -447,7 +455,8 @@
                             // If the surface request is already complete, this is a no-op.
                             mSurfaceRequest.willNotProvideSurface();
                         }
-                        initializeInternal(mSurfaceRequest = request);
+                        initializeInternal(mSurfaceRequest = request,
+                                mVideoSourceTimebase = timebase);
                     });
                     break;
             }
@@ -698,7 +707,7 @@
                                             "surface request is required to retry "
                                                     + "initialization.");
                                 }
-                                initializeInternal(mSurfaceRequest);
+                                initializeInternal(mSurfaceRequest, mVideoSourceTimebase);
                             });
                         } else {
                             setState(State.PENDING_RECORDING);
@@ -989,7 +998,8 @@
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void initializeInternal(@NonNull SurfaceRequest surfaceRequest) {
+    private void initializeInternal(@NonNull SurfaceRequest surfaceRequest,
+            @NonNull Timebase videoSourceTimebase) {
         if (mLatestSurface != null) {
             // There's a valid surface. Provide it directly.
             mActiveSurface = mLatestSurface;
@@ -1004,18 +1014,9 @@
             // Fetch and cache nearest camcorder profile, if one exists.
             VideoCapabilities capabilities =
                     VideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
-            Quality highestSupportedQuality =
-                    capabilities.findHighestSupportedQualityFor(surfaceSize);
-            Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
-                    + " for surface size " + surfaceSize);
-            if (highestSupportedQuality != Quality.NONE) {
-                mResolvedCamcorderProfile = capabilities.getProfile(highestSupportedQuality);
-                if (mResolvedCamcorderProfile == null) {
-                    throw new AssertionError("Camera advertised available quality but did not "
-                            + "produce CamcorderProfile for advertised quality.");
-                }
-            }
-            setupVideo(surfaceRequest);
+            mResolvedCamcorderProfile = capabilities.findHighestSupportedCamcorderProfileFor(
+                    surfaceSize);
+            setupVideo(surfaceRequest, videoSourceTimebase);
         }
     }
 
@@ -1105,152 +1106,6 @@
         return activeRecording.getRecordingId() == recordingRecord.getRecordingId();
     }
 
-    @ExecutedBy("mSequentialExecutor")
-    @NonNull
-    private MimeInfo resolveAudioMimeInfo(@NonNull MediaSpec mediaSpec) {
-        String mediaSpecAudioMime = MediaSpec.outputFormatToAudioMime(mediaSpec.getOutputFormat());
-        int mediaSpecAudioProfile =
-                MediaSpec.outputFormatToAudioProfile(mediaSpec.getOutputFormat());
-        String resolvedAudioMime = mediaSpecAudioMime;
-        int resolvedAudioProfile = mediaSpecAudioProfile;
-        boolean camcorderProfileIsCompatible = false;
-        if (mResolvedCamcorderProfile != null) {
-            String camcorderProfileAudioMime = mResolvedCamcorderProfile.getAudioCodecMimeType();
-            int camcorderProfileAudioProfile = mResolvedCamcorderProfile.getRequiredAudioProfile();
-
-            if (camcorderProfileAudioMime == null) {
-                Logger.d(TAG, "CamcorderProfile contains undefined AUDIO mime type so cannot be "
-                        + "used. May rely on fallback defaults to derive settings [chosen mime "
-                        + "type: "
-                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
-            } else if (mediaSpec.getOutputFormat() == MediaSpec.OUTPUT_FORMAT_AUTO) {
-                camcorderProfileIsCompatible = true;
-                resolvedAudioMime = camcorderProfileAudioMime;
-                resolvedAudioProfile = camcorderProfileAudioProfile;
-                Logger.d(TAG, "MediaSpec contains OUTPUT_FORMAT_AUTO. Using CamcorderProfile "
-                        + "to derive AUDIO settings [mime type: "
-                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
-            } else if (Objects.equals(mediaSpecAudioMime, camcorderProfileAudioMime)
-                    && mediaSpecAudioProfile == camcorderProfileAudioProfile) {
-                camcorderProfileIsCompatible = true;
-                resolvedAudioMime = camcorderProfileAudioMime;
-                resolvedAudioProfile = camcorderProfileAudioProfile;
-                Logger.d(TAG, "MediaSpec audio mime/profile matches CamcorderProfile. "
-                        + "Using CamcorderProfile to derive AUDIO settings [mime type: "
-                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
-            } else {
-                Logger.d(TAG, "MediaSpec audio mime or profile does not match CamcorderProfile, so "
-                        + "CamcorderProfile settings cannot be used. May rely on fallback "
-                        + "defaults to derive AUDIO settings [CamcorderProfile mime type: "
-                        + camcorderProfileAudioMime + "(profile: " + camcorderProfileAudioProfile
-                        + "), chosen mime type: "
-                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
-            }
-        }
-
-        MimeInfo.Builder mimeInfoBuilder = MimeInfo.builder(resolvedAudioMime)
-                .setProfile(resolvedAudioProfile);
-        if (camcorderProfileIsCompatible) {
-            mimeInfoBuilder.setCompatibleCamcorderProfile(mResolvedCamcorderProfile);
-        }
-
-        return mimeInfoBuilder.build();
-    }
-
-    @ExecutedBy("mSequentialExecutor")
-    @NonNull
-    private MimeInfo resolveVideoMimeInfo(@NonNull MediaSpec mediaSpec) {
-        String mediaSpecVideoMime = MediaSpec.outputFormatToVideoMime(mediaSpec.getOutputFormat());
-        String resolvedVideoMime = mediaSpecVideoMime;
-        boolean camcorderProfileIsCompatible = false;
-        if (mResolvedCamcorderProfile != null) {
-            String camcorderProfileVideoMime = mResolvedCamcorderProfile.getVideoCodecMimeType();
-            // Use camcorder profile settings if the media spec's output format
-            // is set to auto or happens to match the CamcorderProfile's output format.
-            if (camcorderProfileVideoMime == null) {
-                Logger.d(TAG, "CamcorderProfile contains undefined VIDEO mime type so cannot be "
-                        + "used. May rely on fallback defaults to derive settings [chosen mime "
-                        + "type: " + resolvedVideoMime + "]");
-            } else if (mediaSpec.getOutputFormat() == MediaSpec.OUTPUT_FORMAT_AUTO) {
-                camcorderProfileIsCompatible = true;
-                resolvedVideoMime = camcorderProfileVideoMime;
-                Logger.d(TAG, "MediaSpec contains OUTPUT_FORMAT_AUTO. Using CamcorderProfile "
-                        + "to derive VIDEO settings [mime type: " + resolvedVideoMime + "]");
-            } else if (Objects.equals(mediaSpecVideoMime, camcorderProfileVideoMime)) {
-                camcorderProfileIsCompatible = true;
-                resolvedVideoMime = camcorderProfileVideoMime;
-                Logger.d(TAG, "MediaSpec video mime matches CamcorderProfile. Using "
-                        + "CamcorderProfile to derive VIDEO settings [mime type: "
-                        + resolvedVideoMime + "]");
-            } else {
-                Logger.d(TAG, "MediaSpec video mime does not match CamcorderProfile, so "
-                        + "CamcorderProfile settings cannot be used. May rely on fallback "
-                        + "defaults to derive VIDEO settings [CamcorderProfile mime type: "
-                        + camcorderProfileVideoMime + ", chosen mime type: "
-                        + resolvedVideoMime + "]");
-            }
-        } else {
-            Logger.d(TAG,
-                    "No CamcorderProfile present. May rely on fallback defaults to derive VIDEO "
-                            + "settings [chosen mime type: " + resolvedVideoMime + "]");
-        }
-
-        MimeInfo.Builder mimeInfoBuilder = MimeInfo.builder(resolvedVideoMime);
-        if (camcorderProfileIsCompatible) {
-            mimeInfoBuilder.setCompatibleCamcorderProfile(mResolvedCamcorderProfile);
-        }
-
-        return mimeInfoBuilder.build();
-    }
-
-    @NonNull
-    private static AudioSource.Settings resolveAudioSourceSettings(@NonNull MimeInfo audioMimeInfo,
-            @NonNull AudioSpec audioSpec) {
-        Supplier<AudioSource.Settings> settingsSupplier;
-        if (audioMimeInfo.getCompatibleCamcorderProfile() != null) {
-            settingsSupplier = new AudioSourceSettingsCamcorderProfileResolver(audioSpec,
-                    audioMimeInfo.getCompatibleCamcorderProfile());
-        } else {
-            settingsSupplier = new AudioSourceSettingsDefaultResolver(audioSpec);
-        }
-
-        return settingsSupplier.get();
-    }
-
-    @NonNull
-    private static AudioEncoderConfig resolveAudioEncoderConfig(@NonNull MimeInfo audioMimeInfo,
-            @NonNull AudioSource.Settings audioSourceSettings, @NonNull AudioSpec audioSpec) {
-        Supplier<AudioEncoderConfig> configSupplier;
-        if (audioMimeInfo.getCompatibleCamcorderProfile() != null) {
-            configSupplier = new AudioEncoderConfigCamcorderProfileResolver(
-                    audioMimeInfo.getMimeType(), audioMimeInfo.getProfile(), audioSpec,
-                    audioSourceSettings, audioMimeInfo.getCompatibleCamcorderProfile());
-        } else {
-            configSupplier = new AudioEncoderConfigDefaultResolver(audioMimeInfo.getMimeType(),
-                    audioMimeInfo.getProfile(), audioSpec, audioSourceSettings);
-        }
-
-        return configSupplier.get();
-    }
-
-    @NonNull
-    private static VideoEncoderConfig resolveVideoEncoderConfig(@NonNull MimeInfo videoMimeInfo,
-            @NonNull VideoSpec videoSpec, @NonNull Size surfaceSize,
-            @Nullable Range<Integer> expectedFrameRateRange) {
-        Supplier<VideoEncoderConfig> configSupplier;
-        if (videoMimeInfo.getCompatibleCamcorderProfile() != null) {
-            configSupplier = new VideoEncoderConfigCamcorderProfileResolver(
-                    videoMimeInfo.getMimeType(), videoSpec, surfaceSize,
-                    videoMimeInfo.getCompatibleCamcorderProfile(),
-                    expectedFrameRateRange);
-        } else {
-            configSupplier = new VideoEncoderConfigDefaultResolver(videoMimeInfo.getMimeType(),
-                    videoSpec, surfaceSize, expectedFrameRateRange);
-        }
-
-        return configSupplier.get();
-    }
-
     /**
      * Setup audio related resources.
      *
@@ -1263,7 +1118,8 @@
             throws ResourceCreationException {
         MediaSpec mediaSpec = getObservableData(mMediaSpec);
         // Resolve the audio mime info
-        MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec);
+        MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedCamcorderProfile);
+        Timebase audioSourceTimebase = Timebase.UPTIME;
 
         // Select and create the audio source
         AudioSource.Settings audioSourceSettings =
@@ -1272,6 +1128,8 @@
             if (mAudioSource != null) {
                 releaseCurrentAudioSource();
             }
+            // TODO: set audioSourceTimebase to AudioSource. Currently AudioSource hard code
+            //  AudioTimestamp.TIMEBASE_MONOTONIC.
             mAudioSource = setupAudioSource(recordingToStart, audioSourceSettings);
             Logger.d(TAG, String.format("Set up new audio source: 0x%x", mAudioSource.hashCode()));
         } catch (AudioSourceAccessException e) {
@@ -1280,7 +1138,7 @@
 
         // Select and create the audio encoder
         AudioEncoderConfig audioEncoderConfig = resolveAudioEncoderConfig(audioMimeInfo,
-                audioSourceSettings, mediaSpec.getAudioSpec());
+                audioSourceTimebase, audioSourceSettings, mediaSpec.getAudioSpec());
         try {
             mAudioEncoder = mAudioEncoderFactory.createEncoder(mExecutor, audioEncoderConfig);
         } catch (InvalidConfigException e) {
@@ -1323,7 +1181,7 @@
             }
 
             @Override
-            public void onFailure(Throwable t) {
+            public void onFailure(@NonNull Throwable t) {
                 Logger.d(TAG, String.format("An error occurred while attempting to "
                         + "release audio source: 0x%x", audioSource.hashCode()));
             }
@@ -1331,15 +1189,16 @@
     }
 
     @ExecutedBy("mSequentialExecutor")
-    private void setupVideo(@NonNull SurfaceRequest surfaceRequest) {
+    private void setupVideo(@NonNull SurfaceRequest surfaceRequest, @NonNull Timebase timebase) {
         MediaSpec mediaSpec = getObservableData(mMediaSpec);
-        MimeInfo videoMimeInfo = resolveVideoMimeInfo(mediaSpec);
+        MimeInfo videoMimeInfo = resolveVideoMimeInfo(mediaSpec, mResolvedCamcorderProfile);
 
         // The VideoSpec from mMediaSpec only contains settings requested by the recorder, but
         // the actual settings may need to differ depending on the FPS chosen by the camera.
         // The expected frame rate from the camera is passed on here from the SurfaceRequest.
         VideoEncoderConfig config = resolveVideoEncoderConfig(
                 videoMimeInfo,
+                timebase,
                 mediaSpec.getVideoSpec(),
                 surfaceRequest.getResolution(),
                 surfaceRequest.getExpectedFrameRate());
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
index 640c671..6246d93 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapabilities.java
@@ -165,6 +165,35 @@
     }
 
     /**
+     * Finds the supported CamcorderProfileProxy with the resolution nearest to the given
+     * {@link Size}.
+     *
+     * <p>The supported CamcorderProfileProxy means the corresponding {@link Quality} is also
+     * supported. If the size aligns exactly with the pixel count of a CamcorderProfileProxy, that
+     * CamcorderProfileProxy will be selected. If the size falls between two
+     * CamcorderProfileProxy, the higher resolution will always be selected. Otherwise, the
+     * nearest CamcorderProfileProxy will be selected, whether that CamcorderProfileProxy's
+     * resolution is above or below the given size.
+     *
+     * @see #findHighestSupportedQualityFor(Size)
+     */
+    @Nullable
+    public CamcorderProfileProxy findHighestSupportedCamcorderProfileFor(@NonNull Size size) {
+        CamcorderProfileProxy camcorderProfile = null;
+        Quality highestSupportedQuality = findHighestSupportedQualityFor(size);
+        Logger.d(TAG,
+                "Using supported quality of " + highestSupportedQuality + " for size " + size);
+        if (highestSupportedQuality != Quality.NONE) {
+            camcorderProfile = getProfile(highestSupportedQuality);
+            if (camcorderProfile == null) {
+                throw new AssertionError("Camera advertised available quality but did not "
+                        + "produce CamcorderProfile for advertised quality.");
+            }
+        }
+        return camcorderProfile;
+    }
+
+    /**
      * Finds the nearest quality by number of pixels to the given {@link Size}.
      *
      * <p>If the size aligns exactly with the pixel count of a supported quality, that quality
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 9d7dcac..bbca48f 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
@@ -29,18 +29,23 @@
 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;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
 import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_CLASS;
 import static androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME;
 import static androidx.camera.core.internal.ThreadConfig.OPTION_BACKGROUND_EXECUTOR;
 import static androidx.camera.core.internal.UseCaseEventConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.video.StreamInfo.STREAM_ID_ERROR;
+import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_ENCODER_INFO_FINDER;
 import static androidx.camera.video.impl.VideoCaptureConfig.OPTION_VIDEO_OUTPUT;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoEncoderConfig;
+import static androidx.camera.video.internal.config.VideoConfigUtil.resolveVideoMimeInfo;
 
 import static java.util.Collections.singletonList;
 import static java.util.Objects.requireNonNull;
 
 import android.graphics.ImageFormat;
 import android.graphics.Rect;
+import android.hardware.camera2.CameraDevice;
 import android.media.MediaCodec;
 import android.util.Pair;
 import android.util.Range;
@@ -55,6 +60,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.VisibleForTesting;
+import androidx.arch.core.util.Function;
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
@@ -63,6 +69,7 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
+import androidx.camera.core.impl.CamcorderProfileProxy;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
 import androidx.camera.core.impl.CameraInfoInternal;
@@ -79,6 +86,7 @@
 import androidx.camera.core.impl.Observable.Observer;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
 import androidx.camera.core.impl.utils.Threads;
@@ -92,15 +100,26 @@
 import androidx.camera.core.processing.SurfaceEffectNode;
 import androidx.camera.video.StreamInfo.StreamState;
 import androidx.camera.video.impl.VideoCaptureConfig;
+import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.video.internal.compat.quirk.PreviewStretchWhenVideoCaptureIsBoundQuirk;
+import androidx.camera.video.internal.config.MimeInfo;
+import androidx.camera.video.internal.encoder.InvalidConfigException;
+import androidx.camera.video.internal.encoder.VideoEncoderConfig;
+import androidx.camera.video.internal.encoder.VideoEncoderInfo;
+import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl;
 import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.util.Preconditions;
+import androidx.core.util.Supplier;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.lang.reflect.Type;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
@@ -129,6 +148,8 @@
     private static final String SURFACE_UPDATE_KEY =
             "androidx.camera.video.VideoCapture.streamUpdate";
     private static final Defaults DEFAULT_CONFIG = new Defaults();
+    private static final boolean HAS_PREVIEW_STRETCH_QUIRK =
+            DeviceQuirks.get(PreviewStretchWhenVideoCaptureIsBoundQuirk.class) != null;
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     DeferrableSurface mDeferrableSurface;
@@ -146,6 +167,8 @@
     private SurfaceEffectInternal mSurfaceEffect;
     @Nullable
     private SurfaceEffectNode mNode;
+    @Nullable
+    private VideoEncoderInfo mVideoEncoderInfo;
 
     /**
      * Create a VideoCapture associated with the given {@link VideoOutput}.
@@ -316,6 +339,8 @@
     @Override
     public void onDetached() {
         clearPipeline();
+
+        mVideoEncoderInfo = null;
     }
 
     /**
@@ -412,8 +437,9 @@
         }
     }
 
+    @VisibleForTesting
     @NonNull
-    private SettableSurface getCameraSettableSurface() {
+    SettableSurface getCameraSettableSurface() {
         Preconditions.checkNotNull(mNode);
         return (SettableSurface) requireNonNull(mDeferrableSurface);
     }
@@ -446,17 +472,25 @@
         // TODO(b/229410005): The expected FPS range will need to come from the camera rather
         //  than what is requested in the config. For now we use the default range of (30, 30)
         //  for behavioral consistency.
-        Range<Integer> targetFpsRange = config.getTargetFramerate(Defaults.DEFAULT_FPS_RANGE);
+        Range<Integer> targetFpsRange = requireNonNull(
+                config.getTargetFramerate(Defaults.DEFAULT_FPS_RANGE));
+        Timebase timebase;
         if (mSurfaceEffect != null) {
-            mNode = new SurfaceEffectNode(camera, APPLY_CROP_ROTATE_AND_MIRRORING,
-                    mSurfaceEffect);
+            MediaSpec mediaSpec = requireNonNull(getMediaSpec());
+            Rect cropRect = requireNonNull(getCropRect(resolution));
+            timebase = camera.getCameraInfoInternal().getTimebase();
+            cropRect = adjustCropRectIfNeeded(cropRect, resolution,
+                    () -> getVideoEncoderInfo(config.getVideoEncoderInfoFinder(),
+                            VideoCapabilities.from(camera.getCameraInfo()), timebase, mediaSpec,
+                            resolution, targetFpsRange));
+            mNode = new SurfaceEffectNode(camera, APPLY_CROP_ROTATE_AND_MIRRORING, mSurfaceEffect);
             SettableSurface cameraSurface = new SettableSurface(
                     SurfaceEffect.VIDEO_CAPTURE,
                     resolution,
                     ImageFormat.PRIVATE,
                     getSensorToBufferTransformMatrix(),
                     /*hasEmbeddedTransform=*/true,
-                    requireNonNull(getCropRect(resolution)),
+                    cropRect,
                     getRelativeRotation(camera),
                     /*mirroring=*/false);
             SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
@@ -467,9 +501,15 @@
         } else {
             mSurfaceRequest = new SurfaceRequest(resolution, camera, false, targetFpsRange);
             mDeferrableSurface = mSurfaceRequest.getDeferrableSurface();
+            // When camera buffers from a REALTIME device are passed directly to a video encoder
+            // from the camera, automatic compensation is done to account for differing timebases
+            // of the audio and camera subsystems. See the document of
+            // CameraMetadata#SENSOR_INFO_TIMESTAMP_SOURCE_REALTIME. So the timebase is always
+            // UPTIME when encoder surface is directly sent to camera.
+            timebase = Timebase.UPTIME;
         }
 
-        config.getVideoOutput().onSurfaceRequested(mSurfaceRequest);
+        config.getVideoOutput().onSurfaceRequested(mSurfaceRequest, timebase);
         sendTransformationInfoIfReady(resolution);
         // Since VideoCapture is in video module and can't be recognized by core module, use
         // MediaCodec class instead.
@@ -478,6 +518,9 @@
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
         sessionConfigBuilder.addErrorListener(
                 (sessionConfig, error) -> resetPipeline(cameraId, config, resolution));
+        if (HAS_PREVIEW_STRETCH_QUIRK) {
+            sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        }
 
         return sessionConfigBuilder;
     }
@@ -537,11 +580,22 @@
                 SurfaceRequest::willNotProvideSurface;
         private static final VideoCaptureConfig<?> DEFAULT_CONFIG;
 
+        private static final Function<VideoEncoderConfig, VideoEncoderInfo>
+                DEFAULT_VIDEO_ENCODER_INFO_FINDER = encoderInfo -> {
+                    try {
+                        return VideoEncoderInfoImpl.from(encoderInfo);
+                    } catch (InvalidConfigException e) {
+                        Logger.w(TAG, "Unable to find VideoEncoderInfo", e);
+                        return null;
+                    }
+                };
+
         static final Range<Integer> DEFAULT_FPS_RANGE = new Range<>(30, 30);
 
         static {
             Builder<?> builder = new Builder<>(DEFAULT_VIDEO_OUTPUT)
-                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY);
+                    .setSurfaceOccupancyPriority(DEFAULT_SURFACE_OCCUPANCY_PRIORITY)
+                    .setVideoEncoderInfoFinder(DEFAULT_VIDEO_ENCODER_INFO_FINDER);
 
             DEFAULT_CONFIG = builder.getUseCaseConfig();
         }
@@ -630,6 +684,217 @@
     }
 
     @MainThread
+    @NonNull
+    private Rect adjustCropRectIfNeeded(@NonNull Rect cropRect, @NonNull Size resolution,
+            @NonNull Supplier<VideoEncoderInfo> videoEncoderInfoFinder) {
+        if (!isCropNeeded(cropRect, resolution)) {
+            return cropRect;
+        }
+        VideoEncoderInfo videoEncoderInfo = videoEncoderInfoFinder.get();
+        if (videoEncoderInfo == null) {
+            Logger.w(TAG, "Crop is needed but can't find the encoder info to adjust the cropRect");
+            return cropRect;
+        }
+        return adjustCropRectToValidSize(cropRect, resolution, videoEncoderInfo);
+    }
+
+    /**
+     * This method resizes the crop rectangle to a valid size.
+     *
+     * <p>The valid size must fulfill
+     * <ul>
+     * <li>The multiple of VideoEncoderInfo.getWidthAlignment()/getHeightAlignment() alignment</li>
+     * <li>In the scope of Surface resolution and VideoEncoderInfo.getSupportedWidths()
+     * /getSupportedHeights().</li>
+     * </ul>
+     *
+     * <p>When the size is not a multiple of the alignment, it seeks to shrink or enlarge the size
+     * with the smallest amount of change and ensures that the size is within the surface
+     * resolution and supported widths and heights. The new cropping rectangle position (left,
+     * right, top, and bottom) is then calculated by extending or indenting from the center of
+     * the original cropping rectangle.
+     */
+    @NonNull
+    private static Rect adjustCropRectToValidSize(@NonNull Rect cropRect, @NonNull Size resolution,
+            @NonNull VideoEncoderInfo videoEncoderInfo) {
+        Logger.d(TAG, String.format("Adjust cropRect %s by width/height alignment %d/%d and "
+                        + "supported widths %s / supported heights %s",
+                rectToString(cropRect),
+                videoEncoderInfo.getWidthAlignment(),
+                videoEncoderInfo.getHeightAlignment(),
+                videoEncoderInfo.getSupportedWidths(),
+                videoEncoderInfo.getSupportedHeights()
+        ));
+
+        // Construct all up/down alignment combinations.
+        int widthAlignment = videoEncoderInfo.getWidthAlignment();
+        int heightAlignment = videoEncoderInfo.getHeightAlignment();
+        Range<Integer> supportedWidths = videoEncoderInfo.getSupportedWidths();
+        Range<Integer> supportedHeights = videoEncoderInfo.getSupportedHeights();
+        int widthAlignedDown = alignDown(cropRect.width(), widthAlignment, supportedWidths);
+        int widthAlignedUp = alignUp(cropRect.width(), widthAlignment, supportedWidths);
+        int heightAlignedDown = alignDown(cropRect.height(), heightAlignment, supportedHeights);
+        int heightAlignedUp = alignUp(cropRect.height(), heightAlignment, supportedHeights);
+
+        // Use Set to filter out duplicates.
+        Set<Size> candidateSet = new HashSet<>();
+        addBySupportedSize(candidateSet, widthAlignedDown, heightAlignedDown, resolution,
+                videoEncoderInfo);
+        addBySupportedSize(candidateSet, widthAlignedDown, heightAlignedUp, resolution,
+                videoEncoderInfo);
+        addBySupportedSize(candidateSet, widthAlignedUp, heightAlignedDown, resolution,
+                videoEncoderInfo);
+        addBySupportedSize(candidateSet, widthAlignedUp, heightAlignedUp, resolution,
+                videoEncoderInfo);
+        if (candidateSet.isEmpty()) {
+            Logger.w(TAG, "Can't find valid cropped size");
+            return cropRect;
+        }
+        List<Size> candidatesList = new ArrayList<>(candidateSet);
+        Logger.d(TAG, "candidatesList = " + candidatesList);
+
+        // Find the smallest change in dimensions.
+        //noinspection ComparatorCombinators - Suggestion by Comparator.comparingInt is for API24+
+        Collections.sort(candidatesList,
+                (s1, s2) -> (Math.abs(s1.getWidth() - cropRect.width()) + Math.abs(
+                        s1.getHeight() - cropRect.height()))
+                        - (Math.abs(s2.getWidth() - cropRect.width()) + Math.abs(
+                        s2.getHeight() - cropRect.height())));
+        Logger.d(TAG, "sorted candidatesList = " + candidatesList);
+        Size newSize = candidatesList.get(0);
+        int newWidth = newSize.getWidth();
+        int newHeight = newSize.getHeight();
+
+        if (newWidth == cropRect.width() && newHeight == cropRect.height()) {
+            Logger.d(TAG, "No need to adjust cropRect because crop size is valid.");
+            return cropRect;
+        }
+
+        // New width/height should be multiple of 2 since VideoCapabilities.get*Alignment()
+        // returns power of 2. This ensures width/2 and height/2 are not rounded off.
+        // New width/height smaller than resolution ensures calculated cropRect never exceeds
+        // the resolution.
+        Preconditions.checkState(newWidth % 2 == 0 && newHeight % 2 == 0
+                && newWidth <= resolution.getWidth() && newHeight <= resolution.getHeight());
+        Rect newCropRect = new Rect(cropRect);
+        if (newWidth != cropRect.width()) {
+            // Note: When the width/height of cropRect is odd number, Rect.centerX/Y() will be
+            // offset to the left/top by 0.5.
+            newCropRect.left = Math.max(0, cropRect.centerX() - newWidth / 2);
+            newCropRect.right = newCropRect.left + newWidth;
+            if (newCropRect.right > resolution.getWidth()) {
+                newCropRect.right = resolution.getWidth();
+                newCropRect.left = newCropRect.right - newWidth;
+            }
+        }
+        if (newHeight != cropRect.height()) {
+            newCropRect.top = Math.max(0, cropRect.centerY() - newHeight / 2);
+            newCropRect.bottom = newCropRect.top + newHeight;
+            if (newCropRect.bottom > resolution.getHeight()) {
+                newCropRect.bottom = resolution.getHeight();
+                newCropRect.top = newCropRect.bottom - newHeight;
+            }
+        }
+        Logger.d(TAG, String.format("Adjust cropRect from %s to %s", rectToString(cropRect),
+                rectToString(newCropRect)));
+        return newCropRect;
+    }
+
+    private static void addBySupportedSize(@NonNull Set<Size> candidates, int width, int height,
+            @NonNull Size resolution, @NonNull VideoEncoderInfo videoEncoderInfo) {
+        if (width > resolution.getWidth() || height > resolution.getHeight()) {
+            return;
+        }
+        try {
+            Range<Integer> supportedHeights = videoEncoderInfo.getSupportedHeightsFor(width);
+            candidates.add(new Size(width, supportedHeights.clamp(height)));
+        } catch (IllegalArgumentException e) {
+            Logger.w(TAG, "No supportedHeights for width: " + width, e);
+        }
+        try {
+            Range<Integer> supportedWidths = videoEncoderInfo.getSupportedWidthsFor(height);
+            candidates.add(new Size(supportedWidths.clamp(width), height));
+        } catch (IllegalArgumentException e) {
+            Logger.w(TAG, "No supportedWidths for height: " + height, e);
+        }
+    }
+
+    private static boolean isCropNeeded(@NonNull Rect cropRect, @NonNull Size resolution) {
+        return resolution.getWidth() != cropRect.width()
+                || resolution.getHeight() != cropRect.height();
+    }
+
+    private static int alignDown(int length, int alignment,
+            @NonNull Range<Integer> supportedLength) {
+        return align(true, length, alignment, supportedLength);
+    }
+
+    private static int alignUp(int length, int alignment,
+            @NonNull Range<Integer> supportedRange) {
+        return align(false, length, alignment, supportedRange);
+    }
+
+    private static int align(boolean alignDown, int length, int alignment,
+            @NonNull Range<Integer> supportedRange) {
+        int remainder = length % alignment;
+        int newLength;
+        if (remainder == 0) {
+            newLength = length;
+        } else if (alignDown) {
+            newLength = length - remainder;
+        } else {
+            newLength = length + (alignment - remainder);
+        }
+        // Clamp new length by supportedRange, which is supposed to be valid length.
+        return supportedRange.clamp(newLength);
+    }
+
+    @MainThread
+    @Nullable
+    private VideoEncoderInfo getVideoEncoderInfo(
+            @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder,
+            @NonNull VideoCapabilities videoCapabilities,
+            @NonNull Timebase timebase,
+            @NonNull MediaSpec mediaSpec,
+            @NonNull Size resolution,
+            @NonNull Range<Integer> targetFps) {
+        if (mVideoEncoderInfo != null) {
+            return mVideoEncoderInfo;
+        }
+        // Cache the VideoEncoderInfo as it should be the same when recreating the pipeline.
+        // This avoids recreating the MediaCodec instance to get encoder information.
+        // Note: We should clear the cache if the MediaSpec changes at any time, especially when
+        // the Encoder-related content in the VideoSpec changes. i.e. when we need to observe the
+        // MediaSpec Observable.
+        return mVideoEncoderInfo = resolveVideoEncoderInfo(videoEncoderInfoFinder,
+                videoCapabilities, timebase, mediaSpec, resolution, targetFps);
+    }
+
+    @Nullable
+    private static VideoEncoderInfo resolveVideoEncoderInfo(
+            @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder,
+            @NonNull VideoCapabilities videoCapabilities,
+            @NonNull Timebase timebase,
+            @NonNull MediaSpec mediaSpec,
+            @NonNull Size resolution,
+            @NonNull Range<Integer> targetFps) {
+        // Find the nearest CamcorderProfile
+        CamcorderProfileProxy camcorderProfileProxy =
+                videoCapabilities.findHighestSupportedCamcorderProfileFor(resolution);
+
+        // Resolve the VideoEncoderConfig
+        MimeInfo videoMimeInfo = resolveVideoMimeInfo(mediaSpec, camcorderProfileProxy);
+        VideoEncoderConfig videoEncoderConfig = resolveVideoEncoderConfig(
+                videoMimeInfo,
+                timebase,
+                mediaSpec.getVideoSpec(),
+                resolution,
+                targetFps);
+
+        return videoEncoderInfoFinder.apply(videoEncoderConfig);
+    }
+
+    @MainThread
     private void setupSurfaceUpdateNotifier(@NonNull SessionConfig.Builder sessionConfigBuilder,
             boolean isStreamActive) {
         if (mSurfaceUpdateFuture != null) {
@@ -914,6 +1179,14 @@
             return new VideoCaptureConfig<>(OptionsBundle.from(mMutableConfig));
         }
 
+        @NonNull
+        Builder<T> setVideoEncoderInfoFinder(
+                @NonNull Function<VideoEncoderConfig, VideoEncoderInfo> videoEncoderInfoFinder) {
+            getMutableConfig().insertOption(OPTION_VIDEO_ENCODER_INFO_FINDER,
+                    videoEncoderInfoFinder);
+            return this;
+        }
+
         /**
          * Builds an immutable {@link VideoCaptureConfig} from the current state.
          *
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
index 56311d8..222de64 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoOutput.java
@@ -25,6 +25,7 @@
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.impl.ConstantObservable;
 import androidx.camera.core.impl.Observable;
+import androidx.camera.core.impl.Timebase;
 import androidx.core.util.Consumer;
 
 import java.util.concurrent.Executor;
@@ -91,6 +92,17 @@
     void onSurfaceRequested(@NonNull SurfaceRequest request);
 
     /**
+     * Called when a new {@link Surface} has been requested by a video frame producer.
+     *
+     * @param timebase the video source timebase
+     * @hide
+     */
+    @RestrictTo(Scope.LIBRARY)
+    default void onSurfaceRequested(@NonNull SurfaceRequest request, @NonNull Timebase timebase) {
+        onSurfaceRequested(request);
+    }
+
+    /**
      * Returns an observable {@link StreamInfo} which contains the information of the
      * {@link VideoOutput}.
      *
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
index dac4381..c9bd4de 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/impl/VideoCaptureConfig.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.arch.core.util.Function;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.ImageOutputConfig;
@@ -26,6 +27,10 @@
 import androidx.camera.core.internal.ThreadConfig;
 import androidx.camera.video.VideoCapture;
 import androidx.camera.video.VideoOutput;
+import androidx.camera.video.internal.encoder.VideoEncoderConfig;
+import androidx.camera.video.internal.encoder.VideoEncoderInfo;
+
+import java.util.Objects;
 
 /**
  * Config for a video capture use case.
@@ -46,6 +51,10 @@
     public static final Option<VideoOutput> OPTION_VIDEO_OUTPUT =
             Option.create("camerax.video.VideoCapture.videoOutput", VideoOutput.class);
 
+    public static final Option<Function<VideoEncoderConfig, VideoEncoderInfo>>
+            OPTION_VIDEO_ENCODER_INFO_FINDER =
+            Option.create("camerax.video.VideoCapture.videoEncoderInfoFinder", Function.class);
+
     // *********************************************************************************************
 
     private final OptionsBundle mConfig;
@@ -60,6 +69,11 @@
         return (T) retrieveOption(OPTION_VIDEO_OUTPUT);
     }
 
+    @NonNull
+    public Function<VideoEncoderConfig, VideoEncoderInfo> getVideoEncoderInfoFinder() {
+        return Objects.requireNonNull(retrieveOption(OPTION_VIDEO_ENCODER_INFO_FINDER));
+    }
+
     /**
      * Retrieves the format of the image that is fed as input.
      *
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 33843db..151ba52 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -77,6 +77,9 @@
         if (NegativeLatLongSavesIncorrectlyQuirk.load()) {
             quirks.add(new NegativeLatLongSavesIncorrectlyQuirk());
         }
+        if (PreviewStretchWhenVideoCaptureIsBoundQuirk.load()) {
+            quirks.add(new PreviewStretchWhenVideoCaptureIsBoundQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewStretchWhenVideoCaptureIsBoundQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewStretchWhenVideoCaptureIsBoundQuirk.java
new file mode 100644
index 0000000..6e433bf
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/PreviewStretchWhenVideoCaptureIsBoundQuirk.java
@@ -0,0 +1,61 @@
+/*
+ * 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.video.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: b/227469801
+ *     Description: Quirk indicates Preview is stretched when VideoCapture is bound.
+ *     Device(s): Samsung J3, Samsung J7, Samsung J1 Ace neo and Oppo A37F
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class PreviewStretchWhenVideoCaptureIsBoundQuirk implements Quirk {
+
+    static boolean load() {
+        return isHuaweiP8Lite() || isSamsungJ3() || isSamsungJ7() || isSamsungJ1AceNeo()
+                || isOppoA37F();
+    }
+
+    private static boolean isHuaweiP8Lite() {
+        return "HUAWEI".equalsIgnoreCase(Build.MANUFACTURER)
+                && "HUAWEI ALE-L04".equalsIgnoreCase(Build.MODEL);
+    }
+
+    private static boolean isSamsungJ3() {
+        return "Samsung".equalsIgnoreCase(Build.MANUFACTURER)
+                && "sm-j320f".equalsIgnoreCase(Build.MODEL);
+    }
+
+    private static boolean isSamsungJ7() {
+        return "Samsung".equalsIgnoreCase(Build.MANUFACTURER)
+                && "sm-j700f".equalsIgnoreCase(Build.MODEL);
+    }
+
+    private static boolean isSamsungJ1AceNeo() {
+        return "Samsung".equalsIgnoreCase(Build.MANUFACTURER)
+                && "sm-j111f".equalsIgnoreCase(Build.MODEL);
+    }
+
+    private static boolean isOppoA37F() {
+        return "OPPO".equalsIgnoreCase(Build.MANUFACTURER) && "A37F".equalsIgnoreCase(Build.MODEL);
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioConfigUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioConfigUtil.java
index f388b3b..61bea4c 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioConfigUtil.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioConfigUtil.java
@@ -20,14 +20,21 @@
 import android.util.Rational;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CamcorderProfileProxy;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.video.AudioSpec;
+import androidx.camera.video.MediaSpec;
 import androidx.camera.video.internal.AudioSource;
+import androidx.camera.video.internal.encoder.AudioEncoderConfig;
+import androidx.core.util.Supplier;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A collection of utilities used for resolving and debugging audio configurations.
@@ -49,6 +56,113 @@
     private AudioConfigUtil() {
     }
 
+    /**
+     * Resolves the audio mime information into a {@link MimeInfo}.
+     *
+     * @param mediaSpec        the media spec to resolve the mime info.
+     * @param camcorderProfile the camcorder profile to resolve the mime info. It can be null if
+     *                         there is no relevant camcorder profile.
+     * @return the audio MimeInfo.
+     */
+    @NonNull
+    public static MimeInfo resolveAudioMimeInfo(@NonNull MediaSpec mediaSpec,
+            @Nullable CamcorderProfileProxy camcorderProfile) {
+        String mediaSpecAudioMime = MediaSpec.outputFormatToAudioMime(mediaSpec.getOutputFormat());
+        int mediaSpecAudioProfile =
+                MediaSpec.outputFormatToAudioProfile(mediaSpec.getOutputFormat());
+        String resolvedAudioMime = mediaSpecAudioMime;
+        int resolvedAudioProfile = mediaSpecAudioProfile;
+        boolean camcorderProfileIsCompatible = false;
+        if (camcorderProfile != null) {
+            String camcorderProfileAudioMime = camcorderProfile.getAudioCodecMimeType();
+            int camcorderProfileAudioProfile = camcorderProfile.getRequiredAudioProfile();
+
+            if (camcorderProfileAudioMime == null) {
+                Logger.d(TAG, "CamcorderProfile contains undefined AUDIO mime type so cannot be "
+                        + "used. May rely on fallback defaults to derive settings [chosen mime "
+                        + "type: "
+                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
+            } else if (mediaSpec.getOutputFormat() == MediaSpec.OUTPUT_FORMAT_AUTO) {
+                camcorderProfileIsCompatible = true;
+                resolvedAudioMime = camcorderProfileAudioMime;
+                resolvedAudioProfile = camcorderProfileAudioProfile;
+                Logger.d(TAG, "MediaSpec contains OUTPUT_FORMAT_AUTO. Using CamcorderProfile "
+                        + "to derive AUDIO settings [mime type: "
+                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
+            } else if (Objects.equals(mediaSpecAudioMime, camcorderProfileAudioMime)
+                    && mediaSpecAudioProfile == camcorderProfileAudioProfile) {
+                camcorderProfileIsCompatible = true;
+                resolvedAudioMime = camcorderProfileAudioMime;
+                resolvedAudioProfile = camcorderProfileAudioProfile;
+                Logger.d(TAG, "MediaSpec audio mime/profile matches CamcorderProfile. "
+                        + "Using CamcorderProfile to derive AUDIO settings [mime type: "
+                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
+            } else {
+                Logger.d(TAG, "MediaSpec audio mime or profile does not match CamcorderProfile, so "
+                        + "CamcorderProfile settings cannot be used. May rely on fallback "
+                        + "defaults to derive AUDIO settings [CamcorderProfile mime type: "
+                        + camcorderProfileAudioMime + "(profile: " + camcorderProfileAudioProfile
+                        + "), chosen mime type: "
+                        + resolvedAudioMime + "(profile: " + resolvedAudioProfile + ")]");
+            }
+        }
+
+        MimeInfo.Builder mimeInfoBuilder = MimeInfo.builder(resolvedAudioMime)
+                .setProfile(resolvedAudioProfile);
+        if (camcorderProfileIsCompatible) {
+            mimeInfoBuilder.setCompatibleCamcorderProfile(camcorderProfile);
+        }
+
+        return mimeInfoBuilder.build();
+    }
+
+    /**
+     * Resolves the audio source settings into a {@link AudioSource.Settings}.
+     *
+     * @param audioMimeInfo the audio mime info.
+     * @param audioSpec     the audio spec.
+     * @return a AudioSource.Settings.
+     */
+    @NonNull
+    public static AudioSource.Settings resolveAudioSourceSettings(@NonNull MimeInfo audioMimeInfo,
+            @NonNull AudioSpec audioSpec) {
+        Supplier<AudioSource.Settings> settingsSupplier;
+        if (audioMimeInfo.getCompatibleCamcorderProfile() != null) {
+            settingsSupplier = new AudioSourceSettingsCamcorderProfileResolver(audioSpec,
+                    audioMimeInfo.getCompatibleCamcorderProfile());
+        } else {
+            settingsSupplier = new AudioSourceSettingsDefaultResolver(audioSpec);
+        }
+
+        return settingsSupplier.get();
+    }
+
+    /**
+     * Resolves video related information into a {@link AudioEncoderConfig}.
+     *
+     * @param audioMimeInfo       the audio mime info.
+     * @param inputTimebase       the timebase of the input frame.
+     * @param audioSourceSettings the audio source settings.
+     * @param audioSpec           the audio spec.
+     * @return a AudioEncoderConfig.
+     */
+    @NonNull
+    public static AudioEncoderConfig resolveAudioEncoderConfig(@NonNull MimeInfo audioMimeInfo,
+            @NonNull Timebase inputTimebase, @NonNull AudioSource.Settings audioSourceSettings,
+            @NonNull AudioSpec audioSpec) {
+        Supplier<AudioEncoderConfig> configSupplier;
+        if (audioMimeInfo.getCompatibleCamcorderProfile() != null) {
+            configSupplier = new AudioEncoderConfigCamcorderProfileResolver(
+                    audioMimeInfo.getMimeType(), audioMimeInfo.getProfile(), inputTimebase,
+                    audioSpec, audioSourceSettings, audioMimeInfo.getCompatibleCamcorderProfile());
+        } else {
+            configSupplier = new AudioEncoderConfigDefaultResolver(audioMimeInfo.getMimeType(),
+                    audioMimeInfo.getProfile(), inputTimebase, audioSpec, audioSourceSettings);
+        }
+
+        return configSupplier.get();
+    }
+
     static int resolveAudioSource(@NonNull AudioSpec audioSpec) {
         int resolvedAudioSource = audioSpec.getSource();
         if (resolvedAudioSource == AudioSpec.SOURCE_AUTO) {
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolver.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolver.java
index 381510b5..21d59a3 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolver.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigCamcorderProfileResolver.java
@@ -23,6 +23,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.CamcorderProfileProxy;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.video.AudioSpec;
 import androidx.camera.video.internal.AudioSource;
 import androidx.camera.video.internal.encoder.AudioEncoderConfig;
@@ -40,6 +41,7 @@
     private static final String TAG = "AudioEncCmcrdrPrflRslvr";
 
     private final String mMimeType;
+    private final Timebase mInputTimebase;
     private final int mAudioProfile;
     private final AudioSpec mAudioSpec;
     private final AudioSource.Settings mAudioSourceSettings;
@@ -50,6 +52,7 @@
      *
      * @param mimeType            The mime type for the audio encoder
      * @param audioProfile        The profile required for the audio encoder
+     * @param inputTimebase       The timebase of the input frame
      * @param audioSpec           The {@link AudioSpec} which defines the settings that should be
      *                            used with the audio encoder.
      * @param audioSourceSettings The settings used to configure the source of audio.
@@ -57,11 +60,12 @@
      *                            range settings.
      */
     public AudioEncoderConfigCamcorderProfileResolver(@NonNull String mimeType,
-            int audioProfile, @NonNull AudioSpec audioSpec,
+            int audioProfile, @NonNull Timebase inputTimebase, @NonNull AudioSpec audioSpec,
             @NonNull AudioSource.Settings audioSourceSettings,
             @NonNull CamcorderProfileProxy camcorderProfile) {
         mMimeType = mimeType;
         mAudioProfile = audioProfile;
+        mInputTimebase = inputTimebase;
         mAudioSpec = audioSpec;
         mAudioSourceSettings = audioSourceSettings;
         mCamcorderProfile = camcorderProfile;
@@ -81,6 +85,7 @@
         return AudioEncoderConfig.builder()
                 .setMimeType(mMimeType)
                 .setProfile(mAudioProfile)
+                .setInputTimebase(mInputTimebase)
                 .setChannelCount(mAudioSourceSettings.getChannelCount())
                 .setSampleRate(mAudioSourceSettings.getSampleRate())
                 .setBitrate(resolvedBitrate)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolver.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolver.java
index 0b756a8..8673254 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolver.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/AudioEncoderConfigDefaultResolver.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.video.AudioSpec;
 import androidx.camera.video.internal.AudioSource;
 import androidx.camera.video.internal.encoder.AudioEncoderConfig;
@@ -39,6 +40,7 @@
     private final int mAudioProfile;
     private final AudioSpec mAudioSpec;
     private final AudioSource.Settings mAudioSourceSettings;
+    private final Timebase mInputTimeBase;
 
     // Base config based on generic 720p AAC(LC) quality will be scaled by actual source settings.
     // TODO: These should vary based on quality/codec and be derived from actual devices
@@ -51,15 +53,17 @@
      *
      * @param mimeType            The mime type for the audio encoder
      * @param audioProfile        The profile required for the audio encoder
+     * @param inputTimebase       The timebase of the input frame.
      * @param audioSpec           The {@link AudioSpec} which defines the settings that should be
      *                            used with the audio encoder.
      * @param audioSourceSettings The settings used to configure the source of audio.
      */
     public AudioEncoderConfigDefaultResolver(@NonNull String mimeType,
-            int audioProfile, @NonNull AudioSpec audioSpec,
+            int audioProfile, @NonNull Timebase inputTimebase, @NonNull AudioSpec audioSpec,
             @NonNull AudioSource.Settings audioSourceSettings) {
         mMimeType = mimeType;
         mAudioProfile = audioProfile;
+        mInputTimeBase = inputTimebase;
         mAudioSpec = audioSpec;
         mAudioSourceSettings = audioSourceSettings;
     }
@@ -79,6 +83,7 @@
         return AudioEncoderConfig.builder()
                 .setMimeType(mMimeType)
                 .setProfile(mAudioProfile)
+                .setInputTimebase(mInputTimeBase)
                 .setChannelCount(mAudioSourceSettings.getChannelCount())
                 .setSampleRate(mAudioSourceSettings.getSampleRate())
                 .setBitrate(resolvedBitrate)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoConfigUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoConfigUtil.java
index 0e14ec1..16416a7 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoConfigUtil.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoConfigUtil.java
@@ -18,12 +18,20 @@
 
 import android.util.Range;
 import android.util.Rational;
+import android.util.Size;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CamcorderProfileProxy;
+import androidx.camera.core.impl.Timebase;
+import androidx.camera.video.MediaSpec;
 import androidx.camera.video.VideoSpec;
+import androidx.camera.video.internal.encoder.VideoEncoderConfig;
+import androidx.core.util.Supplier;
+
+import java.util.Objects;
 
 /**
  * A collection of utilities used for resolving and debugging video configurations.
@@ -36,6 +44,88 @@
     private VideoConfigUtil() {
     }
 
+    /**
+     * Resolves the video mime information into a {@link MimeInfo}.
+     *
+     * @param mediaSpec        the media spec to resolve the mime info.
+     * @param camcorderProfile the camcorder profile to resolve the mime info. It can be null if
+     *                         there is no relevant camcorder profile.
+     * @return the video MimeInfo.
+     */
+    @NonNull
+    public static MimeInfo resolveVideoMimeInfo(@NonNull MediaSpec mediaSpec,
+            @Nullable CamcorderProfileProxy camcorderProfile) {
+        String mediaSpecVideoMime = MediaSpec.outputFormatToVideoMime(mediaSpec.getOutputFormat());
+        String resolvedVideoMime = mediaSpecVideoMime;
+        boolean camcorderProfileIsCompatible = false;
+        if (camcorderProfile != null) {
+            String camcorderProfileVideoMime = camcorderProfile.getVideoCodecMimeType();
+            // Use camcorder profile settings if the media spec's output format
+            // is set to auto or happens to match the CamcorderProfile's output format.
+            if (camcorderProfileVideoMime == null) {
+                Logger.d(TAG, "CamcorderProfile contains undefined VIDEO mime type so cannot be "
+                        + "used. May rely on fallback defaults to derive settings [chosen mime "
+                        + "type: " + resolvedVideoMime + "]");
+            } else if (mediaSpec.getOutputFormat() == MediaSpec.OUTPUT_FORMAT_AUTO) {
+                camcorderProfileIsCompatible = true;
+                resolvedVideoMime = camcorderProfileVideoMime;
+                Logger.d(TAG, "MediaSpec contains OUTPUT_FORMAT_AUTO. Using CamcorderProfile "
+                        + "to derive VIDEO settings [mime type: " + resolvedVideoMime + "]");
+            } else if (Objects.equals(mediaSpecVideoMime, camcorderProfileVideoMime)) {
+                camcorderProfileIsCompatible = true;
+                resolvedVideoMime = camcorderProfileVideoMime;
+                Logger.d(TAG, "MediaSpec video mime matches CamcorderProfile. Using "
+                        + "CamcorderProfile to derive VIDEO settings [mime type: "
+                        + resolvedVideoMime + "]");
+            } else {
+                Logger.d(TAG, "MediaSpec video mime does not match CamcorderProfile, so "
+                        + "CamcorderProfile settings cannot be used. May rely on fallback "
+                        + "defaults to derive VIDEO settings [CamcorderProfile mime type: "
+                        + camcorderProfileVideoMime + ", chosen mime type: "
+                        + resolvedVideoMime + "]");
+            }
+        } else {
+            Logger.d(TAG,
+                    "No CamcorderProfile present. May rely on fallback defaults to derive VIDEO "
+                            + "settings [chosen mime type: " + resolvedVideoMime + "]");
+        }
+
+        MimeInfo.Builder mimeInfoBuilder = MimeInfo.builder(resolvedVideoMime);
+        if (camcorderProfileIsCompatible) {
+            mimeInfoBuilder.setCompatibleCamcorderProfile(camcorderProfile);
+        }
+
+        return mimeInfoBuilder.build();
+    }
+
+    /**
+     * Resolves video related information into a {@link VideoEncoderConfig}.
+     *
+     * @param videoMimeInfo          the video mime info.
+     * @param videoSpec              the video spec.
+     * @param inputTimebase          the timebase of the input frame.
+     * @param surfaceSize            the surface size.
+     * @param expectedFrameRateRange the expected frame rate range. It could be null.
+     * @return a VideoEncoderConfig.
+     */
+    @NonNull
+    public static VideoEncoderConfig resolveVideoEncoderConfig(@NonNull MimeInfo videoMimeInfo,
+            @NonNull Timebase inputTimebase, @NonNull VideoSpec videoSpec,
+            @NonNull Size surfaceSize, @Nullable Range<Integer> expectedFrameRateRange) {
+        Supplier<VideoEncoderConfig> configSupplier;
+        if (videoMimeInfo.getCompatibleCamcorderProfile() != null) {
+            configSupplier = new VideoEncoderConfigCamcorderProfileResolver(
+                    videoMimeInfo.getMimeType(), inputTimebase, videoSpec, surfaceSize,
+                    videoMimeInfo.getCompatibleCamcorderProfile(),
+                    expectedFrameRateRange);
+        } else {
+            configSupplier = new VideoEncoderConfigDefaultResolver(videoMimeInfo.getMimeType(),
+                    inputTimebase, videoSpec, surfaceSize, expectedFrameRateRange);
+        }
+
+        return configSupplier.get();
+    }
+
     static int resolveFrameRate(@NonNull Range<Integer> preferredRange,
             int exactFrameRateHint, @Nullable Range<Integer> strictOperatingFpsRange) {
         Range<Integer> refinedRange;
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolver.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolver.java
index dc703be..5020129 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolver.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigCamcorderProfileResolver.java
@@ -24,6 +24,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.CamcorderProfileProxy;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.video.VideoSpec;
 import androidx.camera.video.internal.encoder.VideoEncoderConfig;
 import androidx.core.util.Supplier;
@@ -39,6 +40,7 @@
     private static final String TAG = "VidEncCmcrdrPrflRslvr";
 
     private final String mMimeType;
+    private final Timebase mInputTimebase;
     private final VideoSpec mVideoSpec;
     private final Size mSurfaceSize;
     private final CamcorderProfileProxy mCamcorderProfile;
@@ -49,6 +51,7 @@
      * Constructor for a VideoEncoderConfigCamcorderProfileResolver.
      *
      * @param mimeType         The mime type for the video encoder
+     * @param inputTimebase    The timebase of the input frame
      * @param videoSpec        The {@link VideoSpec} which defines the settings that should be
      *                         used with the video encoder.
      * @param surfaceSize      The size of the surface required by the camera for the video encoder.
@@ -62,11 +65,13 @@
      *                               available and it does not need to be used in calculations.
      */
     public VideoEncoderConfigCamcorderProfileResolver(@NonNull String mimeType,
+            @NonNull Timebase inputTimebase,
             @NonNull VideoSpec videoSpec,
             @NonNull Size surfaceSize,
             @NonNull CamcorderProfileProxy camcorderProfile,
             @Nullable Range<Integer> expectedFrameRateRange) {
         mMimeType = mimeType;
+        mInputTimebase = inputTimebase;
         mVideoSpec = videoSpec;
         mSurfaceSize = surfaceSize;
         mCamcorderProfile = camcorderProfile;
@@ -90,6 +95,7 @@
 
         return VideoEncoderConfig.builder()
                 .setMimeType(mMimeType)
+                .setInputTimebase(mInputTimebase)
                 .setResolution(mSurfaceSize)
                 .setBitrate(resolvedBitrate)
                 .setFrameRate(resolvedFrameRate)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolver.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolver.java
index 4f84677..75d159f 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolver.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/config/VideoEncoderConfigDefaultResolver.java
@@ -23,6 +23,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.Logger;
+import androidx.camera.core.impl.Timebase;
 import androidx.camera.video.VideoSpec;
 import androidx.camera.video.internal.encoder.VideoEncoderConfig;
 import androidx.core.util.Supplier;
@@ -45,6 +46,8 @@
     private static final Range<Integer> VALID_FRAME_RATE_RANGE = new Range<>(1, 60);
 
     private final String mMimeType;
+
+    private final Timebase mInputTimebase;
     private final VideoSpec mVideoSpec;
     private final Size mSurfaceSize;
     @Nullable
@@ -53,15 +56,17 @@
     /**
      * Constructor for a VideoEncoderConfigDefaultResolver.
      *
-     * @param mimeType    The mime type for the video encoder
-     * @param videoSpec   The {@link VideoSpec} which defines the settings that should be used with
-     *                    the video encoder.
-     * @param surfaceSize The size of the surface required by the camera for the video encoder.
+     * @param mimeType      The mime type for the video encoder
+     * @param inputTimebase The time base of the input frame.
+     * @param videoSpec     The {@link VideoSpec} which defines the settings that should be used
+     *                      with the video encoder.
+     * @param surfaceSize   The size of the surface required by the camera for the video encoder.
      */
     public VideoEncoderConfigDefaultResolver(@NonNull String mimeType,
-            @NonNull VideoSpec videoSpec, @NonNull Size surfaceSize,
-            @Nullable Range<Integer> expectedFrameRateRange) {
+            @NonNull Timebase inputTimebase, @NonNull VideoSpec videoSpec,
+            @NonNull Size surfaceSize, @Nullable Range<Integer> expectedFrameRateRange) {
         mMimeType = mimeType;
+        mInputTimebase = inputTimebase;
         mVideoSpec = videoSpec;
         mSurfaceSize = surfaceSize;
         mExpectedFrameRateRange = expectedFrameRateRange;
@@ -85,6 +90,7 @@
 
         return VideoEncoderConfig.builder()
                 .setMimeType(mMimeType)
+                .setInputTimebase(mInputTimebase)
                 .setResolution(mSurfaceSize)
                 .setBitrate(resolvedBitrate)
                 .setFrameRate(resolvedFrameRate)
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java
index 3af219d..67c7000 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderConfig.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Timebase;
 
 import com.google.auto.value.AutoValue;
 
@@ -54,6 +55,10 @@
     @Override
     public abstract int getProfile();
 
+    @Override
+    @NonNull
+    public abstract Timebase getInputTimebase();
+
     /** Gets the bitrate. */
     public abstract int getBitrate();
 
@@ -103,6 +108,10 @@
         @NonNull
         public abstract Builder setProfile(int profile);
 
+        /** Sets the source timebase. */
+        @NonNull
+        public abstract Builder setInputTimebase(@NonNull Timebase timebase);
+
         /** Sets the bitrate. */
         @NonNull
         public abstract Builder setBitrate(int bitrate);
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfo.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfo.java
new file mode 100644
index 0000000..1389a93
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfo.java
@@ -0,0 +1,33 @@
+/*
+ * 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.video.internal.encoder;
+
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * AudioEncoderInfo provides audio encoder related information and capabilities.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface AudioEncoderInfo extends EncoderInfo {
+
+    /** Returns the range of supported bitrates in bits/second. */
+    @NonNull
+    Range<Integer> getBitrateRange();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImpl.java
new file mode 100644
index 0000000..fccd846
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/AudioEncoderInfoImpl.java
@@ -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.camera.video.internal.encoder;
+
+import android.media.MediaCodecInfo;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Objects;
+
+/**
+ * AudioEncoderInfoImpl provides audio encoder related information and capabilities.
+ *
+ * <p>The implementation wraps and queries {@link MediaCodecInfo} relevant capability classes
+ * such as {@link MediaCodecInfo.CodecCapabilities}, {@link MediaCodecInfo.EncoderCapabilities}
+ * and {@link MediaCodecInfo.AudioCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class AudioEncoderInfoImpl extends EncoderInfoImpl implements AudioEncoderInfo {
+
+    private final MediaCodecInfo.AudioCapabilities mAudioCapabilities;
+
+    /**
+     * Returns an AudioEncoderInfoImpl from a AudioEncoderConfig.
+     *
+     * <p>The input AudioEncoderConfig is used to find the corresponding encoder.
+     *
+     * @throws InvalidConfigException if the encoder is not found.
+     */
+    @NonNull
+    public static AudioEncoderInfoImpl from(@NonNull AudioEncoderConfig encoderConfig)
+            throws InvalidConfigException {
+        return new AudioEncoderInfoImpl(findCodecAndGetCodecInfo(encoderConfig),
+                encoderConfig.getMimeType());
+    }
+
+    AudioEncoderInfoImpl(@NonNull MediaCodecInfo codecInfo, @NonNull String mime)
+            throws InvalidConfigException {
+        super(codecInfo, mime);
+        mAudioCapabilities = Objects.requireNonNull(mCodecCapabilities.getAudioCapabilities());
+    }
+
+    @NonNull
+    @Override
+    public Range<Integer> getBitrateRange() {
+        return mAudioCapabilities.getBitrateRange();
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
index 9176234..95e5de1 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/Encoder.java
@@ -39,6 +39,10 @@
     @NonNull
     EncoderInput getInput();
 
+    /** Returns the EncoderInfo which provides encoder's information and capabilities. */
+    @NonNull
+    EncoderInfo getEncoderInfo();
+
     /**
      * Starts the encoder.
      *
@@ -74,7 +78,7 @@
     /**
      * Pauses the encoder.
      *
-     * <p>{@link #pause} only work between {@link #start} and {@link #stop}.
+     * <p>{@code pause} only work between {@link #start} and {@link #stop}.
      * Once the encoder is paused, it will drop the input data until {@link #start} is invoked
      * again.
      */
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
index 5a1a228..b3596b2 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderConfig.java
@@ -21,6 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.impl.CamcorderProfileProxy;
+import androidx.camera.core.impl.Timebase;
 
 /**
  * The configuration represents the required parameters to configure an encoder.
@@ -38,7 +39,7 @@
      *
      * <p>For example, "video/avc" for a video encoder and "audio/mp4a-latm" for an audio encoder.
      *
-     * @see {@link MediaFormat}
+     * @see MediaFormat
      */
     @NonNull
     String getMimeType();
@@ -55,6 +56,12 @@
     int getProfile();
 
     /**
+     * Gets the input timebase.
+     */
+    @NonNull
+    Timebase getInputTimebase();
+
+    /**
      * Transfers the config to a {@link MediaFormat}.
      *
      * @return the result {@link MediaFormat}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index d6214b1..7a966e4 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -28,7 +28,7 @@
 
 import android.annotation.SuppressLint;
 import android.media.MediaCodec;
-import android.media.MediaCodecList;
+import android.media.MediaCodecInfo;
 import android.media.MediaFormat;
 import android.os.Bundle;
 import android.util.Range;
@@ -155,6 +155,7 @@
     final MediaCodec mMediaCodec;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final EncoderInput mEncoderInput;
+    private final EncoderInfo mEncoderInfo;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Executor mEncoderExecutor;
     private final ListenableFuture<Void> mReleasedFuture;
@@ -228,10 +229,10 @@
 
         mMediaFormat = encoderConfig.toMediaFormat();
         Logger.d(mTag, "mMediaFormat = " + mMediaFormat);
-        mMediaCodec = mEncoderFinder.findEncoder(mMediaFormat,
-                new MediaCodecList(MediaCodecList.ALL_CODECS));
+        mMediaCodec = mEncoderFinder.findEncoder(mMediaFormat);
         Logger.i(mTag, "Selected encoder: " + mMediaCodec.getName());
-
+        mEncoderInfo = createEncoderInfo(mIsVideoEncoder, mMediaCodec.getCodecInfo(),
+                encoderConfig.getMimeType());
         try {
             reset();
         } catch (MediaCodec.CodecException e) {
@@ -285,6 +286,12 @@
         return mEncoderInput;
     }
 
+    @NonNull
+    @Override
+    public EncoderInfo getEncoderInfo() {
+        return mEncoderInfo;
+    }
+
     /**
      * Starts the encoder.
      *
@@ -952,6 +959,13 @@
         }
     }
 
+    @NonNull
+    private static EncoderInfo createEncoderInfo(boolean isVideoEncoder,
+            @NonNull MediaCodecInfo codecInfo, @NonNull String mime) throws InvalidConfigException {
+        return isVideoEncoder ? new VideoEncoderInfoImpl(codecInfo, mime)
+                : new AudioEncoderInfoImpl(codecInfo, mime);
+    }
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     static long generatePresentationTimeUs() {
         return TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfo.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfo.java
new file mode 100644
index 0000000..b7add59
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfo.java
@@ -0,0 +1,28 @@
+/*
+ * 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.video.internal.encoder;
+
+import androidx.annotation.NonNull;
+
+/**
+ * EncoderInfo provides encoder related information and capabilities.
+ */
+public interface EncoderInfo {
+    /** Returns the name of the encoder. */
+    @NonNull
+    String getName();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfoImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfoImpl.java
new file mode 100644
index 0000000..6f2968b
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderInfoImpl.java
@@ -0,0 +1,67 @@
+/*
+ * 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.video.internal.encoder;
+
+import android.media.MediaCodec;
+import android.media.MediaCodecInfo;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.video.internal.workaround.EncoderFinder;
+
+import java.util.Objects;
+
+/**
+ * An EncoderInfo base implementation providing encoder related information and capabilities.
+ *
+ * <p>The implementation wraps and queries {@link MediaCodecInfo} relevant capability classes
+ * such as {@link MediaCodecInfo.CodecCapabilities} and
+ * {@link MediaCodecInfo.EncoderCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public abstract class EncoderInfoImpl implements EncoderInfo {
+    private final MediaCodecInfo mMediaCodecInfo;
+    protected final MediaCodecInfo.CodecCapabilities mCodecCapabilities;
+
+    EncoderInfoImpl(@NonNull MediaCodecInfo codecInfo, @NonNull String mime)
+            throws InvalidConfigException {
+        mMediaCodecInfo = codecInfo;
+        try {
+            mCodecCapabilities = Objects.requireNonNull(codecInfo.getCapabilitiesForType(mime));
+        } catch (RuntimeException e) {
+            // MediaCodecInfo.getCapabilitiesForType(mime) will throw exception if the mime is not
+            // supported.
+            throw new InvalidConfigException("Unable to get CodecCapabilities for mime: " + mime,
+                    e);
+        }
+    }
+
+    @Override
+    @NonNull
+    public String getName() {
+        return mMediaCodecInfo.getName();
+    }
+
+    @NonNull
+    static MediaCodecInfo findCodecAndGetCodecInfo(@NonNull EncoderConfig encoderConfig)
+            throws InvalidConfigException {
+        MediaCodec codec = new EncoderFinder().findEncoder(encoderConfig.toMediaFormat());
+        MediaCodecInfo codecInfo = codec.getCodecInfo();
+        codec.release();
+        return codecInfo;
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java
index d3ff4af..dfe5a49 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderConfig.java
@@ -22,6 +22,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Timebase;
 
 import com.google.auto.value.AutoValue;
 
@@ -54,6 +55,10 @@
     @Override
     public abstract int getProfile();
 
+    @Override
+    @NonNull
+    public abstract Timebase getInputTimebase();
+
     /** Gets the resolution. */
     @NonNull
     public abstract Size getResolution();
@@ -102,6 +107,10 @@
         @NonNull
         public abstract Builder setProfile(int profile);
 
+        /** Sets the source timebase. */
+        @NonNull
+        public abstract Builder setInputTimebase(@NonNull Timebase timebase);
+
         /** Sets the resolution. */
         @NonNull
         public abstract Builder setResolution(@NonNull Size resolution);
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfo.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfo.java
new file mode 100644
index 0000000..03ef5f2
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfo.java
@@ -0,0 +1,70 @@
+/*
+ * 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.video.internal.encoder;
+
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+/**
+ * VideoEncoderInfo provides video encoder related information and capabilities.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public interface VideoEncoderInfo extends EncoderInfo {
+    /** Returns the range of supported video widths. */
+    @NonNull
+    Range<Integer> getSupportedWidths();
+
+    /** Returns the range of supported video heights. */
+    @NonNull
+    Range<Integer> getSupportedHeights();
+
+    /**
+     * Returns the range of supported video widths for a video height.
+     *
+     * @throws IllegalArgumentException if height is not supported.
+     * @see #getSupportedHeights()
+     * @see #getHeightAlignment()
+     */
+    @NonNull
+    Range<Integer> getSupportedWidthsFor(int height);
+
+    /**
+     * Returns the range of supported video heights for a video width.
+     *
+     * @throws IllegalArgumentException if width is not supported.
+     * @see #getSupportedWidths()
+     * @see #getWidthAlignment()
+     */
+    @NonNull
+    Range<Integer> getSupportedHeightsFor(int width);
+
+    /**
+     * Returns the alignment requirement for video width (in pixels).
+     *
+     * <p>This is usually a power-of-2 value that video width must be a multiple of.
+     */
+    int getWidthAlignment();
+
+    /**
+     * Returns the alignment requirement for video height (in pixels).
+     *
+     * <p>This is usually a power-of-2 value that video height must be a multiple of.
+     */
+    int getHeightAlignment();
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImpl.java
new file mode 100644
index 0000000..5970932
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/VideoEncoderInfoImpl.java
@@ -0,0 +1,108 @@
+/*
+ * 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.video.internal.encoder;
+
+import android.media.MediaCodecInfo;
+import android.util.Range;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import java.util.Objects;
+
+/**
+ * VideoEncoderInfoImpl provides video encoder related information and capabilities.
+ *
+ * <p>The implementation wraps and queries {@link MediaCodecInfo} relevant capability classes
+ * such as {@link MediaCodecInfo.CodecCapabilities}, {@link MediaCodecInfo.EncoderCapabilities}
+ * and {@link MediaCodecInfo.VideoCapabilities}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class VideoEncoderInfoImpl extends EncoderInfoImpl implements VideoEncoderInfo {
+
+    private final MediaCodecInfo.VideoCapabilities mVideoCapabilities;
+    /**
+     * Returns a VideoEncoderInfoImpl from a VideoEncoderConfig.
+     *
+     * <p>The input VideoEncoderConfig is used to find the corresponding encoder.
+     *
+     * @throws InvalidConfigException if the encoder is not found.
+     */
+    @NonNull
+    public static VideoEncoderInfoImpl from(@NonNull VideoEncoderConfig encoderConfig)
+            throws InvalidConfigException {
+        return new VideoEncoderInfoImpl(findCodecAndGetCodecInfo(encoderConfig),
+                encoderConfig.getMimeType());
+    }
+
+    VideoEncoderInfoImpl(@NonNull MediaCodecInfo codecInfo, @NonNull String mime)
+            throws InvalidConfigException {
+        super(codecInfo, mime);
+        mVideoCapabilities = Objects.requireNonNull(mCodecCapabilities.getVideoCapabilities());
+    }
+
+    @NonNull
+    @Override
+    public Range<Integer> getSupportedWidths() {
+        return mVideoCapabilities.getSupportedWidths();
+    }
+
+    @NonNull
+    @Override
+    public Range<Integer> getSupportedHeights() {
+        return mVideoCapabilities.getSupportedHeights();
+    }
+
+    @NonNull
+    @Override
+    public Range<Integer> getSupportedWidthsFor(int height) {
+        try {
+            return mVideoCapabilities.getSupportedWidthsFor(height);
+        } catch (Throwable t) {
+            throw toIllegalArgumentException(t);
+        }
+    }
+
+    @NonNull
+    @Override
+    public Range<Integer> getSupportedHeightsFor(int width) {
+        try {
+            return mVideoCapabilities.getSupportedHeightsFor(width);
+        } catch (Throwable t) {
+            throw toIllegalArgumentException(t);
+        }
+    }
+
+    @Override
+    public int getWidthAlignment() {
+        return mVideoCapabilities.getWidthAlignment();
+    }
+
+    @Override
+    public int getHeightAlignment() {
+        return mVideoCapabilities.getHeightAlignment();
+    }
+
+    @NonNull
+    private static IllegalArgumentException toIllegalArgumentException(@NonNull Throwable t) {
+        if (t instanceof IllegalArgumentException) {
+            return (IllegalArgumentException) t;
+        } else {
+            return new IllegalArgumentException(t);
+        }
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
index e2ef908..d93bdef 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/workaround/EncoderFinder.java
@@ -70,9 +70,9 @@
      * format.
      */
     @NonNull
-    public MediaCodec findEncoder(@NonNull MediaFormat mediaFormat,
-            @NonNull MediaCodecList mediaCodecList) throws InvalidConfigException {
+    public MediaCodec findEncoder(@NonNull MediaFormat mediaFormat) throws InvalidConfigException {
         MediaCodec codec;
+        MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.ALL_CODECS);
         String encoderName = findEncoderForFormat(mediaFormat, mediaCodecList);
         try {
             if (TextUtils.isEmpty(encoderName)) {
@@ -105,7 +105,7 @@
         try {
             // If the frame rate value is assigned, keep it and restore it later.
             if (mShouldRemoveKeyFrameRate && mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
-                tempFrameRate = Integer.valueOf(mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE));
+                tempFrameRate = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
                 // Reset frame rate value in API 21.
                 mediaFormat.setString(MediaFormat.KEY_FRAME_RATE, null);
             }
@@ -116,8 +116,7 @@
             //  be added.
             if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M && mediaFormat.containsKey(
                     MediaFormat.KEY_AAC_PROFILE)) {
-                tempAacProfile = Integer.valueOf(
-                        mediaFormat.getInteger(MediaFormat.KEY_AAC_PROFILE));
+                tempAacProfile = mediaFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
                 mediaFormat.setString(MediaFormat.KEY_AAC_PROFILE, null);
             }
 
@@ -130,12 +129,12 @@
         } finally {
             // Restore the frame rate value.
             if (tempFrameRate != null) {
-                mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, tempFrameRate.intValue());
+                mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, tempFrameRate);
             }
 
             // Restore the aac profile value.
             if (tempAacProfile != null) {
-                mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, tempAacProfile.intValue());
+                mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, tempAacProfile);
             }
         }
     }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCapabilitiesTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCapabilitiesTest.kt
index 29f8235..b77ffb6 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCapabilitiesTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCapabilitiesTest.kt
@@ -116,4 +116,46 @@
         assertThat(videoCapabilities.findHighestSupportedQualityFor(exactSize720p))
             .isEqualTo(Quality.HD)
     }
+
+    @Test
+    fun findHighestSupportedCamcorderProfileFor_returnsHigherProfile() {
+        val videoCapabilities = VideoCapabilities.from(cameraInfo)
+        // Create a size between 720p and 2160p
+        val (width720p, height720p) = CamcorderProfileUtil.RESOLUTION_720P
+        val inBetweenSize = Size(width720p + 10, height720p)
+
+        assertThat(videoCapabilities.findHighestSupportedCamcorderProfileFor(inBetweenSize))
+            .isEqualTo(PROFILE_2160P)
+    }
+
+    @Test
+    fun findHighestSupportedCamcorderProfileFor_returnsHighestProfile_whenAboveHighest() {
+        val videoCapabilities = VideoCapabilities.from(cameraInfo)
+        // Create a size between greater than the max quality (UHD)
+        val (width2160p, height2160p) = CamcorderProfileUtil.RESOLUTION_2160P
+        val aboveHighestSize = Size(width2160p + 10, height2160p)
+
+        assertThat(videoCapabilities.findHighestSupportedCamcorderProfileFor(aboveHighestSize))
+            .isEqualTo(PROFILE_2160P)
+    }
+
+    @Test
+    fun findHighestSupportedCamcorderProfileFor_returnsLowestProfile_whenBelowLowest() {
+        val videoCapabilities = VideoCapabilities.from(cameraInfo)
+        // Create a size below the lowest quality (HD)
+        val (width720p, height720p) = CamcorderProfileUtil.RESOLUTION_720P
+        val belowLowestSize = Size(width720p - 10, height720p)
+
+        assertThat(videoCapabilities.findHighestSupportedCamcorderProfileFor(belowLowestSize))
+            .isEqualTo(PROFILE_720P)
+    }
+
+    @Test
+    fun findHighestSupportedCamcorderProfileFor_returnsExactProfile_whenExactSizeGiven() {
+        val videoCapabilities = VideoCapabilities.from(cameraInfo)
+        val exactSize720p = CamcorderProfileUtil.RESOLUTION_720P
+
+        assertThat(videoCapabilities.findHighestSupportedCamcorderProfileFor(exactSize720p))
+            .isEqualTo(PROFILE_720P)
+    }
 }
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index dc31faf..cac814d 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -17,10 +17,13 @@
 package androidx.camera.video
 
 import android.content.Context
+import android.graphics.Rect
 import android.os.Build
 import android.os.Looper
+import android.util.Range
 import android.util.Size
 import android.view.Surface
+import androidx.arch.core.util.Function
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.SurfaceRequest
@@ -30,8 +33,11 @@
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.MutableStateObservable
 import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.Timebase
+import androidx.camera.core.impl.utils.TransformUtils.rectToSize
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.processing.SurfaceEffectInternal
 import androidx.camera.testing.CamcorderProfileUtil
 import androidx.camera.testing.CamcorderProfileUtil.PROFILE_1080P
 import androidx.camera.testing.CamcorderProfileUtil.PROFILE_2160P
@@ -52,10 +58,13 @@
 import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import androidx.camera.video.StreamInfo.StreamState
 import androidx.camera.video.impl.VideoCaptureConfig
-import androidx.core.util.Consumer
+import androidx.camera.video.internal.encoder.FakeVideoEncoderInfo
+import androidx.camera.video.internal.encoder.VideoEncoderConfig
+import androidx.camera.video.internal.encoder.VideoEncoderInfo
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.TimeUnit
 import org.junit.After
 import org.junit.Assert.assertThrows
 import org.junit.Test
@@ -67,7 +76,6 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import java.util.concurrent.TimeUnit
 
 private val ANY_SIZE = Size(640, 480)
 private const val CAMERA_ID_0 = "0"
@@ -106,12 +114,8 @@
 
     @Test
     fun setTargetResolution_throwsException() {
-        val videoOutput = createVideoOutput()
-
         assertThrows(UnsupportedOperationException::class.java) {
-            VideoCapture.Builder(videoOutput)
-                .setTargetResolution(ANY_SIZE)
-                .build()
+            createVideoCapture(targetResolution = ANY_SIZE)
         }
     }
 
@@ -121,7 +125,7 @@
         val videoOutput = createVideoOutput()
 
         // Act.
-        val videoCapture = VideoCapture.withOutput(videoOutput)
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Assert.
         assertThat(videoCapture.output).isEqualTo(videoOutput)
@@ -134,10 +138,10 @@
         createCameraUseCaseAdapter()
 
         var surfaceRequest: SurfaceRequest? = null
-        val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest = it })
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoOutput = createVideoOutput(surfaceRequestListener = { request, _ ->
+            surfaceRequest = request
+        })
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -147,15 +151,67 @@
     }
 
     @Test
+    fun addUseCases_cameraIsUptime_requestIsUptime() {
+        testTimebase(cameraTimebase = Timebase.UPTIME, expectedTimebase = Timebase.UPTIME)
+    }
+
+    @Test
+    fun addUseCases_cameraIsRealtime_requestIsUptime() {
+        testTimebase(cameraTimebase = Timebase.REALTIME, expectedTimebase = Timebase.UPTIME)
+    }
+
+    @Test
+    fun addUseCasesWithSurfaceEffect__cameraIsUptime_requestIsUptime() {
+        testTimebase(
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor()),
+            cameraTimebase = Timebase.UPTIME,
+            expectedTimebase = Timebase.UPTIME
+        )
+    }
+
+    @Test
+    fun addUseCasesWithSurfaceEffect__cameraIsRealtime_requestIsRealtime() {
+        testTimebase(
+            effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor()),
+            cameraTimebase = Timebase.REALTIME,
+            expectedTimebase = Timebase.REALTIME
+        )
+    }
+
+    private fun testTimebase(
+        effect: SurfaceEffectInternal? = null,
+        cameraTimebase: Timebase,
+        expectedTimebase: Timebase
+    ) {
+        // Arrange.
+        setupCamera(timebase = cameraTimebase)
+        createCameraUseCaseAdapter()
+
+        var timebase: Timebase? = null
+        val videoOutput = createVideoOutput(surfaceRequestListener = { _, tb ->
+            timebase = tb
+        })
+        val videoCapture = VideoCapture.Builder(videoOutput)
+            .setSessionOptionUnpacker { _, _ -> }
+            .build()
+        effect?.let { videoCapture.setEffect(it) }
+
+        // Act.
+        addAndAttachUseCases(videoCapture)
+        shadowOf(Looper.getMainLooper()).idle()
+
+        // Assert.
+        assertThat(timebase).isEqualTo(expectedTimebase)
+    }
+
+    @Test
     fun addUseCases_withNullMediaSpec_throwException() {
         // Arrange.
         setupCamera()
         createCameraUseCaseAdapter()
 
         val videoOutput = createVideoOutput(mediaSpec = null)
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Assert.
         assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
@@ -189,9 +245,7 @@
                     it.setQualitySelector(QualitySelector.from(quality))
                 }.build()
             )
-            val videoCapture = VideoCapture.Builder(videoOutput)
-                .setSessionOptionUnpacker { _, _ -> }
-                .build()
+            val videoCapture = createVideoCapture(videoOutput)
 
             // Act.
             addAndAttachUseCases(videoCapture)
@@ -238,9 +292,7 @@
                 )
             }.build()
         )
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -265,9 +317,7 @@
                 it.setQualitySelector(QualitySelector.from(Quality.FHD))
             }.build()
         )
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Assert.
         assertThrows(CameraUseCaseAdapter.CameraException::class.java) {
@@ -287,9 +337,7 @@
                 it.setQualitySelector(QualitySelector.from(Quality.UHD))
             }.build()
         )
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -309,15 +357,13 @@
         createCameraUseCaseAdapter()
 
         var surfaceResult: SurfaceRequest.Result? = null
-        val videoOutput = createVideoOutput { surfaceRequest ->
+        val videoOutput = createVideoOutput { surfaceRequest, _ ->
             surfaceRequest.provideSurface(
                 mock(Surface::class.java),
                 CameraXExecutors.directExecutor()
             ) { surfaceResult = it }
         }
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -338,7 +384,7 @@
     @Test
     fun setTargetRotation_rotationIsChanged() {
         // Arrange.
-        val videoCapture = VideoCapture.withOutput(createVideoOutput())
+        val videoCapture = createVideoCapture()
 
         // Act.
         videoCapture.targetRotation = Surface.ROTATION_180
@@ -354,17 +400,15 @@
         createCameraUseCaseAdapter()
         val listener = mock(SurfaceRequest.TransformationInfoListener::class.java)
         val videoOutput = createVideoOutput(
-            surfaceRequestListener = {
-                it.setTransformationInfoListener(
+            surfaceRequestListener = { surfaceRequest, _ ->
+                surfaceRequest.setTransformationInfoListener(
                     CameraXExecutors.directExecutor(),
                     listener
                 )
             }
         )
 
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -380,7 +424,7 @@
         createCameraUseCaseAdapter()
         var transformationInfo: SurfaceRequest.TransformationInfo? = null
         val videoOutput = createVideoOutput(
-            surfaceRequestListener = { surfaceRequest ->
+            surfaceRequestListener = { surfaceRequest, _ ->
                 surfaceRequest.setTransformationInfoListener(
                     CameraXExecutors.directExecutor()
                 ) {
@@ -388,10 +432,7 @@
                 }
             }
         )
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setTargetRotation(Surface.ROTATION_90)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput, targetRotation = Surface.ROTATION_90)
 
         // Act.
         addAndAttachUseCases(videoCapture)
@@ -447,7 +488,7 @@
         createCameraUseCaseAdapter()
         val effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor(), false)
         var appSurfaceReadyToRelease = false
-        val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest ->
+        val videoOutput = createVideoOutput(surfaceRequestListener = { surfaceRequest, _ ->
             surfaceRequest.provideSurface(
                 mock(Surface::class.java),
                 CameraXExecutors.mainThreadExecutor()
@@ -455,9 +496,7 @@
                 appSurfaceReadyToRelease = true
             }
         })
-        val videoCapture = VideoCapture.Builder(videoOutput)
-            .setSessionOptionUnpacker { _, _ -> }
-            .build()
+        val videoCapture = createVideoCapture(videoOutput)
 
         // Act: bind and provide Surface.
         videoCapture.setEffect(effect)
@@ -490,6 +529,108 @@
         assertThat(appSurfaceReadyToRelease).isTrue()
     }
 
+    @Test
+    fun adjustCropRect_noAdjustment() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(8, 8, 808, 608),
+            expectedCropRect = Rect(8, 8, 808, 608),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_toSmallerSize() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(8, 8, 811, 608), // 803x600 -> 800x600
+            expectedCropRect = Rect(9, 8, 809, 608),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_toLargerSize() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(8, 8, 805, 608), // 797x600 -> 800x600
+            expectedCropRect = Rect(6, 8, 806, 608),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_toLargerSize_fromTopLeft() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(0, 0, 797, 600), // 797x600 -> 800x600
+            expectedCropRect = Rect(0, 0, 800, 600),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_toLargerSize_fromBottomRight() {
+        testAdjustCropRectToValidSize(
+            // Quality.HD maps to 1280x720 (4:3)
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(1280 - 797, 720 - 600, 1280, 720), // 797x600 -> 800x600
+            expectedCropRect = Rect(1280 - 800, 720 - 600, 1280, 720),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_clampBySupportedWidthsHeights() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(
+                widthAlignment = 8,
+                heightAlignment = 8,
+                supportedWidths = Range(80, 800),
+                supportedHeights = Range(100, 800),
+            ),
+            cropRect = Rect(8, 8, 48, 48), // 40x40
+            expectedCropRect = Rect(0, 0, 80, 100),
+        )
+    }
+
+    @Test
+    fun adjustCropRect_toSmallestDimensionChange() {
+        testAdjustCropRectToValidSize(
+            videoEncoderInfo = createVideoEncoderInfo(widthAlignment = 8, heightAlignment = 8),
+            cropRect = Rect(8, 8, 811, 607), // 803x599 -> 800x600
+            expectedCropRect = Rect(9, 7, 809, 607),
+        )
+    }
+
+    private fun testAdjustCropRectToValidSize(
+        quality: Quality = Quality.HD, // Quality.HD maps to 1280x720 (4:3)
+        videoEncoderInfo: VideoEncoderInfo = createVideoEncoderInfo(),
+        cropRect: Rect,
+        expectedCropRect: Rect,
+    ) {
+        // Arrange.
+        setupCamera()
+        createCameraUseCaseAdapter()
+        var surfaceRequest: SurfaceRequest? = null
+        val videoOutput = createVideoOutput(
+            mediaSpec = MediaSpec.builder().configureVideo {
+                it.setQualitySelector(QualitySelector.from(quality))
+            }.build(),
+            surfaceRequestListener = { request, _ -> surfaceRequest = request }
+        )
+        val videoCapture = createVideoCapture(
+            videoOutput, videoEncoderInfoFinder = { videoEncoderInfo }
+        )
+        val effect = FakeSurfaceEffectInternal(CameraXExecutors.mainThreadExecutor(), false)
+        videoCapture.setEffect(effect)
+        videoCapture.setViewPortCropRect(cropRect)
+
+        // Act.
+        addAndAttachUseCases(videoCapture)
+        shadowOf(Looper.getMainLooper()).idle()
+
+        // Assert.
+        assertThat(surfaceRequest).isNotNull()
+        assertThat(surfaceRequest!!.resolution).isEqualTo(rectToSize(expectedCropRect))
+        assertThat(videoCapture.cameraSettableSurface.cropRect).isEqualTo(expectedCropRect)
+    }
+
     private fun assertSupportedResolutions(
         videoCapture: VideoCapture<out VideoOutput>,
         vararg expectedResolutions: Size
@@ -502,19 +643,35 @@
         }
     }
 
+    private fun createVideoEncoderInfo(
+        widthAlignment: Int = 2,
+        heightAlignment: Int = 2,
+        supportedWidths: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+        supportedHeights: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+    ): VideoEncoderInfo {
+        return FakeVideoEncoderInfo(
+            _widthAlignment = widthAlignment,
+            _heightAlignment = heightAlignment,
+            _supportedWidths = supportedWidths,
+            _supportedHeights = supportedHeights,
+        )
+    }
+
     private fun createVideoOutput(
         streamState: StreamState = StreamState.ACTIVE,
         mediaSpec: MediaSpec? = MediaSpec.builder().build(),
-        surfaceRequestListener: Consumer<SurfaceRequest> = Consumer { it.willNotProvideSurface() }
-    ): TestVideoOutput = TestVideoOutput(streamState, mediaSpec) {
-        surfaceRequestsToRelease.add(it)
-        surfaceRequestListener.accept(it)
+        surfaceRequestListener: (SurfaceRequest, Timebase) -> Unit = { surfaceRequest, _ ->
+            surfaceRequest.willNotProvideSurface()
+        }
+    ): TestVideoOutput = TestVideoOutput(streamState, mediaSpec) { surfaceRequest, timebase ->
+        surfaceRequestsToRelease.add(surfaceRequest)
+        surfaceRequestListener.invoke(surfaceRequest, timebase)
     }
 
     private class TestVideoOutput constructor(
         streamState: StreamState,
         mediaSpec: MediaSpec?,
-        val surfaceRequestCallback: Consumer<SurfaceRequest>
+        val surfaceRequestCallback: (SurfaceRequest, Timebase) -> Unit
     ) : VideoOutput {
 
         private val streamInfoObservable: MutableStateObservable<StreamInfo> =
@@ -529,7 +686,11 @@
             MutableStateObservable.withInitialState(mediaSpec)
 
         override fun onSurfaceRequested(surfaceRequest: SurfaceRequest) {
-            surfaceRequestCallback.accept(surfaceRequest)
+            surfaceRequestCallback.invoke(surfaceRequest, Timebase.UPTIME)
+        }
+
+        override fun onSurfaceRequested(surfaceRequest: SurfaceRequest, timebase: Timebase) {
+            surfaceRequestCallback.invoke(surfaceRequest, timebase)
         }
 
         override fun getStreamInfo(): Observable<StreamInfo> = streamInfoObservable
@@ -552,13 +713,28 @@
             CameraUtil.createCameraUseCaseAdapter(context, CameraSelector.DEFAULT_BACK_CAMERA)
     }
 
+    private fun createVideoCapture(
+        videoOutput: VideoOutput = createVideoOutput(),
+        targetRotation: Int? = null,
+        targetResolution: Size? = null,
+        videoEncoderInfoFinder: Function<VideoEncoderConfig, VideoEncoderInfo>? = null,
+    ): VideoCapture<VideoOutput> = VideoCapture.Builder(videoOutput)
+        .setSessionOptionUnpacker { _, _ -> }
+        .apply {
+            targetRotation?.let { setTargetRotation(it) }
+            targetResolution?.let { setTargetResolution(it) }
+            videoEncoderInfoFinder?.let { setVideoEncoderInfoFinder(it) }
+        }.build()
+
     private fun setupCamera(
         cameraId: String = CAMERA_ID_0,
-        vararg profiles: CamcorderProfileProxy = CAMERA_0_PROFILES
+        vararg profiles: CamcorderProfileProxy = CAMERA_0_PROFILES,
+        timebase: Timebase = Timebase.UPTIME,
     ) {
         val cameraInfo = FakeCameraInfoInternal(cameraId).apply {
             camcorderProfileProvider =
                 FakeCamcorderProfileProvider.Builder().addProfile(*profiles).build()
+            setTimebase(timebase)
         }
         val camera = FakeCamera(cameraId, null, cameraInfo)
 
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt
new file mode 100644
index 0000000..f78553d
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeEncoderInfo.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.video.internal.encoder
+
+open class FakeEncoderInfo(var _name: String = "fake.encoder") : EncoderInfo {
+    override fun getName(): String = _name
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.kt
new file mode 100644
index 0000000..1b05685
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/encoder/FakeVideoEncoderInfo.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.camera.video.internal.encoder
+
+import android.util.Range
+
+class FakeVideoEncoderInfo(
+    var _supportedWidths: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+    var _supportedHeights: Range<Int> = Range.create(0, Integer.MAX_VALUE),
+    var _widthAlignment: Int = 2,
+    var _heightAlignment: Int = 2,
+) : FakeEncoderInfo(), VideoEncoderInfo {
+
+    override fun getSupportedWidths(): Range<Int> {
+        return _supportedWidths
+    }
+
+    override fun getSupportedHeights(): Range<Int> {
+        return _supportedHeights
+    }
+
+    override fun getSupportedWidthsFor(height: Int): Range<Int> {
+        return _supportedWidths
+    }
+
+    override fun getSupportedHeightsFor(width: Int): Range<Int> {
+        return _supportedHeights
+    }
+
+    override fun getWidthAlignment(): Int {
+       return _widthAlignment
+    }
+
+    override fun getHeightAlignment(): Int {
+        return _heightAlignment
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 22c5014..5416004 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -81,6 +81,7 @@
 import androidx.core.app.ActivityCompat;
 import androidx.core.content.ContextCompat;
 import androidx.core.math.MathUtils;
+import androidx.lifecycle.Lifecycle;
 import androidx.test.espresso.idling.CountingIdlingResource;
 
 import com.google.common.base.Preconditions;
@@ -497,6 +498,13 @@
                 new FutureCallback<ExtensionsManager>() {
                     @Override
                     public void onSuccess(@Nullable ExtensionsManager extensionsManager) {
+                        // There might be timing issue that the activity has been destroyed when
+                        // the onSuccess callback is received. Skips the afterward flow when the
+                        // situation happens.
+                        if (CameraExtensionsActivity.this.getLifecycle().getCurrentState()
+                                == Lifecycle.State.DESTROYED) {
+                            return;
+                        }
                         mExtensionsManager = extensionsManager;
                         if (!bindUseCasesWithCurrentExtensionMode()) {
                             bindUseCasesWithNextExtensionMode();
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/climate/AutomotiveCarClimate.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/climate/AutomotiveCarClimate.java
index 52873967..f12c933 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/climate/AutomotiveCarClimate.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/climate/AutomotiveCarClimate.java
@@ -91,8 +91,7 @@
 
     @VisibleForTesting
     static final float DEFAULT_SAMPLE_RATE_HZ = 5f;
-    @VisibleForTesting
-    static final int HVAC_ELECTRIC_DEFROSTER = 320865556;
+    public static final int HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID = 320865556;
 
     // TODO(b/240347704): replace FEATURE_HVAC_ELECTRIC_DEFROSTER value with
     //  HVAC_ELECTRIC_DEFROSTER_ON if it becomes available.
@@ -114,7 +113,7 @@
                     .put(FEATURE_HVAC_DUAL_MODE, HVAC_DUAL_ON)
                     .put(FEATURE_HVAC_DEFROSTER, HVAC_DEFROSTER)
                     .put(FEATURE_HVAC_MAX_DEFROSTER, HVAC_MAX_DEFROST_ON)
-                    .put(FEATURE_HVAC_ELECTRIC_DEFROSTER, HVAC_ELECTRIC_DEFROSTER)
+                    .put(FEATURE_HVAC_ELECTRIC_DEFROSTER, HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID)
                     .buildOrThrow();
 
     private final Map<CarClimateStateCallback, OnCarPropertyResponseListener> mListenerMap =
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
index 891a4d9..c2ba63e0 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/common/PropertyUtils.java
@@ -17,6 +17,7 @@
 package androidx.car.app.hardware.common;
 
 import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+import static androidx.car.app.hardware.climate.AutomotiveCarClimate.HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID;
 import static androidx.car.app.hardware.common.CarUnit.IMPERIAL_GALLON;
 import static androidx.car.app.hardware.common.CarUnit.LITER;
 import static androidx.car.app.hardware.common.CarUnit.MILLILITER;
@@ -131,6 +132,7 @@
             append(VehiclePropertyIds.HVAC_DUAL_ON, CAR_PERMISSION_CLIMATE_CONTROL);
             append(VehiclePropertyIds.HVAC_DEFROSTER, CAR_PERMISSION_CLIMATE_CONTROL);
             append(VehiclePropertyIds.HVAC_MAX_DEFROST_ON, CAR_PERMISSION_CLIMATE_CONTROL);
+            append(HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID, CAR_PERMISSION_CLIMATE_CONTROL);
         }
     };
 
@@ -191,6 +193,7 @@
             append(VehiclePropertyIds.HVAC_DUAL_ON, CAR_PERMISSION_CLIMATE_CONTROL);
             append(VehiclePropertyIds.HVAC_DEFROSTER, CAR_PERMISSION_CLIMATE_CONTROL);
             append(VehiclePropertyIds.HVAC_MAX_DEFROST_ON, CAR_PERMISSION_CLIMATE_CONTROL);
+            append(HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID, CAR_PERMISSION_CLIMATE_CONTROL);
         }
     };
     private static final Set<Integer> ON_CHANGE_PROPERTIES =
diff --git a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/climate/AutomotiveCarClimateTest.java b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/climate/AutomotiveCarClimateTest.java
index d5ae1f9..78cdd50 100644
--- a/car/app/app-automotive/src/test/java/androidx/car/app/hardware/climate/AutomotiveCarClimateTest.java
+++ b/car/app/app-automotive/src/test/java/androidx/car/app/hardware/climate/AutomotiveCarClimateTest.java
@@ -33,7 +33,7 @@
 import static android.car.VehiclePropertyIds.HVAC_TEMPERATURE_SET;
 
 import static androidx.car.app.hardware.climate.AutomotiveCarClimate.DEFAULT_SAMPLE_RATE_HZ;
-import static androidx.car.app.hardware.climate.AutomotiveCarClimate.HVAC_ELECTRIC_DEFROSTER;
+import static androidx.car.app.hardware.climate.AutomotiveCarClimate.HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID;
 import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_CABIN_TEMPERATURE;
 import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_FAN_DIRECTION;
 import static androidx.car.app.hardware.climate.ClimateProfileRequest.FEATURE_FAN_SPEED;
@@ -205,15 +205,17 @@
         mAutomotiveCarClimate.registerClimateStateCallback(mExecutor, builder.build(), listener);
 
         Map<Integer, List<CarZone>> propertyIdsWithCarZones =
-                ImmutableMap.<Integer, List<CarZone>>builder().put(HVAC_ELECTRIC_DEFROSTER,
-                        Collections.singletonList(mCarZone)).buildKeepingLast();
+                ImmutableMap.<Integer, List<CarZone>>builder()
+                .put(HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID, Collections.singletonList(mCarZone))
+                .buildKeepingLast();
 
         ArgumentCaptor<OnCarPropertyResponseListener> captor = ArgumentCaptor.forClass(
                 OnCarPropertyResponseListener.class);
         verify(mPropertyManager).submitRegisterListenerRequest(eq(propertyIdsWithCarZones),
                 eq(DEFAULT_SAMPLE_RATE_HZ), captor.capture(), eq(mExecutor));
 
-        mResponse.add(CarPropertyResponse.builder().setPropertyId(HVAC_ELECTRIC_DEFROSTER)
+        mResponse.add(CarPropertyResponse.builder().setPropertyId(
+                HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID)
                 .setCarZones(Collections.singletonList(mCarZone)).setValue(true).setStatus(
                 STATUS_SUCCESS).build());
 
@@ -886,8 +888,10 @@
         List<Set<CarZone>> carZones = new ArrayList<>();
         carZones.add(Collections.singleton(FRONT_LEFT_ZONE));
         carZones.add(Collections.singleton(FRONT_RIGHT_ZONE));
-        List<Integer> propertyIds = Collections.singletonList(HVAC_ELECTRIC_DEFROSTER);
-        mCarPropertyProfiles.add(CarPropertyProfile.builder().setPropertyId(HVAC_ELECTRIC_DEFROSTER)
+        List<Integer> propertyIds = Collections.singletonList(
+                HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID);
+        mCarPropertyProfiles.add(CarPropertyProfile.builder().setPropertyId(
+                HVAC_ELECTRIC_DEFROSTER_ON_PROPERTY_ID)
                 .setCarZones(carZones).setStatus(STATUS_SUCCESS).build());
         ListenableFuture<List<CarPropertyProfile<?>>> listenableCarPropertyProfile =
                 Futures.immediateFuture(mCarPropertyProfiles);
diff --git a/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta03.txt b/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta03.txt
index 36c0cee..eb3a5ef 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta03.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_1.3.0-beta03.txt
@@ -989,7 +989,7 @@
   }
 
   @androidx.compose.foundation.ExperimentalFoundationApi public interface BringIntoViewResponder {
-    method @androidx.compose.foundation.ExperimentalFoundationApi public suspend Object? bringChildIntoView(androidx.compose.ui.geometry.Rect localRect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public suspend Object? bringChildIntoView(kotlin.jvm.functions.Function0<androidx.compose.ui.geometry.Rect> localRect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.geometry.Rect calculateRectForParent(androidx.compose.ui.geometry.Rect localRect);
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 36c0cee..eb3a5ef 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -989,7 +989,7 @@
   }
 
   @androidx.compose.foundation.ExperimentalFoundationApi public interface BringIntoViewResponder {
-    method @androidx.compose.foundation.ExperimentalFoundationApi public suspend Object? bringChildIntoView(androidx.compose.ui.geometry.Rect localRect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public suspend Object? bringChildIntoView(kotlin.jvm.functions.Function0<androidx.compose.ui.geometry.Rect> localRect, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method @androidx.compose.foundation.ExperimentalFoundationApi public androidx.compose.ui.geometry.Rect calculateRectForParent(androidx.compose.ui.geometry.Rect localRect);
   }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt
new file mode 100644
index 0000000..77bf51f
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/LineBreakingDemo.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.demos.text
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@OptIn(ExperimentalTextApi::class)
+private val lineBreakOptions = listOf(
+    "Simple" to LineBreak.Simple,
+    "Paragraph" to LineBreak.Paragraph,
+    "Heading" to LineBreak.Heading,
+    "Custom" to LineBreak(
+        strategy = LineBreak.Strategy.Balanced,
+        strictness = LineBreak.Strictness.Strict,
+        wordBreak = LineBreak.WordBreak.Default
+    )
+)
+
+private val demoText = "This is an example text\n今日は自由が丘で焼き鳥を食べます。"
+private val presetNameStyle = SpanStyle(fontWeight = FontWeight.Bold, fontSize = 16.sp)
+
+@OptIn(ExperimentalTextApi::class)
+@Composable
+fun TextLineBreakingDemo() {
+    val selectedFontSize = remember { mutableStateOf(16f) }
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        Text("Font size: ${selectedFontSize.value}")
+        Slider(
+            value = selectedFontSize.value,
+            onValueChange = { value -> selectedFontSize.value = value },
+            valueRange = 8f..48f
+        )
+
+        Row(Modifier.fillMaxWidth()) {
+            val textModifier = Modifier
+                .wrapContentHeight()
+                .padding(horizontal = 5.dp)
+                .border(1.dp, Color.Gray)
+
+            lineBreakOptions.forEach { (presetName, preset) ->
+                Text(
+                    text = buildAnnotatedString {
+                        withStyle(presetNameStyle) {
+                            append(presetName)
+                            append(":\n")
+                        }
+                        append(demoText)
+                    },
+                    style = TextStyle(
+                        lineBreak = preset,
+                        fontSize = selectedFontSize.value.sp
+                    ),
+                    modifier = textModifier.weight(1f)
+                )
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index f67371f..02b0fe2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -38,6 +38,7 @@
         ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
         ComposableDemo("Interactive text") { InteractiveTextDemo() },
         ComposableDemo("Ellipsize and letterspacing") { EllipsizeWithLetterSpacing() },
+        ComposableDemo("Line breaking") { TextLineBreakingDemo() },
         DemoCategory(
             "Text Overflow",
             listOf(
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
index 7416de3..94609f4 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/BringIntoViewSamples.kt
@@ -155,10 +155,12 @@
                         return Rect(Offset.Zero, localRect.size)
                     }
 
-                    override suspend fun bringChildIntoView(localRect: Rect) {
+                    override suspend fun bringChildIntoView(localRect: () -> Rect?) {
                         // Offset the content right and down by the offset of the requested area so
                         // that it will always be aligned to the top-left of the box.
-                        offset = -localRect.topLeft.round()
+                        localRect()?.let {
+                            offset = -it.topLeft.round()
+                        }
                     }
                 }
             })
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index f42e6a8..101c951 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -315,11 +315,11 @@
 
     @Test
     fun focusable_requestsBringIntoView_whenFocused() {
-        val requestedRects = mutableListOf<Rect>()
+        val requestedRects = mutableListOf<Rect?>()
         val bringIntoViewResponder = object : BringIntoViewResponder {
             override fun calculateRectForParent(localRect: Rect): Rect = localRect
-            override suspend fun bringChildIntoView(localRect: Rect) {
-                requestedRects += localRect
+            override suspend fun bringChildIntoView(localRect: () -> Rect?) {
+                requestedRects += localRect()
             }
         }
         val focusRequester = FocusRequester()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
index dd85516..637e5b2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableFocusableInteractionTest.kt
@@ -45,7 +45,6 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import kotlinx.coroutines.runBlocking
 import org.junit.After
@@ -87,118 +86,118 @@
 
     @Test
     fun scrollsFocusedFocusableIntoView_whenFullyInViewAndBecomesFullyHidden() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 50.dp
+        viewportSize = 50.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(40.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(40.toDp())
             .assertIsDisplayed()
     }
 
     @Test
     fun scrollsFocusedFocusableIntoView_whenFullyInViewAndBecomesPartiallyHidden() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 95.dp
+        viewportSize = 95.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(85.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(85.toDp())
             .assertIsDisplayed()
     }
 
     @Test
     fun scrollsFocusedFocusableIntoView_whenPartiallyInViewAndBecomesMoreHidden() {
-        var viewportSize by mutableStateOf(95.dp)
+        var viewportSize by mutableStateOf(95.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 91.dp
+        viewportSize = 91.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(81.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(81.toDp())
             .assertIsDisplayed()
     }
 
     @Test
     fun scrollsFocusedFocusableIntoView_whenPartiallyInViewAndBecomesFullyHidden() {
-        var viewportSize by mutableStateOf(95.dp)
+        var viewportSize by mutableStateOf(95.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 90.dp
+        viewportSize = 90.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(80.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(80.toDp())
             .assertIsDisplayed()
     }
 
     @Test
     fun scrollsFocusedFocusableIntoView_whenViewportAnimatedQuickly() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
@@ -206,13 +205,13 @@
         // on every frame, for a few frames. The underlying bug in b/230756508 would lose track
         // of the focusable after the second frame.
         rule.mainClock.autoAdvance = false
-        viewportSize = 80.dp
+        viewportSize = 80.toDp()
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
-        viewportSize = 60.dp
+        viewportSize = 60.toDp()
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
-        viewportSize = 40.dp
+        viewportSize = 40.toDp()
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
 
@@ -225,24 +224,24 @@
 
     @Test
     fun scrollFromViewportShrink_isInterrupted_byGesture() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Shrink the viewport to start the scroll animation.
         rule.mainClock.autoAdvance = false
-        viewportSize = 80.dp
+        viewportSize = 80.toDp()
         // Run the first frame of the scroll animation.
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
@@ -272,24 +271,24 @@
      */
     @Test
     fun scrollsFocusedFocusableIntoView_whenViewportExpandedThenReshrunk_afterInterruption() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Shrink the viewport to start the scroll animation.
         rule.mainClock.autoAdvance = false
-        viewportSize = 80.dp
+        viewportSize = 80.toDp()
         // Run the first frame of the scroll animation.
         rule.mainClock.advanceTimeByFrame()
         rule.waitForIdle()
@@ -312,13 +311,13 @@
             .assertIsNotDisplayed()
 
         // Expand the viewport back to its original size to bring the focusable back into view.
-        viewportSize = 100.dp
+        viewportSize = 100.toDp()
         rule.waitForIdle()
         Thread.sleep(2000)
 
         // Shrink the viewport again, this should trigger another scroll animation to keep the
         // scrollable in view.
-        viewportSize = 50.dp
+        viewportSize = 50.toDp()
         rule.waitForIdle()
 
         rule.onNodeWithTag(focusableTag)
@@ -327,65 +326,65 @@
 
     @Test
     fun doesNotScrollFocusedFocusableIntoView_whenNotInViewAndViewportShrunk() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(viewportSize) {
                 // Put a focusable just below the bottom of the viewport, out of view.
-                Spacer(Modifier.size(100.dp))
-                TestFocusable(10.dp)
+                Spacer(Modifier.size(100.toDp()))
+                TestFocusable(10.toDp())
             }
         }
         requestFocusAndScrollToTop()
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(100.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(100.toDp())
             .assertIsNotDisplayed()
             .assertIsFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 50.dp
+        viewportSize = 50.toDp()
 
         // Focusable should not have moved since it was never in view.
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(100.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(100.toDp())
             .assertIsNotDisplayed()
     }
 
     @Test
     fun doesNotScrollUnfocusedFocusableIntoView_whenViewportShrunk() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the bottom of the viewport.
-                Spacer(Modifier.size(90.dp))
-                TestFocusable(size = 10.dp)
+                Spacer(Modifier.size(90.toDp()))
+                TestFocusable(size = 10.toDp())
             }
         }
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsDisplayed()
             .assertIsNotFocused()
 
         // Act: Shrink the viewport.
-        viewportSize = 50.dp
+        viewportSize = 50.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(90.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(90.toDp())
             .assertIsNotDisplayed()
     }
 
     @Test
     fun doesNotScrollFocusedFocusableIntoView_whenPartiallyInViewAndViewportGrown() {
-        var viewportSize by mutableStateOf(50.dp)
+        var viewportSize by mutableStateOf(50.toDp())
 
         rule.setContent {
             TestScrollableColumn(size = viewportSize) {
                 // Put a focusable in the middle of the viewport, but ensure we're a lot bigger
                 // than the viewport so it can grow without requiring a scroll.
-                Spacer(Modifier.size(100.dp))
-                TestFocusable(size = 10.dp)
-                Spacer(Modifier.size(100.dp))
+                Spacer(Modifier.size(100.toDp()))
+                TestFocusable(size = 10.toDp())
+                Spacer(Modifier.size(100.toDp()))
             }
         }
         rule.runOnIdle {
@@ -395,25 +394,25 @@
         // viewport), then scroll up by half the focusable height so it's partially in view.
         rule.waitForIdle()
         runBlocking {
-            val halfFocusableSize = with(rule.density) { (10.dp / 2).toPx() }
+            val halfFocusableSize = with(rule.density) { (10.toDp() / 2).toPx() }
             scrollState.scrollBy(-halfFocusableSize)
         }
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(45.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(45.toDp())
             .assertIsDisplayed()
             .assertIsFocused()
 
         // Act: Grow the viewport.
-        viewportSize = 100.dp
+        viewportSize = 100.toDp()
 
         rule.onNodeWithTag(focusableTag)
-            .assertScrollAxisPositionInRootIsEqualTo(45.dp)
+            .assertScrollAxisPositionInRootIsEqualTo(45.toDp())
             .assertIsDisplayed()
     }
 
     @Test
     fun scrollsToNewFocusable_whenFocusedChildChangesDuringAnimation() {
-        var viewportSize by mutableStateOf(100.dp)
+        var viewportSize by mutableStateOf(100.toDp())
         val focusRequester1 = FocusRequester()
         val focusRequester2 = FocusRequester()
 
@@ -421,16 +420,16 @@
             TestScrollableColumn(size = viewportSize) {
                 Box(
                     Modifier
-                        .size(10.dp)
+                        .size(10.toDp())
                         .background(Color.Blue)
                         .testTag("focusable1")
                         .focusRequester(focusRequester1)
                         .focusable()
                 )
-                Spacer(Modifier.size(100.dp))
+                Spacer(Modifier.size(100.toDp()))
                 Box(
                     Modifier
-                        .size(10.dp)
+                        .size(10.toDp())
                         .background(Color.Blue)
                         .testTag("focusable2")
                         .focusRequester(focusRequester2)
@@ -449,7 +448,7 @@
 
         // Shrink the viewport, which should scroll to keep focusable2 in-view.
         rule.runOnIdle {
-            viewportSize = 20.dp
+            viewportSize = 20.toDp()
         }
 
         // Tick the clock forward to let the animation start and run a bit.
@@ -475,7 +474,7 @@
         val modifier = Modifier
             .testTag(scrollableAreaTag)
             .size(size)
-            .border(2.dp, Color.Black)
+            .border(2.toDp(), Color.Black)
 
         when (orientation) {
             Orientation.Vertical -> {
@@ -505,6 +504,13 @@
         )
     }
 
+    /**
+     * Sizes and offsets of the composables in these tests must be specified using this function.
+     * If they're specified using `xx.dp` syntax, a rounding error somewhere in the layout system
+     * will cause the pixel values to be off-by-one.
+     */
+    private fun Int.toDp(): Dp = with(rule.density) { this@toDp.toDp() }
+
     private fun requestFocusAndScrollToTop() {
         rule.runOnIdle {
             focusRequester.requestFocus()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
index af5d456..d3a38ce 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequesterViewIntegrationTest.kt
@@ -146,7 +146,7 @@
         val requesterOffset = IntOffset(1, 2)
         val rectangleToRequest = Rect(Offset(10f, 20f), Size(30f, 40f))
         val expectedRectangle = AndroidRect(11, 22, 41, 62)
-        val requests = mutableListOf<Rect>()
+        val requests = mutableListOf<Rect?>()
         lateinit var scope: CoroutineScope
         val bringIntoViewRequester = BringIntoViewRequester()
         rule.setContent {
@@ -154,7 +154,7 @@
             AndroidView(
                 modifier = Modifier
                     // This offset needs to be non-zero or it won't see the request at all.
-                    .fakeScrollable { requests += it },
+                    .fakeScrollable { requests += it() },
                 factory = { context ->
                     val parent = FakeScrollable(context)
                     val child = ComposeView(context)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
index 02ce4e7..9593894 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponderTest.kt
@@ -62,7 +62,7 @@
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { requestedRect = it }
+                    .fakeScrollable { requestedRect = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
         }
@@ -82,11 +82,11 @@
     fun bringIntoView_rectInChild() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        lateinit var requestedRect: Rect
+        var requestedRect: Rect? = null
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { requestedRect = it }
+                    .fakeScrollable { requestedRect = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
         }
@@ -104,12 +104,12 @@
     fun bringIntoView_childWithSize() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        lateinit var requestedRect: Rect
+        var requestedRect: Rect? = null
         rule.setContent {
             Box(Modifier) {
                 Box(
                     Modifier
-                        .fakeScrollable { requestedRect = it }
+                        .fakeScrollable { requestedRect = it() }
                         .size(20f.toDp(), 10f.toDp())
                         .offset { IntOffset(40, 30) }
                         .bringIntoViewRequester(bringIntoViewRequester)
@@ -130,12 +130,12 @@
     fun bringIntoView_childBiggerThanParent() {
         // Arrange.
         val bringIntoViewRequester = BringIntoViewRequester()
-        lateinit var requestedRect: Rect
+        var requestedRect: Rect? = null
         rule.setContent {
             Box(
                 Modifier
                     .size(1f.toDp())
-                    .fakeScrollable { requestedRect = it }
+                    .fakeScrollable { requestedRect = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
                     .size(20f.toDp(), 10f.toDp())
             )
@@ -153,15 +153,15 @@
     @Test
     fun bringIntoView_propagatesToMultipleResponders() {
         // Arrange.
-        lateinit var outerRequest: Rect
-        lateinit var innerRequest: Rect
+        var outerRequest: Rect? = null
+        var innerRequest: Rect? = null
         val bringIntoViewRequester = BringIntoViewRequester()
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { outerRequest = it }
+                    .fakeScrollable { outerRequest = it() }
                     .offset(2f.toDp(), 1f.toDp())
-                    .fakeScrollable { innerRequest = it }
+                    .fakeScrollable { innerRequest = it() }
                     .size(20f.toDp(), 10f.toDp())
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
@@ -180,15 +180,15 @@
     @Test
     fun bringIntoView_onlyPropagatesUp() {
         // Arrange.
-        lateinit var parentRequest: Rect
+        var parentRequest: Rect? = null
         var childRequest: Rect? = null
         val bringIntoViewRequester = BringIntoViewRequester()
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { parentRequest = it }
+                    .fakeScrollable { parentRequest = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
-                    .fakeScrollable { childRequest = it }
+                    .fakeScrollable { childRequest = it() }
             )
         }
 
@@ -205,14 +205,14 @@
     @Test
     fun bringIntoView_propagatesUp_whenRectForParentReturnsInput() {
         // Arrange.
-        lateinit var parentRequest: Rect
+        var parentRequest: Rect? = null
         var childRequest: Rect? = null
         val bringIntoViewRequester = BringIntoViewRequester()
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { parentRequest = it }
-                    .fakeScrollable { childRequest = it }
+                    .fakeScrollable { parentRequest = it() }
+                    .fakeScrollable { childRequest = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
         }
@@ -230,12 +230,12 @@
     @Test
     fun bringIntoView_translatesByCalculateRectForParent() {
         // Arrange.
-        lateinit var requestedRect: Rect
+        var requestedRect: Rect? = null
         val bringIntoViewRequester = BringIntoViewRequester()
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable { requestedRect = it }
+                    .fakeScrollable { requestedRect = it() }
                     .fakeScrollable(Offset(2f, 3f)) {}
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
@@ -603,7 +603,7 @@
         rule.setContent {
             Box(
                 Modifier
-                    .fakeScrollable(Offset.Zero) { requestedRect = it }
+                    .fakeScrollable(Offset.Zero) { requestedRect = it() }
                     .bringIntoViewRequester(bringIntoViewRequester)
             )
         }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
index 0adee6c..5e94f7a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/BringIntoViewScrollableInteractionTest.kt
@@ -31,13 +31,20 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.withFrameMillis
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Color.Companion.Blue
 import androidx.compose.ui.graphics.Color.Companion.Green
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.graphics.Color.Companion.Red
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
@@ -878,6 +885,53 @@
         assertChildMaxInView()
     }
 
+    /** See b/241591211. */
+    @Test
+    fun doesNotCrashWhenCoordinatesDetachedDuringOperation() {
+        val requests = mutableListOf<() -> Rect?>()
+        val responder = object : BringIntoViewResponder {
+            override fun calculateRectForParent(localRect: Rect): Rect = localRect
+
+            override suspend fun bringChildIntoView(localRect: () -> Rect?) {
+                requests += localRect
+            }
+        }
+        val requester = BringIntoViewRequester()
+        var coordinates: LayoutCoordinates? = null
+        var attach by mutableStateOf(true)
+        setContentAndInitialize {
+            if (attach) {
+                Box(
+                    modifier = Modifier
+                        .bringIntoViewResponder(responder)
+                        .bringIntoViewRequester(requester)
+                        .onPlaced { coordinates = it }
+                        .size(10.toDp())
+                )
+
+                LaunchedEffect(Unit) {
+                    // Wait a frame to allow the modifiers to be wired up and the coordinates to get
+                    // attached.
+                    withFrameMillis {}
+                    requester.bringIntoView()
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(requests).hasSize(1)
+            assertThat(coordinates?.isAttached).isTrue()
+        }
+
+        attach = false
+
+        rule.runOnIdle {
+            assertThat(coordinates?.isAttached).isFalse()
+            // This call should not crash.
+            requests.single().invoke()
+        }
+    }
+
     private fun setContentAndInitialize(content: @Composable () -> Unit) {
         rule.setContent {
             testScope = rememberCoroutineScope()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
index 131236c..253454b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/relocation/FakeScrollable.kt
@@ -33,13 +33,13 @@
 @OptIn(ExperimentalFoundationApi::class)
 internal fun Modifier.fakeScrollable(
     parentOffset: Offset = Offset.Zero,
-    onBringIntoView: suspend (Rect) -> Unit
+    onBringIntoView: suspend (() -> Rect?) -> Unit
 ): Modifier = bringIntoViewResponder(
     object : BringIntoViewResponder {
         override fun calculateRectForParent(localRect: Rect): Rect =
             localRect.translate(parentOffset)
 
-        override suspend fun bringChildIntoView(localRect: Rect) {
+        override suspend fun bringChildIntoView(localRect: () -> Rect?) {
             onBringIntoView(localRect)
         }
     })
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
index 1fec6e2..19a823d 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
@@ -35,9 +35,12 @@
  * A [BringIntoViewParent] that delegates to the [View] hosting the composition.
  */
 private class AndroidBringIntoViewParent(private val view: View) : BringIntoViewParent {
-    override suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates) {
+    override suspend fun bringChildIntoView(
+        childCoordinates: LayoutCoordinates,
+        boundsProvider: () -> Rect?
+    ) {
         val childOffset = childCoordinates.positionInRoot()
-        val rootRect = rect.translate(childOffset)
+        val rootRect = boundsProvider()?.translate(childOffset) ?: return
         view.requestRectangleOnScreen(rootRect.toRect(), false)
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewModifier.kt
index 0e07948..5b6159f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ContentInViewModifier.kt
@@ -86,7 +86,10 @@
         return computeDestination(localRect, oldSize)
     }
 
-    override suspend fun bringChildIntoView(localRect: Rect) {
+    override suspend fun bringChildIntoView(localRect: () -> Rect?) {
+        // TODO(b/241591211) Read the request's bounds lazily in case they change.
+        @Suppress("NAME_SHADOWING")
+        val localRect = localRect() ?: return
         performBringIntoView(
             source = localRect,
             destination = calculateRectForParent(localRect)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
index d2feeae..7e5402e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -44,17 +44,21 @@
  */
 internal fun interface BringIntoViewParent {
     /**
-     * Scrolls this node's content so that [rect] will be in visible bounds. Must ensure that the
+     * Scrolls this node's content so that [boundsProvider] will be in visible bounds. Must ensure that the
      * request is propagated up to the parent node.
      *
      * This method will not return until this request has been satisfied or interrupted by a
      * newer request.
      *
-     * @param rect The rectangle to bring into view, relative to [childCoordinates].
      * @param childCoordinates The [LayoutCoordinates] of the child node making the request. This
-     * parent can use these [LayoutCoordinates] to translate [rect] into its own coordinates.
+     * parent can use these [LayoutCoordinates] to translate [boundsProvider] into its own
+     * coordinates.
+     * @param boundsProvider A function returning the rectangle to bring into view, relative to
+     * [childCoordinates]. The function may return a different value over time, if the bounds of the
+     * request change while the request is being processed. If the rectangle cannot be calculated,
+     * e.g. because [childCoordinates] is not attached, return null.
      */
-    suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates)
+    suspend fun bringChildIntoView(childCoordinates: LayoutCoordinates, boundsProvider: () -> Rect?)
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index 769a43d..d178c065 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -138,12 +138,12 @@
      * is null) be brought into view by the [parent]&nbsp;[BringIntoViewParent].
      */
     suspend fun bringIntoView(rect: Rect?) {
-        val layoutCoordinates = layoutCoordinates ?: return
-
-        // If the rect is not specified, use a rectangle representing the entire composable.
-        val sourceRect = rect ?: layoutCoordinates.size.toSize().toRect()
-
-        // Convert the rect into parent coordinates.
-        parent.bringChildIntoView(sourceRect, layoutCoordinates)
+        parent.bringChildIntoView(layoutCoordinates ?: return) {
+            // If the rect is not specified, use a rectangle representing the entire composable.
+            // If the coordinates are detached when this call is made, we don't bother even
+            // submitting the request, but if the coordinates become detached while the request
+            // is being handled we just return a null Rect.
+            rect ?: layoutCoordinates?.size?.toSize()?.toRect()
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
index 4a0f0b9..1c5fe2c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
@@ -82,11 +82,14 @@
      * `Animatable` you get this for free, since it will cancel the previous animation when a new
      * one is started while preserving velocity.
      *
-     * @param localRect The rectangle that should be brought into view, relative to this node. This
-     * is the same rectangle that will have been passed to [calculateRectForParent].
+     * @param localRect A function returning the rectangle that should be brought into view,
+     * relative to this node. This is the same rectangle that will have been passed to
+     * [calculateRectForParent]. The function may return a different value over time, if the bounds
+     * of the request change while the request is being processed. If the rectangle cannot be
+     * calculated, e.g. because the [LayoutCoordinates] are not attached, return null.
      */
     @ExperimentalFoundationApi
-    suspend fun bringChildIntoView(localRect: Rect)
+    suspend fun bringChildIntoView(localRect: () -> Rect?)
 }
 
 /**
@@ -185,14 +188,21 @@
     private var newestDispatchedRequest: Pair<Rect, Job>? = null
 
     /**
-     * Responds to a child's request by first converting [rect] into this node's [LayoutCoordinates]
+     * Responds to a child's request by first converting [boundsProvider] into this node's [LayoutCoordinates]
      * and then, concurrently, calling the [responder] and the [parent] to handle the request.
      */
-    override suspend fun bringChildIntoView(rect: Rect, childCoordinates: LayoutCoordinates) {
+    override suspend fun bringChildIntoView(
+        childCoordinates: LayoutCoordinates,
+        boundsProvider: () -> Rect?
+    ) {
         coroutineScope {
             val layoutCoordinates = layoutCoordinates ?: return@coroutineScope
             if (!childCoordinates.isAttached) return@coroutineScope
-            val localRect = layoutCoordinates.localRectOf(childCoordinates, rect)
+            // TODO(b/241591211) Read the request's bounds lazily in case they change.
+            val localRect = layoutCoordinates.localRectOf(
+                sourceCoordinates = childCoordinates,
+                rect = boundsProvider() ?: return@coroutineScope
+            )
 
             // Immediately make this request the tail of the queue, before suspending, so that
             // any requests that come in while suspended will join on this one.
@@ -264,12 +274,14 @@
             // parent, or parent before the child).
             launch {
                 // Bring the requested Child into this parent's view.
-                responder.bringChildIntoView(localRect)
+                // TODO(b/241591211) Read the request's bounds lazily in case they change.
+                responder.bringChildIntoView { localRect }
             }
 
             // TODO I think this needs to be in launch, since if the parent is cancelled (this
             //  throws a CE) due to animation interruption, the child should continue animating.
-            parent.bringChildIntoView(parentRect, layoutCoordinates)
+            // TODO(b/241591211) Read the request's bounds lazily in case they change.
+            parent.bringChildIntoView(layoutCoordinates) { parentRect }
         }
         // Don't try to null out newestDispatchedRequest here, bringChildIntoView will take care of
         // that.
diff --git a/compose/ui/ui-text/api/1.3.0-beta03.txt b/compose/ui/ui-text/api/1.3.0-beta03.txt
index 057beaf..0f79aa3 100644
--- a/compose/ui/ui-text/api/1.3.0-beta03.txt
+++ b/compose/ui/ui-text/api/1.3.0-beta03.txt
@@ -222,8 +222,8 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getLineHeight();
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 057beaf..0f79aa3 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -222,8 +222,8 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getLineHeight();
diff --git a/compose/ui/ui-text/api/public_plus_experimental_1.3.0-beta03.txt b/compose/ui/ui-text/api/public_plus_experimental_1.3.0-beta03.txt
index 6d70e25..c08cd32 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_1.3.0-beta03.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_1.3.0-beta03.txt
@@ -238,10 +238,13 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
@@ -250,6 +253,7 @@
     method public androidx.compose.ui.text.style.TextIndent? getTextIndent();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
@@ -518,10 +522,12 @@
   @androidx.compose.runtime.Immutable public final class TextStyle {
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     method @androidx.compose.ui.text.ExperimentalTextApi public float getAlpha();
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
@@ -534,6 +540,7 @@
     method public androidx.compose.ui.text.font.FontSynthesis? getFontSynthesis();
     method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
     method public long getLetterSpacing();
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
@@ -565,6 +572,7 @@
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
     property public final long letterSpacing;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -1373,6 +1381,66 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
+  @androidx.compose.runtime.Immutable @androidx.compose.ui.text.ExperimentalTextApi public final class LineBreak {
+    ctor public LineBreak(int strategy, int strictness, int wordBreak);
+    method public androidx.compose.ui.text.style.LineBreak copy(optional int strategy, optional int strictness, optional int wordBreak);
+    method public int getStrategy();
+    method public int getStrictness();
+    method public int getWordBreak();
+    property public final int strategy;
+    property public final int strictness;
+    property public final int wordBreak;
+    field public static final androidx.compose.ui.text.style.LineBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.Companion {
+    method public androidx.compose.ui.text.style.LineBreak getHeading();
+    method public androidx.compose.ui.text.style.LineBreak getParagraph();
+    method public androidx.compose.ui.text.style.LineBreak getSimple();
+    property public final androidx.compose.ui.text.style.LineBreak Heading;
+    property public final androidx.compose.ui.text.style.LineBreak Paragraph;
+    property public final androidx.compose.ui.text.style.LineBreak Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strategy {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strategy.Companion Companion;
+  }
+
+  public static final class LineBreak.Strategy.Companion {
+    method public int getBalanced();
+    method public int getHighQuality();
+    method public int getSimple();
+    property public final int Balanced;
+    property public final int HighQuality;
+    property public final int Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strictness {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strictness.Companion Companion;
+  }
+
+  public static final class LineBreak.Strictness.Companion {
+    method public int getDefault();
+    method public int getLoose();
+    method public int getNormal();
+    method public int getStrict();
+    property public final int Default;
+    property public final int Loose;
+    property public final int Normal;
+    property public final int Strict;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.WordBreak {
+    field public static final androidx.compose.ui.text.style.LineBreak.WordBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.WordBreak.Companion {
+    method public int getDefault();
+    method public int getPhrase();
+    property public final int Default;
+    property public final int Phrase;
+  }
+
   public final class LineHeightStyle {
     ctor public LineHeightStyle(float alignment, int trim);
     method public float getAlignment();
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index 6d70e25..c08cd32 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -238,10 +238,13 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.PlatformParagraphStyle? getPlatformStyle();
@@ -250,6 +253,7 @@
     method public androidx.compose.ui.text.style.TextIndent? getTextIndent();
     method @androidx.compose.runtime.Stable public androidx.compose.ui.text.ParagraphStyle merge(optional androidx.compose.ui.text.ParagraphStyle? other);
     method @androidx.compose.runtime.Stable public operator androidx.compose.ui.text.ParagraphStyle plus(androidx.compose.ui.text.ParagraphStyle other);
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.PlatformParagraphStyle? platformStyle;
@@ -518,10 +522,12 @@
   @androidx.compose.runtime.Immutable public final class TextStyle {
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     ctor public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    ctor @androidx.compose.ui.text.ExperimentalTextApi public TextStyle(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
-    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.TextStyle copy(androidx.compose.ui.graphics.Brush? brush, optional float alpha, optional long fontSize, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontSynthesis? fontSynthesis, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional String? fontFeatureSettings, optional long letterSpacing, optional androidx.compose.ui.text.style.BaselineShift? baselineShift, optional androidx.compose.ui.text.style.TextGeometricTransform? textGeometricTransform, optional androidx.compose.ui.text.intl.LocaleList? localeList, optional long background, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.graphics.Shadow? shadow, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformTextStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle, optional androidx.compose.ui.text.style.LineBreak? lineBreak);
     method @androidx.compose.ui.text.ExperimentalTextApi public float getAlpha();
     method public long getBackground();
     method public androidx.compose.ui.text.style.BaselineShift? getBaselineShift();
@@ -534,6 +540,7 @@
     method public androidx.compose.ui.text.font.FontSynthesis? getFontSynthesis();
     method public androidx.compose.ui.text.font.FontWeight? getFontWeight();
     method public long getLetterSpacing();
+    method @androidx.compose.ui.text.ExperimentalTextApi public androidx.compose.ui.text.style.LineBreak? getLineBreak();
     method public long getLineHeight();
     method public androidx.compose.ui.text.style.LineHeightStyle? getLineHeightStyle();
     method public androidx.compose.ui.text.intl.LocaleList? getLocaleList();
@@ -565,6 +572,7 @@
     property public final androidx.compose.ui.text.font.FontSynthesis? fontSynthesis;
     property public final androidx.compose.ui.text.font.FontWeight? fontWeight;
     property public final long letterSpacing;
+    property @androidx.compose.ui.text.ExperimentalTextApi public final androidx.compose.ui.text.style.LineBreak? lineBreak;
     property public final long lineHeight;
     property public final androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle;
     property public final androidx.compose.ui.text.intl.LocaleList? localeList;
@@ -1373,6 +1381,66 @@
     method @androidx.compose.runtime.Stable public static float lerp(float start, float stop, float fraction);
   }
 
+  @androidx.compose.runtime.Immutable @androidx.compose.ui.text.ExperimentalTextApi public final class LineBreak {
+    ctor public LineBreak(int strategy, int strictness, int wordBreak);
+    method public androidx.compose.ui.text.style.LineBreak copy(optional int strategy, optional int strictness, optional int wordBreak);
+    method public int getStrategy();
+    method public int getStrictness();
+    method public int getWordBreak();
+    property public final int strategy;
+    property public final int strictness;
+    property public final int wordBreak;
+    field public static final androidx.compose.ui.text.style.LineBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.Companion {
+    method public androidx.compose.ui.text.style.LineBreak getHeading();
+    method public androidx.compose.ui.text.style.LineBreak getParagraph();
+    method public androidx.compose.ui.text.style.LineBreak getSimple();
+    property public final androidx.compose.ui.text.style.LineBreak Heading;
+    property public final androidx.compose.ui.text.style.LineBreak Paragraph;
+    property public final androidx.compose.ui.text.style.LineBreak Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strategy {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strategy.Companion Companion;
+  }
+
+  public static final class LineBreak.Strategy.Companion {
+    method public int getBalanced();
+    method public int getHighQuality();
+    method public int getSimple();
+    property public final int Balanced;
+    property public final int HighQuality;
+    property public final int Simple;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.Strictness {
+    field public static final androidx.compose.ui.text.style.LineBreak.Strictness.Companion Companion;
+  }
+
+  public static final class LineBreak.Strictness.Companion {
+    method public int getDefault();
+    method public int getLoose();
+    method public int getNormal();
+    method public int getStrict();
+    property public final int Default;
+    property public final int Loose;
+    property public final int Normal;
+    property public final int Strict;
+  }
+
+  @kotlin.jvm.JvmInline public static final value class LineBreak.WordBreak {
+    field public static final androidx.compose.ui.text.style.LineBreak.WordBreak.Companion Companion;
+  }
+
+  public static final class LineBreak.WordBreak.Companion {
+    method public int getDefault();
+    method public int getPhrase();
+    property public final int Default;
+    property public final int Phrase;
+  }
+
   public final class LineHeightStyle {
     ctor public LineHeightStyle(float alignment, int trim);
     method public float getAlignment();
diff --git a/compose/ui/ui-text/api/restricted_1.3.0-beta03.txt b/compose/ui/ui-text/api/restricted_1.3.0-beta03.txt
index 057beaf..0f79aa3 100644
--- a/compose/ui/ui-text/api/restricted_1.3.0-beta03.txt
+++ b/compose/ui/ui-text/api/restricted_1.3.0-beta03.txt
@@ -222,8 +222,8 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getLineHeight();
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 057beaf..0f79aa3 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -222,8 +222,8 @@
   }
 
   @androidx.compose.runtime.Immutable public final class ParagraphStyle {
-    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
+    ctor public ParagraphStyle(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent);
     method public androidx.compose.ui.text.ParagraphStyle copy(optional androidx.compose.ui.text.style.TextAlign? textAlign, optional androidx.compose.ui.text.style.TextDirection? textDirection, optional long lineHeight, optional androidx.compose.ui.text.style.TextIndent? textIndent, optional androidx.compose.ui.text.PlatformParagraphStyle? platformStyle, optional androidx.compose.ui.text.style.LineHeightStyle? lineHeightStyle);
     method public long getLineHeight();
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt
new file mode 100644
index 0000000..91cf6d9
--- /dev/null
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/LineBreakSamples.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.text.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.unit.sp
+
+@OptIn(ExperimentalTextApi::class)
+@Sampled
+@Composable
+fun LineBreakSample() {
+    Text(
+        text = "Title of an article",
+        style = TextStyle(
+            fontSize = 20.sp,
+            lineBreak = LineBreak.Heading
+        )
+    )
+
+    Text(
+        text = "A long paragraph in an article",
+        style = TextStyle(
+            lineBreak = LineBreak.Paragraph
+        )
+    )
+}
+
+@OptIn(ExperimentalTextApi::class)
+@Sampled
+@Composable
+fun AndroidLineBreakSample() {
+    val customTitleLineBreak = LineBreak(
+        strategy = LineBreak.Strategy.Simple,
+        strictness = LineBreak.Strictness.Loose,
+        wordBreak = LineBreak.WordBreak.Default
+    )
+
+    Text(
+        text = "Title of an article",
+        style = TextStyle(
+            fontSize = 20.sp,
+            lineBreak = customTitleLineBreak
+        )
+    )
+
+    val defaultStrictnessParagraphLineBreak =
+        LineBreak.Paragraph.copy(strictness = LineBreak.Strictness.Default)
+
+    Text(
+        text = "A long paragraph in an article",
+        style = TextStyle(
+            lineBreak = defaultStrictnessParagraphLineBreak
+        )
+    )
+}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt
new file mode 100644
index 0000000..b7769c1
--- /dev/null
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/style/LineBreakTest.kt
@@ -0,0 +1,157 @@
+/*
+ * 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.text.style
+
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTextApi::class)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LineBreakTest {
+    @Test
+    fun equals_different_strategy_returns_false() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.HighQuality,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.equals(otherLineBreak)).isFalse()
+    }
+
+    @Test
+    fun equals_different_style_returns_false() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Loose,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.equals(otherLineBreak)).isFalse()
+    }
+
+    @Test
+    fun equals_different_wordBreak_returns_false() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Default
+        )
+
+        assertThat(lineBreak.equals(otherLineBreak)).isFalse()
+    }
+
+    @Test
+    fun equals_same_flags_returns_true() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.equals(otherLineBreak)).isTrue()
+    }
+
+    @Test
+    fun hashCode_different_for_different_strategy() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.HighQuality,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.hashCode()).isNotEqualTo(otherLineBreak.hashCode())
+    }
+
+    @Test
+    fun hashCode_different_for_different_style() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Loose,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.hashCode()).isNotEqualTo(otherLineBreak.hashCode())
+    }
+
+    @Test
+    fun hashCode_different_for_different_wordBreak() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Default
+        )
+
+        assertThat(lineBreak.hashCode()).isNotEqualTo(otherLineBreak.hashCode())
+    }
+
+    @Test
+    fun hashCode_same_for_same_flags() {
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+        val otherLineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
+
+        assertThat(lineBreak.hashCode()).isEqualTo(otherLineBreak.hashCode())
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
index 94da64b6..314b2c5 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/AndroidParagraph.android.kt
@@ -39,10 +39,22 @@
 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_NORMAL
 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_OPPOSITE
 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_RIGHT
+import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_BALANCED
+import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_HIGH_QUALITY
+import androidx.compose.ui.text.android.LayoutCompat.BREAK_STRATEGY_SIMPLE
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_ALIGNMENT
+import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_BREAK_STRATEGY
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
+import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_STYLE
+import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE
 import androidx.compose.ui.text.android.LayoutCompat.JUSTIFICATION_MODE_INTER_WORD
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_LOOSE
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NONE
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_NORMAL
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_STYLE_STRICT
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_NONE
+import androidx.compose.ui.text.android.LayoutCompat.LINE_BREAK_WORD_STYLE_PHRASE
 import androidx.compose.ui.text.android.TextLayout
 import androidx.compose.ui.text.android.selection.WordBoundary
 import androidx.compose.ui.text.android.style.IndentationFixSpan
@@ -53,6 +65,7 @@
 import androidx.compose.ui.text.platform.extensions.setSpan
 import androidx.compose.ui.text.platform.isIncludeFontPaddingEnabled
 import androidx.compose.ui.text.platform.style.ShaderBrushSpan
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.ResolvedTextDirection
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
@@ -103,6 +116,7 @@
 
     @VisibleForTesting
     internal val charSequence: CharSequence
+
     init {
         require(constraints.minHeight == 0 && constraints.minWidth == 0) {
             "Setting Constraints.minWidth and Constraints.minHeight is not supported, " +
@@ -128,6 +142,10 @@
             else -> DEFAULT_JUSTIFICATION_MODE
         }
 
+        val breakStrategy = toLayoutBreakStrategy(style.lineBreak?.strategy)
+        val lineBreakStyle = toLayoutLineBreakStyle(style.lineBreak?.strictness)
+        val lineBreakWordStyle = toLayoutLineBreakWordStyle(style.lineBreak?.wordBreak)
+
         val ellipsize = if (ellipsis) {
             TextUtils.TruncateAt.END
         } else {
@@ -138,7 +156,10 @@
             alignment = alignment,
             justificationMode = justificationMode,
             ellipsize = ellipsize,
-            maxLines = maxLines
+            maxLines = maxLines,
+            breakStrategy = breakStrategy,
+            lineBreakStyle = lineBreakStyle,
+            lineBreakWordStyle = lineBreakWordStyle
         )
 
         // Ellipsize if there's not enough vertical space to fit all lines
@@ -154,7 +175,10 @@
                     // This will allow to have an ellipsis on that single line. If we measured with
                     // 0 maxLines, it would measure all lines with no ellipsis even though the first
                     // line might be partially visible
-                    maxLines = calculatedMaxLines.coerceAtLeast(1)
+                    maxLines = calculatedMaxLines.coerceAtLeast(1),
+                    breakStrategy = breakStrategy,
+                    lineBreakStyle = lineBreakStyle,
+                    lineBreakWordStyle = lineBreakWordStyle
                 )
             } else {
                 firstLayout
@@ -484,7 +508,10 @@
         alignment: Int,
         justificationMode: Int,
         ellipsize: TextUtils.TruncateAt?,
-        maxLines: Int
+        maxLines: Int,
+        breakStrategy: Int,
+        lineBreakStyle: Int,
+        lineBreakWordStyle: Int
     ) =
         TextLayout(
             charSequence = charSequence,
@@ -498,7 +525,10 @@
             justificationMode = justificationMode,
             layoutIntrinsics = paragraphIntrinsics.layoutIntrinsics,
             includePadding = paragraphIntrinsics.style.isIncludeFontPaddingEnabled(),
-            fallbackLineSpacing = true
+            fallbackLineSpacing = true,
+            breakStrategy = breakStrategy,
+            lineBreakStyle = lineBreakStyle,
+            lineBreakWordStyle = lineBreakWordStyle
         )
 }
 
@@ -515,6 +545,32 @@
     else -> DEFAULT_ALIGNMENT
 }
 
+@OptIn(ExperimentalTextApi::class, InternalPlatformTextApi::class)
+private fun toLayoutBreakStrategy(breakStrategy: LineBreak.Strategy?): Int = when (breakStrategy) {
+    LineBreak.Strategy.Simple -> BREAK_STRATEGY_SIMPLE
+    LineBreak.Strategy.HighQuality -> BREAK_STRATEGY_HIGH_QUALITY
+    LineBreak.Strategy.Balanced -> BREAK_STRATEGY_BALANCED
+    else -> DEFAULT_BREAK_STRATEGY
+}
+
+@OptIn(ExperimentalTextApi::class, InternalPlatformTextApi::class)
+private fun toLayoutLineBreakStyle(lineBreakStrictness: LineBreak.Strictness?): Int =
+    when (lineBreakStrictness) {
+        LineBreak.Strictness.Default -> LINE_BREAK_STYLE_NONE
+        LineBreak.Strictness.Loose -> LINE_BREAK_STYLE_LOOSE
+        LineBreak.Strictness.Normal -> LINE_BREAK_STYLE_NORMAL
+        LineBreak.Strictness.Strict -> LINE_BREAK_STYLE_STRICT
+        else -> DEFAULT_LINE_BREAK_STYLE
+    }
+
+@OptIn(ExperimentalTextApi::class, InternalPlatformTextApi::class)
+private fun toLayoutLineBreakWordStyle(lineBreakWordStyle: LineBreak.WordBreak?): Int =
+    when (lineBreakWordStyle) {
+        LineBreak.WordBreak.Default -> LINE_BREAK_WORD_STYLE_NONE
+        LineBreak.WordBreak.Phrase -> LINE_BREAK_WORD_STYLE_PHRASE
+        else -> DEFAULT_LINE_BREAK_WORD_STYLE
+    }
+
 @OptIn(InternalPlatformTextApi::class)
 private fun TextLayout.numberOfLinesThatFitMaxHeight(maxHeight: Int): Int {
     for (lineIndex in 0 until lineCount) {
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
new file mode 100644
index 0000000..b98a775
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/style/LineBreak.android.kt
@@ -0,0 +1,315 @@
+/*
+ * 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.text.style
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
+
+// TODO(b/246340708): Remove @sample LineBreakSample from the actual class
+/**
+ * When soft wrap is enabled and the width of the text exceeds the width of its container,
+ * line breaks are inserted in the text to split it over multiple lines.
+ *
+ * There are a number of parameters that affect how the line breaks are inserted.
+ * For example, the breaking algorithm can be changed to one with improved readability
+ * at the cost of speed.
+ * Another example is the strictness, which in some languages determines which symbols can appear
+ * at the start of a line.
+ *
+ * This represents a configuration for line breaking on Android, describing [Strategy], [Strictness],
+ * and [WordBreak].
+ *
+ * @sample androidx.compose.ui.text.samples.LineBreakSample
+ * @sample androidx.compose.ui.text.samples.AndroidLineBreakSample
+ *
+ * @param strategy defines the algorithm that inserts line breaks
+ * @param strictness defines the line breaking rules
+ * @param wordBreak defines how words are broken
+ */
+@ExperimentalTextApi
+@Immutable
+actual class LineBreak(
+    val strategy: Strategy,
+    val strictness: Strictness,
+    val wordBreak: WordBreak
+) {
+    actual companion object {
+        /**
+         * The greedy, fast line breaking algorithm. Ideal for text that updates often,
+         * such as a text editor, as the text will reflow minimally.
+         *
+         * <pre>
+         * +---------+
+         * | This is |
+         * | an      |
+         * | example |
+         * | text.   |
+         * | 今日は自  |
+         * | 由が丘で  |
+         * | 焼き鳥を  |
+         * | 食べま   |
+         * | す。     |
+         * +---------+
+         * </pre>
+         */
+        actual val Simple: LineBreak = LineBreak(
+            strategy = Strategy.Simple,
+            strictness = Strictness.Normal,
+            wordBreak = WordBreak.Default
+        )
+
+        /**
+         * Balanced line lengths, hyphenation, and phrase-based breaking.
+         * Suitable for short text such as titles or narrow newspaper columns.
+         *
+         * <pre>
+         * +---------+
+         * | This    |
+         * | is an   |
+         * | example |
+         * | text.   |
+         * | 今日は   |
+         * | 自由が丘  |
+         * | で焼き鳥  |
+         * | を食べ   |
+         * | ます。   |
+         * +---------+
+         * </pre>
+         */
+        actual val Heading: LineBreak = LineBreak(
+            strategy = Strategy.Balanced,
+            strictness = Strictness.Loose,
+            wordBreak = WordBreak.Phrase
+        )
+
+        /**
+         * Slower, higher quality line breaking for improved readability.
+         * Suitable for larger amounts of text.
+         *
+         * <pre>
+         * +---------+
+         * | This    |
+         * | is an   |
+         * | example |
+         * | text.   |
+         * | 今日は自  |
+         * | 由が丘で  |
+         * | 焼き鳥を  |
+         * | 食べま   |
+         * | す。     |
+         * +---------+
+         * </pre>
+         */
+        actual val Paragraph: LineBreak = LineBreak(
+            strategy = Strategy.HighQuality,
+            strictness = Strictness.Strict,
+            wordBreak = WordBreak.Default
+        )
+    }
+
+    fun copy(
+        strategy: Strategy = this.strategy,
+        strictness: Strictness = this.strictness,
+        wordBreak: WordBreak = this.wordBreak
+    ): LineBreak = LineBreak(
+        strategy = strategy,
+        strictness = strictness,
+        wordBreak = wordBreak
+    )
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is LineBreak) return false
+
+        if (strategy != other.strategy) return false
+        if (strictness != other.strictness) return false
+        if (wordBreak != other.wordBreak) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = strategy.hashCode()
+        result = 31 * result + strictness.hashCode()
+        result = 31 * result + wordBreak.hashCode()
+        return result
+    }
+
+    override fun toString(): String =
+        "LineBreak(strategy=$strategy, strictness=$strictness, wordBreak=$wordBreak)"
+
+    /**
+     * The strategy used for line breaking.
+     */
+    @JvmInline
+    value class Strategy private constructor(private val value: Int) {
+        companion object {
+            /**
+             * Basic, fast break strategy. Hyphenation, if enabled, is done only for words
+             * that don't fit on an entire line by themselves.
+             *
+             * <pre>
+             * +---------+
+             * | This is |
+             * | an      |
+             * | example |
+             * | text.   |
+             * +---------+
+             * </pre>
+             */
+            val Simple: Strategy = Strategy(1)
+
+            /**
+             * Does whole paragraph optimization for more readable text,
+             * including hyphenation if enabled.
+             *
+             * <pre>
+             * +---------+
+             * | This    |
+             * | is an   |
+             * | example |
+             * | text.   |
+             * +---------+
+             * </pre>
+             */
+            val HighQuality: Strategy = Strategy(2)
+
+            /**
+             * Attempts to balance the line lengths of the text, also applying automatic
+             * hyphenation if enabled. Suitable for small screens.
+             *
+             * <pre>
+             * +-----------------------+
+             * | This is an            |
+             * | example text.         |
+             * +-----------------------+
+             * </pre>
+             */
+            val Balanced: Strategy = Strategy(3)
+        }
+
+        override fun toString(): String = when (this) {
+            Simple -> "Strategy.Simple"
+            HighQuality -> "Strategy.HighQuality"
+            Balanced -> "Strategy.Balanced"
+            else -> "Invalid"
+        }
+    }
+
+    /**
+     * Describes the strictness of line breaking, determining before which characters
+     * line breaks can be inserted. It is useful when working with CJK scripts.
+     */
+    @JvmInline
+    value class Strictness private constructor(private val value: Int) {
+        companion object {
+            /**
+             * Default breaking rules for the locale, which may correspond to [Normal] or [Strict].
+             */
+            val Default: Strictness = Strictness(1)
+
+            /**
+             * The least restrictive rules, suitable for short lines.
+             *
+             * For example, in Japanese it allows breaking before iteration marks, such as 々, 〻.
+             */
+            val Loose: Strictness = Strictness(2)
+
+            /**
+             * The most common rules for line breaking.
+             *
+             * For example, in Japanese it allows breaking before characters like
+             * small hiragana (ぁ), small katakana (ァ), halfwidth variants (ァ).
+             */
+            val Normal: Strictness = Strictness(3)
+
+            /**
+             * The most stringent rules for line breaking.
+             *
+             * For example, in Japanese it does not allow breaking before characters like
+             * small hiragana (ぁ), small katakana (ァ), halfwidth variants (ァ).
+             */
+            val Strict: Strictness = Strictness(4)
+        }
+
+        override fun toString(): String = when (this) {
+            Default -> "Strictness.None"
+            Loose -> "Strictness.Loose"
+            Normal -> "Strictness.Normal"
+            Strict -> "Strictness.Strict"
+            else -> "Invalid"
+        }
+    }
+
+    /**
+     * Describes how line breaks should be inserted within words.
+     */
+    @JvmInline
+    value class WordBreak private constructor(private val value: Int) {
+        companion object {
+            /**
+             * Default word breaking rules for the locale.
+             * In latin scripts this means inserting line breaks between words,
+             * while in languages that don't use whitespace (e.g. Japanese) the line can break
+             * between characters.
+             *
+             * <pre>
+             * +---------+
+             * | This is |
+             * | an      |
+             * | example |
+             * | text.   |
+             * | 今日は自  |
+             * | 由が丘で  |
+             * | 焼き鳥を  |
+             * | 食べま   |
+             * | す。     |
+             * +---------+
+             * </pre>
+             */
+            val Default: WordBreak = WordBreak(1)
+
+            /**
+             * Line breaking is based on phrases.
+             * In languages that don't use whitespace (e.g. Japanese), line breaks are not inserted
+             * between characters that are part of the same phrase unit.
+             * This is ideal for short text such as titles and UI labels.
+             *
+             * <pre>
+             * +---------+
+             * | This    |
+             * | is an   |
+             * | example |
+             * | text.   |
+             * | 今日は   |
+             * | 自由が丘  |
+             * | で焼き鳥  |
+             * | を食べ   |
+             * | ます。   |
+             * +---------+
+             * </pre>
+             */
+            val Phrase: WordBreak = WordBreak(2)
+        }
+
+        override fun toString(): String = when (this) {
+            Default -> "WordBreak.None"
+            Phrase -> "WordBreak.Phrase"
+            else -> "Invalid"
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
index 4cba34a..9736a79 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ParagraphStyle.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDirection
@@ -49,6 +50,7 @@
  * line, whether to apply additional space as a result of line height to top of first line top and
  * bottom of last line. The configuration is applied only when a [lineHeight] is defined.
  * When null, [LineHeightStyle.Default] is used.
+ * @param lineBreak The line breaking configuration for the text.
  *
  * @see Paragraph
  * @see AnnotatedString
@@ -56,13 +58,15 @@
  * @see TextStyle
  */
 @Immutable
-class ParagraphStyle constructor(
+class ParagraphStyle @ExperimentalTextApi constructor(
     val textAlign: TextAlign? = null,
     val textDirection: TextDirection? = null,
     val lineHeight: TextUnit = TextUnit.Unspecified,
     val textIndent: TextIndent? = null,
     val platformStyle: PlatformParagraphStyle? = null,
-    val lineHeightStyle: LineHeightStyle? = null
+    val lineHeightStyle: LineHeightStyle? = null,
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalTextApi val lineBreak: LineBreak? = null
 ) {
 
     /**
@@ -100,6 +104,51 @@
         lineHeightStyle = null
     )
 
+    /**
+     * Paragraph styling configuration for a paragraph. The difference between [SpanStyle] and
+     * `ParagraphStyle` is that, `ParagraphStyle` can be applied to a whole [Paragraph] while
+     * [SpanStyle] can be applied at the character level.
+     * Once a portion of the text is marked with a `ParagraphStyle`, that portion will be separated from
+     * the remaining as if a line feed character was added.
+     *
+     * @sample androidx.compose.ui.text.samples.ParagraphStyleSample
+     * @sample androidx.compose.ui.text.samples.ParagraphStyleAnnotatedStringsSample
+     *
+     * @param textAlign The alignment of the text within the lines of the paragraph.
+     * @param textDirection The algorithm to be used to resolve the final text direction:
+     * Left To Right or Right To Left.
+     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+     * @param textIndent The indentation of the paragraph.
+     * @param platformStyle Platform specific [ParagraphStyle] parameters.
+     * @param lineHeightStyle the configuration for line height such as vertical alignment of the
+     * line, whether to apply additional space as a result of line height to top of first line top and
+     * bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+     * When null, [LineHeightStyle.Default] is used.
+     *
+     * @see Paragraph
+     * @see AnnotatedString
+     * @see SpanStyle
+     * @see TextStyle
+     */
+    // TODO(b/245939557): Deprecate this when LineBreak is stable
+    @OptIn(ExperimentalTextApi::class)
+    constructor(
+        textAlign: TextAlign? = null,
+        textDirection: TextDirection? = null,
+        lineHeight: TextUnit = TextUnit.Unspecified,
+        textIndent: TextIndent? = null,
+        platformStyle: PlatformParagraphStyle? = null,
+        lineHeightStyle: LineHeightStyle? = null
+    ) : this(
+        textAlign = textAlign,
+        textDirection = textDirection,
+        lineHeight = lineHeight,
+        textIndent = textIndent,
+        platformStyle = platformStyle,
+        lineHeightStyle = lineHeightStyle,
+        lineBreak = null
+    )
+
     init {
         if (lineHeight != TextUnit.Unspecified) {
             // Since we are checking if it's negative, no need to convert Sp into Px at this point.
@@ -115,6 +164,7 @@
      *
      * If the given paragraph style is null, returns this paragraph style.
      */
+    @OptIn(ExperimentalTextApi::class)
     @Stable
     fun merge(other: ParagraphStyle? = null): ParagraphStyle {
         if (other == null) return this
@@ -129,7 +179,8 @@
             textAlign = other.textAlign ?: this.textAlign,
             textDirection = other.textDirection ?: this.textDirection,
             platformStyle = mergePlatformStyle(other.platformStyle),
-            lineHeightStyle = other.lineHeightStyle ?: this.lineHeightStyle
+            lineHeightStyle = other.lineHeightStyle ?: this.lineHeightStyle,
+            lineBreak = other.lineBreak ?: this.lineBreak
         )
     }
 
@@ -145,6 +196,7 @@
     @Stable
     operator fun plus(other: ParagraphStyle): ParagraphStyle = this.merge(other)
 
+    @OptIn(ExperimentalTextApi::class)
     fun copy(
         textAlign: TextAlign? = this.textAlign,
         textDirection: TextDirection? = this.textDirection,
@@ -157,10 +209,13 @@
             lineHeight = lineHeight,
             textIndent = textIndent,
             platformStyle = this.platformStyle,
-            lineHeightStyle = this.lineHeightStyle
+            lineHeightStyle = this.lineHeightStyle,
+            lineBreak = this.lineBreak
         )
     }
 
+    // TODO(b/245939557): Deprecate this when LineBreak is stable
+    @OptIn(ExperimentalTextApi::class)
     fun copy(
         textAlign: TextAlign? = this.textAlign,
         textDirection: TextDirection? = this.textDirection,
@@ -175,10 +230,33 @@
             lineHeight = lineHeight,
             textIndent = textIndent,
             platformStyle = platformStyle,
-            lineHeightStyle = lineHeightStyle
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = this.lineBreak
         )
     }
 
+    @ExperimentalTextApi
+    fun copy(
+        textAlign: TextAlign? = this.textAlign,
+        textDirection: TextDirection? = this.textDirection,
+        lineHeight: TextUnit = this.lineHeight,
+        textIndent: TextIndent? = this.textIndent,
+        platformStyle: PlatformParagraphStyle? = this.platformStyle,
+        lineHeightStyle: LineHeightStyle? = this.lineHeightStyle,
+        lineBreak: LineBreak? = this.lineBreak
+    ): ParagraphStyle {
+        return ParagraphStyle(
+            textAlign = textAlign,
+            textDirection = textDirection,
+            lineHeight = lineHeight,
+            textIndent = textIndent,
+            platformStyle = platformStyle,
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak
+        )
+    }
+
+    @OptIn(ExperimentalTextApi::class)
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is ParagraphStyle) return false
@@ -189,10 +267,12 @@
         if (textIndent != other.textIndent) return false
         if (platformStyle != other.platformStyle) return false
         if (lineHeightStyle != other.lineHeightStyle) return false
+        if (lineBreak != other.lineBreak) return false
 
         return true
     }
 
+    @OptIn(ExperimentalTextApi::class)
     override fun hashCode(): Int {
         var result = textAlign?.hashCode() ?: 0
         result = 31 * result + (textDirection?.hashCode() ?: 0)
@@ -200,9 +280,11 @@
         result = 31 * result + (textIndent?.hashCode() ?: 0)
         result = 31 * result + (platformStyle?.hashCode() ?: 0)
         result = 31 * result + (lineHeightStyle?.hashCode() ?: 0)
+        result = 31 * result + (lineBreak?.hashCode() ?: 0)
         return result
     }
 
+    @OptIn(ExperimentalTextApi::class)
     override fun toString(): String {
         return "ParagraphStyle(" +
             "textAlign=$textAlign, " +
@@ -210,7 +292,8 @@
             "lineHeight=$lineHeight, " +
             "textIndent=$textIndent, " +
             "platformStyle=$platformStyle, " +
-            "lineHeightStyle=$lineHeightStyle" +
+            "lineHeightStyle=$lineHeightStyle, " +
+            "lineBreak=$lineBreak" +
             ")"
     }
 }
@@ -228,6 +311,7 @@
  * between [start] and [stop]. The interpolation can be extrapolated beyond 0.0 and
  * 1.0, so negative values and values greater than 1.0 are valid.
  */
+@OptIn(ExperimentalTextApi::class)
 @Stable
 fun lerp(start: ParagraphStyle, stop: ParagraphStyle, fraction: Float): ParagraphStyle {
     return ParagraphStyle(
@@ -248,7 +332,8 @@
             start.lineHeightStyle,
             stop.lineHeightStyle,
             fraction
-        )
+        ),
+        lineBreak = lerpDiscrete(start.lineBreak, stop.lineBreak, fraction)
     )
 }
 
@@ -263,6 +348,7 @@
     return lerp(startNonNull, stopNonNull, fraction)
 }
 
+@OptIn(ExperimentalTextApi::class)
 internal fun resolveParagraphStyleDefaults(
     style: ParagraphStyle,
     direction: LayoutDirection
@@ -272,5 +358,6 @@
     lineHeight = if (style.lineHeight.isUnspecified) DefaultLineHeight else style.lineHeight,
     textIndent = style.textIndent ?: TextIndent.None,
     platformStyle = style.platformStyle,
-    lineHeightStyle = style.lineHeightStyle
+    lineHeightStyle = style.lineHeightStyle,
+    lineBreak = style.lineBreak ?: LineBreak.Simple
 )
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
index 2f759cc..7d677e1 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
@@ -96,6 +97,7 @@
      * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
      * @param textIndent The indentation of the paragraph.
      */
+    @OptIn(ExperimentalTextApi::class)
     constructor(
         color: Color = Color.Unspecified,
         fontSize: TextUnit = TextUnit.Unspecified,
@@ -139,7 +141,8 @@
             lineHeight = lineHeight,
             textIndent = textIndent,
             platformStyle = null,
-            lineHeightStyle = null
+            lineHeightStyle = null,
+            lineBreak = null
         ),
         platformStyle = null
     )
@@ -179,6 +182,8 @@
      * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
      * When null, [LineHeightStyle.Default] is used.
      */
+    // TODO(b/245939557): Deprecate this when LineBreak is stable
+    @OptIn(ExperimentalTextApi::class)
     constructor(
         color: Color = Color.Unspecified,
         fontSize: TextUnit = TextUnit.Unspecified,
@@ -224,7 +229,97 @@
             lineHeight = lineHeight,
             textIndent = textIndent,
             platformStyle = platformStyle?.paragraphStyle,
-            lineHeightStyle = lineHeightStyle
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = null
+        ),
+        platformStyle = platformStyle
+    )
+
+    /**
+     * Styling configuration for a `Text`.
+     *
+     * @sample androidx.compose.ui.text.samples.TextStyleSample
+     *
+     * @param color The text color.
+     * @param fontSize The size of glyphs to use when painting the text. This
+     * may be [TextUnit.Unspecified] for inheriting from another [TextStyle].
+     * @param fontWeight The typeface thickness to use when painting the text (e.g., bold).
+     * @param fontStyle The typeface variant to use when drawing the letters (e.g., italic).
+     * @param fontSynthesis Whether to synthesize font weight and/or style when the requested weight
+     * or style cannot be found in the provided font family.
+     * @param fontFamily The font family to be used when rendering the text.
+     * @param fontFeatureSettings The advanced typography settings provided by font. The format is
+     * the same as the CSS font-feature-settings attribute:
+     * https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop
+     * @param letterSpacing The amount of space to add between each letter.
+     * @param baselineShift The amount by which the text is shifted up from the current baseline.
+     * @param textGeometricTransform The geometric transformation applied the text.
+     * @param localeList The locale list used to select region-specific glyphs.
+     * @param background The background color for the text.
+     * @param textDecoration The decorations to paint on the text (e.g., an underline).
+     * @param shadow The shadow effect applied on the text.
+     * @param textAlign The alignment of the text within the lines of the paragraph.
+     * @param textDirection The algorithm to be used to resolve the final text and paragraph
+     * direction: Left To Right or Right To Left. If no value is provided the system will use the
+     * [LayoutDirection] as the primary signal.
+     * @param lineHeight Line height for the [Paragraph] in [TextUnit] unit, e.g. SP or EM.
+     * @param textIndent The indentation of the paragraph.
+     * @param platformStyle Platform specific [TextStyle] parameters.
+     * @param lineHeightStyle the configuration for line height such as vertical alignment of the
+     * line, whether to apply additional space as a result of line height to top of first line top
+     * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+     * When null, [LineHeightStyle.Default] is used.
+     * @param lineBreak The line breaking configuration for the text.
+     */
+    @ExperimentalTextApi
+    constructor(
+        color: Color = Color.Unspecified,
+        fontSize: TextUnit = TextUnit.Unspecified,
+        fontWeight: FontWeight? = null,
+        fontStyle: FontStyle? = null,
+        fontSynthesis: FontSynthesis? = null,
+        fontFamily: FontFamily? = null,
+        fontFeatureSettings: String? = null,
+        letterSpacing: TextUnit = TextUnit.Unspecified,
+        baselineShift: BaselineShift? = null,
+        textGeometricTransform: TextGeometricTransform? = null,
+        localeList: LocaleList? = null,
+        background: Color = Color.Unspecified,
+        textDecoration: TextDecoration? = null,
+        shadow: Shadow? = null,
+        textAlign: TextAlign? = null,
+        textDirection: TextDirection? = null,
+        lineHeight: TextUnit = TextUnit.Unspecified,
+        textIndent: TextIndent? = null,
+        platformStyle: PlatformTextStyle? = null,
+        lineHeightStyle: LineHeightStyle? = null,
+        lineBreak: LineBreak? = null
+    ) : this(
+        SpanStyle(
+            color = color,
+            fontSize = fontSize,
+            fontWeight = fontWeight,
+            fontStyle = fontStyle,
+            fontSynthesis = fontSynthesis,
+            fontFamily = fontFamily,
+            fontFeatureSettings = fontFeatureSettings,
+            letterSpacing = letterSpacing,
+            baselineShift = baselineShift,
+            textGeometricTransform = textGeometricTransform,
+            localeList = localeList,
+            background = background,
+            textDecoration = textDecoration,
+            shadow = shadow,
+            platformStyle = platformStyle?.spanStyle
+        ),
+        ParagraphStyle(
+            textAlign = textAlign,
+            textDirection = textDirection,
+            lineHeight = lineHeight,
+            textIndent = textIndent,
+            platformStyle = platformStyle?.paragraphStyle,
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak
         ),
         platformStyle = platformStyle
     )
@@ -266,6 +361,7 @@
      * @param lineHeightStyle the configuration for line height such as vertical alignment of the
      * line, whether to apply additional space as a result of line height to top of first line top
      * and bottom of last line. The configuration is applied only when a [lineHeight] is defined.
+     * @param lineBreak The line breaking configuration for the text.
      */
     @ExperimentalTextApi
     constructor(
@@ -289,7 +385,8 @@
         lineHeight: TextUnit = TextUnit.Unspecified,
         textIndent: TextIndent? = null,
         platformStyle: PlatformTextStyle? = null,
-        lineHeightStyle: LineHeightStyle? = null
+        lineHeightStyle: LineHeightStyle? = null,
+        lineBreak: LineBreak? = null
     ) : this(
         SpanStyle(
             brush = brush,
@@ -315,7 +412,8 @@
             lineHeight = lineHeight,
             textIndent = textIndent,
             platformStyle = platformStyle?.paragraphStyle,
-            lineHeightStyle = lineHeightStyle
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak
         ),
         platformStyle = platformStyle
     )
@@ -388,6 +486,7 @@
     @Stable
     operator fun plus(other: SpanStyle): TextStyle = this.merge(other)
 
+    @OptIn(ExperimentalTextApi::class)
     fun copy(
         color: Color = this.spanStyle.color,
         fontSize: TextUnit = this.spanStyle.fontSize,
@@ -436,12 +535,15 @@
                 lineHeight = lineHeight,
                 textIndent = textIndent,
                 platformStyle = this.paragraphStyle.platformStyle,
-                lineHeightStyle = this.lineHeightStyle
+                lineHeightStyle = this.lineHeightStyle,
+                lineBreak = this.lineBreak
             ),
             platformStyle = this.platformStyle
         )
     }
 
+    // TODO(b/245939557): Deprecate this when LineBreak is stable
+    @OptIn(ExperimentalTextApi::class)
     fun copy(
         color: Color = this.spanStyle.color,
         fontSize: TextUnit = this.spanStyle.fontSize,
@@ -492,7 +594,67 @@
                 lineHeight = lineHeight,
                 textIndent = textIndent,
                 platformStyle = platformStyle?.paragraphStyle,
-                lineHeightStyle = lineHeightStyle
+                lineHeightStyle = lineHeightStyle,
+                lineBreak = this.lineBreak
+            ),
+            platformStyle = platformStyle
+        )
+    }
+
+    @ExperimentalTextApi
+    fun copy(
+        color: Color = this.spanStyle.color,
+        fontSize: TextUnit = this.spanStyle.fontSize,
+        fontWeight: FontWeight? = this.spanStyle.fontWeight,
+        fontStyle: FontStyle? = this.spanStyle.fontStyle,
+        fontSynthesis: FontSynthesis? = this.spanStyle.fontSynthesis,
+        fontFamily: FontFamily? = this.spanStyle.fontFamily,
+        fontFeatureSettings: String? = this.spanStyle.fontFeatureSettings,
+        letterSpacing: TextUnit = this.spanStyle.letterSpacing,
+        baselineShift: BaselineShift? = this.spanStyle.baselineShift,
+        textGeometricTransform: TextGeometricTransform? = this.spanStyle.textGeometricTransform,
+        localeList: LocaleList? = this.spanStyle.localeList,
+        background: Color = this.spanStyle.background,
+        textDecoration: TextDecoration? = this.spanStyle.textDecoration,
+        shadow: Shadow? = this.spanStyle.shadow,
+        textAlign: TextAlign? = this.paragraphStyle.textAlign,
+        textDirection: TextDirection? = this.paragraphStyle.textDirection,
+        lineHeight: TextUnit = this.paragraphStyle.lineHeight,
+        textIndent: TextIndent? = this.paragraphStyle.textIndent,
+        platformStyle: PlatformTextStyle? = this.platformStyle,
+        lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle,
+        lineBreak: LineBreak? = this.paragraphStyle.lineBreak
+    ): TextStyle {
+        return TextStyle(
+            spanStyle = SpanStyle(
+                textForegroundStyle = if (color == this.spanStyle.color) {
+                    spanStyle.textForegroundStyle
+                } else {
+                    TextForegroundStyle.from(color)
+                },
+                fontSize = fontSize,
+                fontWeight = fontWeight,
+                fontStyle = fontStyle,
+                fontSynthesis = fontSynthesis,
+                fontFamily = fontFamily,
+                fontFeatureSettings = fontFeatureSettings,
+                letterSpacing = letterSpacing,
+                baselineShift = baselineShift,
+                textGeometricTransform = textGeometricTransform,
+                localeList = localeList,
+                background = background,
+                textDecoration = textDecoration,
+                shadow = shadow,
+                platformStyle = platformStyle?.spanStyle
+            ),
+            paragraphStyle = ParagraphStyle(
+                textAlign = textAlign,
+                textDirection = textDirection,
+                lineHeight = lineHeight,
+                textIndent = textIndent,
+                platformStyle = platformStyle?.paragraphStyle,
+                lineHeightStyle = lineHeightStyle,
+                lineBreak = lineBreak
             ),
             platformStyle = platformStyle
         )
@@ -520,7 +682,8 @@
         lineHeight: TextUnit = this.paragraphStyle.lineHeight,
         textIndent: TextIndent? = this.paragraphStyle.textIndent,
         platformStyle: PlatformTextStyle? = this.platformStyle,
-        lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle
+        lineHeightStyle: LineHeightStyle? = this.paragraphStyle.lineHeightStyle,
+        lineBreak: LineBreak? = this.paragraphStyle.lineBreak
     ): TextStyle {
         return TextStyle(
             spanStyle = SpanStyle(
@@ -547,7 +710,8 @@
                 lineHeight = lineHeight,
                 textIndent = textIndent,
                 platformStyle = platformStyle?.paragraphStyle,
-                lineHeightStyle = lineHeightStyle
+                lineHeightStyle = lineHeightStyle,
+                lineBreak = lineBreak
             ),
             platformStyle = platformStyle
         )
@@ -677,6 +841,14 @@
      */
     val lineHeightStyle: LineHeightStyle? get() = this.paragraphStyle.lineHeightStyle
 
+    /**
+     * The line breaking configuration of the paragraph.
+     */
+    @ExperimentalTextApi
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalTextApi
+    val lineBreak: LineBreak? get() = this.paragraphStyle.lineBreak
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is TextStyle) return false
@@ -742,8 +914,9 @@
             "textDirection=$textDirection, " +
             "lineHeight=$lineHeight, " +
             "textIndent=$textIndent, " +
-            "platformStyle=$platformStyle" +
-            "lineHeightStyle=$lineHeightStyle" +
+            "platformStyle=$platformStyle, " +
+            "lineHeightStyle=$lineHeightStyle, " +
+            "lineBreak=$lineBreak" +
             ")"
     }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt
new file mode 100644
index 0000000..337f6af9
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/style/LineBreak.kt
@@ -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.compose.ui.text.style
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
+
+/**
+ * When soft wrap is enabled and the width of the text exceeds the width of its container,
+ * line breaks are inserted in the text to split it over multiple lines.
+ *
+ * There are a number of parameters that affect how the line breaks are inserted.
+ * For example, the breaking algorithm can be changed to one with improved readability
+ * at the cost of speed.
+ * Another example is the strictness, which in some languages determines which symbols can appear
+ * at the start of a line.
+ *
+ * `LineBreak` represents a configuration for line breaking, offering several presets
+ * for different use cases: [Simple], [Heading], [Paragraph].
+ *
+ * @sample androidx.compose.ui.text.samples.LineBreakSample
+ *
+ * For further customization, each platform has its own parameters. An example on Android:
+ *
+ * @sample androidx.compose.ui.text.samples.AndroidLineBreakSample
+ */
+@ExperimentalTextApi
+@Immutable
+expect class LineBreak {
+    companion object {
+        /**
+         * Basic, fast line breaking. Ideal for text input fields, as it will cause minimal
+         * text reflow when editing.
+         */
+        val Simple: LineBreak
+
+        /**
+         * Looser breaking rules, suitable for short text such as titles or narrow newspaper
+         * columns. For longer lines of text, use [Paragraph] for improved readability.
+         */
+        val Heading: LineBreak
+
+        /**
+         * Slower, higher quality line breaking for improved readability.
+         * Suitable for larger amounts of text.
+         */
+        val Paragraph: LineBreak
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt
new file mode 100644
index 0000000..21b4e2c
--- /dev/null
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/style/LineBreak.skiko.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.text.style
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.text.ExperimentalTextApi
+
+@Immutable
+@ExperimentalTextApi
+actual class LineBreak private constructor() {
+    actual companion object {
+        actual val Simple: LineBreak = LineBreak()
+
+        actual val Heading: LineBreak = LineBreak()
+
+        actual val Paragraph: LineBreak = LineBreak()
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
index 23888e3..7778d33 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/ParagraphStyleTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.text
 
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.LineHeightStyle.Trim
 import androidx.compose.ui.text.style.LineHeightStyle.Alignment
@@ -163,6 +164,50 @@
 
     @OptIn(ExperimentalTextApi::class)
     @Test
+    fun `merge null with non-null lineBreak uses other's lineBreak`() {
+        val style = ParagraphStyle(lineBreak = null)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Heading)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge non-null with null lineBreak returns original's lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = ParagraphStyle(lineBreak = null)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(style.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge null with null lineBreak returns null`() {
+        val style = ParagraphStyle(lineBreak = null)
+        val otherStyle = ParagraphStyle(lineBreak = null)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(null)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge non-null with non-null lineBreak returns other's lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Heading)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
     fun `merge null platformStyles`() {
         val style1 = ParagraphStyle(platformStyle = null)
         val style2 = ParagraphStyle(platformStyle = null)
@@ -377,6 +422,50 @@
 
     @OptIn(ExperimentalTextApi::class)
     @Test
+    fun `lerp with non-null start, null end, closer to start has non-null lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Heading)
+        val otherStyle = ParagraphStyle(lineHeightStyle = null)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
+
+        assertThat(lerpedStyle.lineBreak).isSameInstanceAs(style.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with non-null start, null end, closer to end has null lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Heading)
+        val otherStyle = ParagraphStyle(lineHeightStyle = null)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
+
+        assertThat(lerpedStyle.lineBreak).isNull()
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with null start, non-null end, closer to start has null lineBreak`() {
+        val style = ParagraphStyle(lineHeightStyle = null)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Heading)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
+
+        assertThat(lerpedStyle.lineBreak).isNull()
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with null start, non-null end, closer to end has non-null lineBreak`() {
+        val style = ParagraphStyle(lineBreak = null)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Heading)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
+
+        assertThat(lerpedStyle.lineBreak).isSameInstanceAs(otherStyle.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
     fun `equals return false for different line height behavior`() {
         val style = ParagraphStyle(lineHeightStyle = null)
         val otherStyle = ParagraphStyle(lineHeightStyle = LineHeightStyle.Default)
@@ -514,4 +603,40 @@
 
         assertThat(style.lineHeightStyle).isNull()
     }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `copy with lineBreak returns new lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val newStyle = style.copy(lineBreak = LineBreak.Heading)
+
+        assertThat(newStyle.lineBreak).isEqualTo(LineBreak.Heading)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `copy without lineBreak uses existing lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val newStyle = style.copy()
+
+        assertThat(newStyle.lineBreak).isEqualTo(style.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `hashCode is same for same lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+
+        assertThat(style.hashCode()).isEqualTo(otherStyle.hashCode())
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `hashCode is different for different lineBreak`() {
+        val style = ParagraphStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = ParagraphStyle(lineBreak = LineBreak.Heading)
+
+        assertThat(style.hashCode()).isNotEqualTo(otherStyle.hashCode())
+    }
 }
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
index d7b1aae..3b12bd3 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleLayoutAttributesTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.LineHeightStyle.Trim
 import androidx.compose.ui.text.style.LineHeightStyle.Alignment
@@ -328,6 +329,21 @@
         ).isFalse()
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun returns_false_for_lineBreak_change() {
+        val style = TextStyle(
+            lineBreak = LineBreak.Heading
+        )
+        assertThat(
+            style.hasSameLayoutAffectingAttributes(
+                TextStyle(
+                    lineBreak = LineBreak.Paragraph
+                )
+            )
+        ).isFalse()
+    }
+
     @Test
     fun should_be_updated_when_a_new_attribute_is_added_to_TextStyle() {
         // TextLayoutHelper TextStyle.hasSameLayoutAffectingAttributes is very easy to forget
@@ -360,7 +376,8 @@
             // ui-text/../androidx/compose/ui/text/TextSpanParagraphStyleTest.kt
             getProperty("paragraphStyle"),
             getProperty("spanStyle"),
-            getProperty("lineHeightStyle")
+            getProperty("lineHeightStyle"),
+            getProperty("lineBreak")
         )
 
         val textStyleProperties = TextStyle::class.memberProperties.map { Property(it) }
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
index 0e5d80f..a773e49 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleResolveDefaultsTest.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.text.style.TextDirection
@@ -72,6 +73,7 @@
             assertThat(it.lineHeight).isEqualTo(DefaultLineHeight)
             assertThat(it.textIndent).isEqualTo(TextIndent.None)
             assertThat(it.platformStyle).isNull()
+            assertThat(it.lineBreak).isEqualTo(LineBreak.Simple)
         }
     }
 
@@ -271,6 +273,17 @@
         ).isEqualTo(TextIndent(12.sp, 13.sp))
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun test_use_provided_values_lineBreak() {
+        assertThat(
+            resolveDefaults(
+                TextStyle(lineBreak = LineBreak.Heading),
+                direction = LayoutDirection.Ltr
+            ).lineBreak
+        ).isEqualTo(LineBreak.Heading)
+    }
+
     @Test
     fun test_use_provided_values_textDirection_with_LTR_layoutDirection() {
         assertThat(
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
index bd0599d..6b5a98f 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/TextStyleTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.text.font.lerp
 import androidx.compose.ui.text.intl.LocaleList
 import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.LineHeightStyle.Trim
 import androidx.compose.ui.text.style.LineHeightStyle.Alignment
@@ -66,6 +67,7 @@
         assertThat(style.textDecoration).isNull()
         assertThat(style.fontFamily).isNull()
         assertThat(style.platformStyle).isNull()
+        assertThat(style.lineBreak).isNull()
     }
 
     @OptIn(ExperimentalTextApi::class)
@@ -150,6 +152,26 @@
         }
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `copy with lineBreak returns new lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Paragraph)
+
+        val newStyle = style.copy(lineBreak = LineBreak.Heading)
+
+        assertThat(newStyle.lineBreak).isEqualTo(LineBreak.Heading)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `copy without lineBreak uses existing lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Paragraph)
+
+        val newStyle = style.copy()
+
+        assertThat(newStyle.lineBreak).isEqualTo(style.lineBreak)
+    }
+
     @Test
     fun `constructor with customized color`() {
         val color = Color.Red
@@ -251,6 +273,14 @@
         assertThat(style.fontFamily).isEqualTo(fontFamily)
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `constructor with customized lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Heading)
+
+        assertThat(style.lineBreak).isEqualTo(LineBreak.Heading)
+    }
+
     @Test
     fun `merge with empty other should return this`() {
         val style = TextStyle()
@@ -669,6 +699,50 @@
         assertThat(mergedStyle.alpha).isEqualTo(0.3f)
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge null and non-null lineBreak uses other's lineBreak`() {
+        val style = TextStyle(lineBreak = null)
+        val otherStyle = TextStyle(lineBreak = LineBreak.Heading)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge non-null and null lineBreak uses original`() {
+        val style = TextStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = TextStyle(lineBreak = null)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(style.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge with both null lineBreak uses null`() {
+        val style = TextStyle(lineBreak = null)
+        val otherStyle = TextStyle(lineBreak = null)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(null)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `merge with both non-null lineBreak uses other's lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Paragraph)
+        val otherStyle = TextStyle(lineBreak = LineBreak.Heading)
+
+        val mergedStyle = style.merge(otherStyle)
+
+        assertThat(mergedStyle.lineBreak).isEqualTo(otherStyle.lineBreak)
+    }
+
     @Test
     fun `plus operator merges other TextStyle`() {
         val style = TextStyle(
@@ -1210,6 +1284,50 @@
         assertThat(newStyle.color).isEqualTo(Color.Unspecified)
     }
 
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with non-null start, null end, closer to start has non-null lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Heading)
+        val otherStyle = TextStyle(lineHeightStyle = null)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
+
+        assertThat(lerpedStyle.lineBreak).isSameInstanceAs(style.lineBreak)
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with non-null start, null end, closer to end has null lineBreak`() {
+        val style = TextStyle(lineBreak = LineBreak.Heading)
+        val otherStyle = TextStyle(lineHeightStyle = null)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
+
+        assertThat(lerpedStyle.lineBreak).isNull()
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with null start, non-null end, closer to start has null lineBreak`() {
+        val style = TextStyle(lineHeightStyle = null)
+        val otherStyle = TextStyle(lineBreak = LineBreak.Heading)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.4f)
+
+        assertThat(lerpedStyle.lineBreak).isNull()
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    @Test
+    fun `lerp with null start, non-null end, closer to end has non-null lineBreak`() {
+        val style = TextStyle(lineBreak = null)
+        val otherStyle = TextStyle(lineBreak = LineBreak.Heading)
+
+        val lerpedStyle = lerp(start = style, stop = otherStyle, fraction = 0.6f)
+
+        assertThat(lerpedStyle.lineBreak).isSameInstanceAs(otherStyle.lineBreak)
+    }
+
     @Test
     fun `toSpanStyle return attributes with correct values`() {
         val color = Color.Red
@@ -1330,13 +1448,19 @@
             alignment = Alignment.Center,
             trim = Trim.None
         )
+        val lineBreak = LineBreak(
+            strategy = LineBreak.Strategy.Balanced,
+            strictness = LineBreak.Strictness.Strict,
+            wordBreak = LineBreak.WordBreak.Phrase
+        )
 
         val style = TextStyle(
             textAlign = textAlign,
             textDirection = textDirection,
             lineHeight = lineHeight,
             textIndent = textIndent,
-            lineHeightStyle = lineHeightStyle
+            lineHeightStyle = lineHeightStyle,
+            lineBreak = lineBreak
         )
 
         assertThat(style.toParagraphStyle()).isEqualTo(
@@ -1345,7 +1469,8 @@
                 textDirection = textDirection,
                 lineHeight = lineHeight,
                 textIndent = textIndent,
-                lineHeightStyle = lineHeightStyle
+                lineHeightStyle = lineHeightStyle,
+                lineBreak = lineBreak
             )
         )
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index 91c3e9d..5588662 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -53,7 +53,6 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Popup
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -548,7 +547,6 @@
         assertThat(childCoordinates!!.positionInParent().x).isEqualTo(thirdPaddingPx)
     }
 
-    @FlakyTest(bugId = 213889751)
     @Test
     fun globalCoordinatesAreInActivityCoordinates() {
         val padding = 30
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a29e428..7b6786d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -724,14 +724,16 @@
      * from the hierarchy.
      */
     fun removeAndroidView(view: AndroidViewHolder) {
-        androidViewsHandler.removeView(view)
-        androidViewsHandler.layoutNodeToHolder.remove(
-            androidViewsHandler.holderToLayoutNode.remove(view)
-        )
-        ViewCompat.setImportantForAccessibility(
-            view,
-            ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO
-        )
+        registerOnEndApplyChangesListener {
+            androidViewsHandler.removeViewInLayout(view)
+            androidViewsHandler.layoutNodeToHolder.remove(
+                androidViewsHandler.holderToLayoutNode.remove(view)
+            )
+            ViewCompat.setImportantForAccessibility(
+                view,
+                ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+            )
+        }
     }
 
     /**
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index e811939..c035945 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -89,7 +89,7 @@
         internal set(value) {
             if (value !== field) {
                 field = value
-                removeAllViews()
+                removeAllViewsInLayout()
                 if (value != null) {
                     addView(value)
                     runUpdate()
diff --git a/core/core/src/main/java/androidx/core/app/NotificationCompat.java b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
index 3e79961..c810410 100644
--- a/core/core/src/main/java/androidx/core/app/NotificationCompat.java
+++ b/core/core/src/main/java/androidx/core/app/NotificationCompat.java
@@ -2982,11 +2982,11 @@
                     // need to shrink all the type to make sure everything fits
                     final float subTextSize = res.getDimensionPixelSize(
                             R.dimen.notification_subtext_size);
-                    contentView.setTextViewTextSize(R.id.text, TypedValue.COMPLEX_UNIT_PX,
-                            subTextSize);
+                    Api16Impl.setTextViewTextSize(contentView, R.id.text,
+                            TypedValue.COMPLEX_UNIT_PX, subTextSize);
                 }
                 // vertical centering
-                contentView.setViewPadding(R.id.line1, 0, 0, 0, 0);
+                Api16Impl.setViewPadding(contentView, R.id.line1, 0, 0, 0, 0);
             }
 
             if (mBuilder.getWhenIfShowing() != 0) {
@@ -2997,7 +2997,7 @@
                                     + (SystemClock.elapsedRealtime() - System.currentTimeMillis()));
                     contentView.setBoolean(R.id.chronometer, "setStarted", true);
                     if (mBuilder.mChronometerCountDown && Build.VERSION.SDK_INT >= 24) {
-                        contentView.setChronometerCountDown(R.id.chronometer,
+                        Api24Impl.setChronometerCountDown(contentView, R.id.chronometer,
                                 mBuilder.mChronometerCountDown);
                     }
                 } else {
@@ -3078,8 +3078,8 @@
             outerView.setViewVisibility(R.id.notification_main_column, View.VISIBLE);
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                 // Adjust padding depending on font size.
-                outerView.setViewPadding(R.id.notification_main_column_container,
-                        0, calculateTopPadding(), 0, 0);
+                Api16Impl.setViewPadding(outerView, R.id.notification_main_column_container, 0,
+                        calculateTopPadding(), 0, 0);
             }
         }
 
@@ -3105,6 +3105,45 @@
         private static float constrain(float amount, float low, float high) {
             return amount < low ? low : (amount > high ? high : amount);
         }
+
+        /**
+         * A class for wrapping calls to {@link NotificationCompat.Style} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() { }
+
+            @DoNotInline
+            static void setTextViewTextSize(RemoteViews remoteViews, int viewId, int units,
+                    float size) {
+                remoteViews.setTextViewTextSize(viewId, units, size);
+            }
+
+            @DoNotInline
+            static void setViewPadding(RemoteViews remoteViews, int viewId, int left, int top,
+                    int right, int bottom) {
+                remoteViews.setViewPadding(viewId, left, top, right, bottom);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link NotificationCompat.Style} methods which
+         * were added in API 24; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(24)
+        static class Api24Impl {
+            private Api24Impl() { }
+
+            @DoNotInline
+            static void setChronometerCountDown(RemoteViews remoteViews, int viewId,
+                    boolean isCountDown) {
+                remoteViews.setChronometerCountDown(viewId, isCountDown);
+            }
+
+        }
     }
 
     /**
@@ -3231,8 +3270,10 @@
         public void apply(NotificationBuilderWithBuilderAccessor builder) {
             if (Build.VERSION.SDK_INT >= 16) {
                 Notification.BigPictureStyle style =
-                        new Notification.BigPictureStyle(builder.getBuilder())
-                                .setBigContentTitle(mBigContentTitle);
+                        Api16Impl.setBigContentTitle(
+                                Api16Impl.createBigPictureStyle(builder.getBuilder()),
+                                mBigContentTitle
+                        );
                 if (mPictureIcon != null) {
                     // Attempts to set the icon for BigPictureStyle; prefers using data as Icon,
                     // with a fallback to store the Bitmap if Icon is not supported directly.
@@ -3243,7 +3284,7 @@
                         }
                         Api31Impl.setBigPicture(style, mPictureIcon.toIcon(context));
                     } else if (mPictureIcon.getType() == IconCompat.TYPE_BITMAP) {
-                        style = style.bigPicture(mPictureIcon.getBitmap());
+                        style = Api16Impl.bigPicture(style, mPictureIcon.getBitmap());
                     }
                 }
                 // Attempts to set the big large icon for BigPictureStyle.
@@ -3348,6 +3389,18 @@
             private Api16Impl() {
             }
 
+            @DoNotInline
+            static Notification.BigPictureStyle bigPicture(
+                    Notification.BigPictureStyle bigPictureStyle, Bitmap b) {
+                return bigPictureStyle.bigPicture(b);
+            }
+
+            @DoNotInline
+            static Notification.BigPictureStyle createBigPictureStyle(
+                    Notification.Builder builder) {
+                return new Notification.BigPictureStyle(builder);
+            }
+
             /**
              * Calls {@link Notification.BigPictureStyle#bigLargeIcon(Bitmap)}
              */
@@ -3363,6 +3416,12 @@
             static void setSummaryText(Notification.BigPictureStyle style, CharSequence text) {
                 style.setSummaryText(text);
             }
+
+            @DoNotInline
+            static Notification.BigPictureStyle setBigContentTitle(
+                    Notification.BigPictureStyle bigPictureStyle, CharSequence title) {
+                return bigPictureStyle.setBigContentTitle(title);
+            }
         }
 
         /**
@@ -3502,11 +3561,11 @@
         public void apply(NotificationBuilderWithBuilderAccessor builder) {
             if (Build.VERSION.SDK_INT >= 16) {
                 Notification.BigTextStyle style =
-                        new Notification.BigTextStyle(builder.getBuilder())
-                                .setBigContentTitle(mBigContentTitle)
-                                .bigText(mBigText);
+                        Api16Impl.createBigTextStyle(builder.getBuilder());
+                style = Api16Impl.setBigContentTitle(style, mBigContentTitle);
+                style = Api16Impl.bigText(style, mBigText);
                 if (mSummaryTextSet) {
-                    style.setSummaryText(mSummaryText);
+                    Api16Impl.setSummaryText(style, mSummaryText);
                 }
             }
         }
@@ -3547,6 +3606,40 @@
             super.clearCompatExtraKeys(extras);
             extras.remove(EXTRA_BIG_TEXT);
         }
+
+        /**
+         * A class for wrapping calls to {@link Notification.BigTextStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() {
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle createBigTextStyle(Notification.Builder builder) {
+                return new Notification.BigTextStyle(builder);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle setBigContentTitle(
+                    Notification.BigTextStyle bigTextStyle, CharSequence title) {
+                return bigTextStyle.setBigContentTitle(title);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle bigText(
+                    Notification.BigTextStyle bigTextStyle, CharSequence bigText) {
+                return bigTextStyle.bigText(bigText);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle setSummaryText(Notification.BigTextStyle bigTextStyle,
+                    CharSequence cs) {
+                return bigTextStyle.setSummaryText(cs);
+            }
+        }
     }
 
     /**
@@ -3820,7 +3913,7 @@
         /**
          * Retrieves a {@link MessagingStyle} from a {@link Notification}, enabling an application
          * that has set a {@link MessagingStyle} using {@link NotificationCompat} or
-         * {@link android.app.Notification.Builder} to send messaging information to another
+         * {@link Notification.Builder} to send messaging information to another
          * application using {@link NotificationCompat}, regardless of the API level of the system.
          *
          * @return {@code null} if there is no {@link MessagingStyle} set, or if the SDK version is
@@ -3829,7 +3922,7 @@
         @Nullable
         public static MessagingStyle extractMessagingStyleFromNotification(
                 @NonNull Notification notification) {
-            Style style = NotificationCompat.Style.extractStyleFromNotification(notification);
+            Style style = Style.extractStyleFromNotification(notification);
             if (style instanceof MessagingStyle) {
                 return (MessagingStyle) style;
             }
@@ -3859,20 +3952,24 @@
             setGroupConversation(isGroupConversation());
 
             if (Build.VERSION.SDK_INT >= 24) {
-                Notification.MessagingStyle frameworkStyle;
+                Object frameworkStyle;
                 if (Build.VERSION.SDK_INT >= 28) {
-                    frameworkStyle = new Notification.MessagingStyle(mUser.toAndroidPerson());
+                    frameworkStyle = Api28Impl.createMessagingStyle(mUser.toAndroidPerson());
                 } else {
-                    frameworkStyle = new Notification.MessagingStyle(mUser.getName());
+                    frameworkStyle =
+                            Api24Impl.createMessagingStyle(
+                                    mUser.getName());
                 }
 
-                for (MessagingStyle.Message message : mMessages) {
-                    frameworkStyle.addMessage(message.toAndroidMessage());
+                for (Message message : mMessages) {
+                    Api24Impl.addMessage((Notification.MessagingStyle) frameworkStyle,
+                            message.toAndroidMessage());
                 }
 
                 if (Build.VERSION.SDK_INT >= 26) {
-                    for (MessagingStyle.Message historicMessage : mHistoricMessages) {
-                        frameworkStyle.addHistoricMessage(historicMessage.toAndroidMessage());
+                    for (Message historicMessage : mHistoricMessages) {
+                        Api26Impl.addHistoricMessage((Notification.MessagingStyle) frameworkStyle,
+                                historicMessage.toAndroidMessage());
                     }
                 }
 
@@ -3884,17 +3981,20 @@
                 // Notification content title so Android won't think it's a group conversation.
                 if (mIsGroupConversation || Build.VERSION.SDK_INT >= 28) {
                     // If group or non-legacy, set MessagingStyle#mConversationTitle.
-                    frameworkStyle.setConversationTitle(mConversationTitle);
+                    Api24Impl.setConversationTitle((Notification.MessagingStyle) frameworkStyle,
+                            mConversationTitle);
                 }
 
                 // For SDK >= 28, we can simply denote the group conversation status regardless of
                 // if we set the conversation title or not.
                 if (Build.VERSION.SDK_INT >= 28) {
-                    frameworkStyle.setGroupConversation(mIsGroupConversation);
+                    Api28Impl.setGroupConversation((Notification.MessagingStyle) frameworkStyle,
+                            mIsGroupConversation);
                 }
-                frameworkStyle.setBuilder(builder.getBuilder());
+                Api16Impl.setBuilder((Notification.MessagingStyle) frameworkStyle,
+                        builder.getBuilder());
             } else {
-                MessagingStyle.Message latestIncomingMessage = findLatestIncomingMessage();
+                Message latestIncomingMessage = findLatestIncomingMessage();
                 // Set the title
                 if (mConversationTitle != null && mIsGroupConversation) {
                     builder.getBuilder().setContentTitle(mConversationTitle);
@@ -3917,7 +4017,7 @@
                     boolean showNames = mConversationTitle != null
                             || hasMessagesWithoutSender();
                     for (int i = mMessages.size() - 1; i >= 0; i--) {
-                        MessagingStyle.Message message = mMessages.get(i);
+                        Message message = mMessages.get(i);
                         CharSequence line;
                         line = showNames ? makeMessageLine(message) : message.getText();
                         if (i != mMessages.size() - 1) {
@@ -3925,17 +4025,18 @@
                         }
                         completeMessage.insert(0, line);
                     }
-                    new Notification.BigTextStyle(builder.getBuilder())
-                            .setBigContentTitle(null)
-                            .bigText(completeMessage);
+                    Notification.BigTextStyle style =
+                            Api16Impl.createBigTextStyle(builder.getBuilder());
+                    style = Api16Impl.setBigContentTitle(style, null);
+                    Api16Impl.bigText(style, completeMessage);
                 }
             }
         }
 
         @Nullable
-        private MessagingStyle.Message findLatestIncomingMessage() {
+        private Message findLatestIncomingMessage() {
             for (int i = mMessages.size() - 1; i >= 0; i--) {
-                MessagingStyle.Message message = mMessages.get(i);
+                Message message = mMessages.get(i);
                 // Incoming messages have a non-empty sender.
                 if (message.getPerson() != null
                         && !TextUtils.isEmpty(message.getPerson().getName())) {
@@ -3951,7 +4052,7 @@
 
         private boolean hasMessagesWithoutSender() {
             for (int i = mMessages.size() - 1; i >= 0; i--) {
-                MessagingStyle.Message message = mMessages.get(i);
+                Message message = mMessages.get(i);
                 if (message.getPerson() != null && message.getPerson().getName() == null) {
                     return true;
                 }
@@ -3959,7 +4060,7 @@
             return false;
         }
 
-        private CharSequence makeMessageLine(@NonNull MessagingStyle.Message message) {
+        private CharSequence makeMessageLine(@NonNull Message message) {
             BidiFormatter bidi = BidiFormatter.getInstance();
             SpannableStringBuilder sb = new SpannableStringBuilder();
             final boolean afterLollipop = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
@@ -4304,7 +4405,7 @@
 
             /**
              * Converts this compat {@link Message} to the base Android framework
-             * {@link android.app.Notification.MessagingStyle.Message}.
+             * {@link Notification.MessagingStyle.Message}.
              * @hide
              */
             @RestrictTo(LIBRARY_GROUP_PREFIX)
@@ -4315,20 +4416,160 @@
                 Person person = getPerson();
                 // Use Person for P and above
                 if (Build.VERSION.SDK_INT >= 28) {
-                    frameworkMessage = new Notification.MessagingStyle.Message(
-                            getText(), getTimestamp(),
+                    frameworkMessage = Api28Impl.createMessage(getText(), getTimestamp(),
                             person == null ? null : person.toAndroidPerson());
                 } else {
-                    frameworkMessage = new Notification.MessagingStyle.Message(
-                            getText(), getTimestamp(),
+                    frameworkMessage = Api24Impl.createMessage(getText(), getTimestamp(),
                             person == null ? null : person.getName());
                 }
 
                 if (getDataMimeType() != null) {
-                    frameworkMessage.setData(getDataMimeType(), getDataUri());
+                    Api24Impl.setData(frameworkMessage, getDataMimeType(), getDataUri());
                 }
                 return frameworkMessage;
             }
+
+            /**
+             * A class for wrapping calls to {@link Notification.MessagingStyle.Message} methods
+             * which were added in API 24; these calls must be wrapped to avoid performance issues.
+             * See the UnsafeNewApiCall lint rule for more details.
+             */
+            @RequiresApi(24)
+            static class Api24Impl {
+                private Api24Impl() {
+                    // This class is not instantiable.
+                }
+
+                @DoNotInline
+                static Notification.MessagingStyle.Message createMessage(CharSequence text,
+                        long timestamp, CharSequence sender) {
+                    return new Notification.MessagingStyle.Message(text, timestamp, sender);
+                }
+
+                @DoNotInline
+                static Notification.MessagingStyle.Message setData(
+                        Notification.MessagingStyle.Message message, String dataMimeType,
+                        Uri dataUri) {
+                    return message.setData(dataMimeType, dataUri);
+                }
+            }
+
+            /**
+             * A class for wrapping calls to {@link Notification.MessagingStyle.Message} methods
+             * which were added in API 28; these calls must be wrapped to avoid performance issues.
+             * See the UnsafeNewApiCall lint rule for more details.
+             */
+            @RequiresApi(28)
+            static class Api28Impl {
+                private Api28Impl() {
+                    // This class is not instantiable.
+                }
+
+                @DoNotInline
+                static Notification.MessagingStyle.Message createMessage(CharSequence text,
+                        long timestamp, android.app.Person sender) {
+                    return new Notification.MessagingStyle.Message(text, timestamp, sender);
+                }
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.MessagingStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() { }
+
+            @DoNotInline
+            static void setBuilder(Notification.Style style, Notification.Builder builder) {
+                style.setBuilder(builder);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle createBigTextStyle(Notification.Builder builder) {
+                return new Notification.BigTextStyle(builder);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle setBigContentTitle(
+                    Notification.BigTextStyle bigTextStyle, CharSequence title) {
+                return bigTextStyle.setBigContentTitle(title);
+            }
+
+            @DoNotInline
+            static Notification.BigTextStyle bigText(
+                    Notification.BigTextStyle bigTextStyle, CharSequence cs) {
+                return bigTextStyle.bigText(cs);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.MessagingStyle} methods which
+         * were added in API 24; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(24)
+        static class Api24Impl {
+            private Api24Impl() { }
+
+            @DoNotInline
+            static Notification.MessagingStyle createMessagingStyle(CharSequence userDisplayName) {
+                return new Notification.MessagingStyle(userDisplayName);
+            }
+
+            @DoNotInline
+            static Notification.MessagingStyle addMessage(
+                    Notification.MessagingStyle messagingStyle,
+                    Notification.MessagingStyle.Message message) {
+                return messagingStyle.addMessage(message);
+            }
+
+            @DoNotInline
+            static Notification.MessagingStyle setConversationTitle(
+                    Notification.MessagingStyle messagingStyle, CharSequence conversationTitle) {
+                return messagingStyle.setConversationTitle(conversationTitle);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.MessagingStyle} methods which
+         * were added in API 26; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(26)
+        static class Api26Impl {
+            private Api26Impl() { }
+
+            @DoNotInline
+            static Notification.MessagingStyle addHistoricMessage(
+                    Notification.MessagingStyle messagingStyle,
+                    Notification.MessagingStyle.Message message) {
+                return messagingStyle.addHistoricMessage(message);
+            }
+
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.MessagingStyle} methods which
+         * were added in API 28; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(28)
+        static class Api28Impl {
+            private Api28Impl() { }
+
+            @DoNotInline
+            static Notification.MessagingStyle createMessagingStyle(android.app.Person user) {
+                return new Notification.MessagingStyle(user);
+            }
+
+            @DoNotInline
+            static Notification.MessagingStyle setGroupConversation(
+                    Notification.MessagingStyle messagingStyle, boolean isGroupConversation) {
+                return messagingStyle.setGroupConversation(isGroupConversation);
+            }
         }
     }
 
@@ -4415,14 +4656,13 @@
         @Override
         public void apply(NotificationBuilderWithBuilderAccessor builder) {
             if (Build.VERSION.SDK_INT >= 16) {
-                Notification.InboxStyle style =
-                        new Notification.InboxStyle(builder.getBuilder())
-                                .setBigContentTitle(mBigContentTitle);
+                Notification.InboxStyle style = Api16Impl.createInboxStyle(builder.getBuilder());
+                style = Api16Impl.setBigContentTitle(style, mBigContentTitle);
                 if (mSummaryTextSet) {
-                    style.setSummaryText(mSummaryText);
+                    Api16Impl.setSummaryText(style, mSummaryText);
                 }
                 for (CharSequence text: mTexts) {
-                    style.addLine(text);
+                    Api16Impl.addLine(style, text);
                 }
             }
         }
@@ -4450,6 +4690,39 @@
             super.clearCompatExtraKeys(extras);
             extras.remove(EXTRA_TEXT_LINES);
         }
+
+        /**
+         * A class for wrapping calls to {@link Notification.InboxStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() { }
+
+            @DoNotInline
+            static Notification.InboxStyle createInboxStyle(Notification.Builder builder) {
+                return new Notification.InboxStyle(builder);
+            }
+
+            @DoNotInline
+            static Notification.InboxStyle setBigContentTitle(Notification.InboxStyle inboxStyle,
+                    CharSequence title) {
+                return inboxStyle.setBigContentTitle(title);
+            }
+
+            @DoNotInline
+            static Notification.InboxStyle setSummaryText(Notification.InboxStyle inboxStyle,
+                    CharSequence cs) {
+                return inboxStyle.setSummaryText(cs);
+            }
+
+            @DoNotInline
+            static Notification.InboxStyle addLine(Notification.InboxStyle inboxStyle,
+                    CharSequence cs) {
+                return inboxStyle.addLine(cs);
+            }
+        }
     }
 
     /**
@@ -4459,13 +4732,13 @@
      * style and still obtain system decorations like the notification header with the expand
      * affordance and actions.
      *
-     * <p>Use {@link NotificationCompat.Builder#setCustomContentView(RemoteViews)},
-     * {@link NotificationCompat.Builder#setCustomBigContentView(RemoteViews)} and
-     * {@link NotificationCompat.Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
+     * <p>Use {@link Builder#setCustomContentView(RemoteViews)},
+     * {@link Builder#setCustomBigContentView(RemoteViews)} and
+     * {@link Builder#setCustomHeadsUpContentView(RemoteViews)} to set the
      * corresponding custom views to display.
      *
      * <p>To use this style with your Notification, feed it to
-     * {@link NotificationCompat.Builder#setStyle(Style)} like so:
+     * {@link Builder#setStyle(Style)} like so:
      * <pre class="prettyprint">
      * Notification noti = new NotificationCompat.Builder()
      *     .setSmallIcon(R.drawable.ic_stat_player)
@@ -4476,8 +4749,8 @@
      * </pre>
      *
      * <p>If you are using this style, consider using the corresponding styles like
-     * {@link androidx.core.R.style#TextAppearance_Compat_Notification} or
-     * {@link androidx.core.R.style#TextAppearance_Compat_Notification_Title} in
+     * {@link R.style#TextAppearance_Compat_Notification} or
+     * {@link R.style#TextAppearance_Compat_Notification_Title} in
      * your custom views in order to get the correct styling on each platform version.
      */
     public static class DecoratedCustomViewStyle extends Style {
@@ -4516,7 +4789,9 @@
         @Override
         public void apply(NotificationBuilderWithBuilderAccessor builder) {
             if (Build.VERSION.SDK_INT >= 24) {
-                builder.getBuilder().setStyle(new Notification.DecoratedCustomViewStyle());
+                Api16Impl.setStyle(builder.getBuilder(),
+                        Api24Impl.createDecoratedCustomViewStyle());
+
             }
         }
 
@@ -4585,7 +4860,7 @@
 
             // In the UI contextual actions appear separately from the standard actions, so we
             // filter them out here.
-            List<NotificationCompat.Action> nonContextualActions =
+            List<Action> nonContextualActions =
                     getNonContextualActions(mBuilder.mActions);
 
             if (showActions && nonContextualActions != null) {
@@ -4606,11 +4881,11 @@
             return remoteViews;
         }
 
-        private static List<NotificationCompat.Action> getNonContextualActions(
-                List<NotificationCompat.Action> actions) {
+        private static List<Action> getNonContextualActions(
+                List<Action> actions) {
             if (actions == null) return null;
-            List<NotificationCompat.Action> nonContextualActions = new ArrayList<>();
-            for (NotificationCompat.Action action : actions) {
+            List<Action> nonContextualActions = new ArrayList<>();
+            for (Action action : actions) {
                 if (!action.isContextual()) {
                     nonContextualActions.add(action);
                 }
@@ -4618,7 +4893,7 @@
             return nonContextualActions;
         }
 
-        private RemoteViews generateActionButton(NotificationCompat.Action action) {
+        private RemoteViews generateActionButton(Action action) {
             final boolean tombstone = (action.actionIntent == null);
             RemoteViews button = new RemoteViews(mBuilder.mContext.getPackageName(),
                     tombstone ? R.layout.notification_action_tombstone
@@ -4634,10 +4909,63 @@
                 button.setOnClickPendingIntent(R.id.action_container, action.actionIntent);
             }
             if (Build.VERSION.SDK_INT >= 15) {
-                button.setContentDescription(R.id.action_container, action.title);
+                Api15Impl.setContentDescription(button, R.id.action_container, action.title);
             }
             return button;
         }
+
+        /**
+         * A class for wrapping calls to {@link Notification.DecoratedCustomViewStyle} methods which
+         * were added in API 15; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(15)
+        static class Api15Impl {
+            private Api15Impl() { }
+
+            @DoNotInline
+            static void setContentDescription(RemoteViews remoteViews, int viewId,
+                    CharSequence contentDescription) {
+                remoteViews.setContentDescription(viewId, contentDescription);
+            }
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.DecoratedCustomViewStyle} methods which
+         * were added in API 16; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         * Note that the runtime converts NewApi classes to Object during init, but only for
+         * initialized classes; if setStyle is passed style objects from newer API versions, if
+         * the type of those objects will be unknown, and a VerifyError will occur. To prevent
+         * this, we explicitly cast the provided style Object to Notification.Style.
+         */
+        @RequiresApi(16)
+        static class Api16Impl {
+            private Api16Impl() { }
+
+            @DoNotInline
+            static Notification.Builder setStyle(Notification.Builder builder,
+                    Object style) {
+                return builder.setStyle((Notification.Style) style);
+            }
+
+        }
+
+        /**
+         * A class for wrapping calls to {@link Notification.DecoratedCustomViewStyle} methods which
+         * were added in API 24; these calls must be wrapped to avoid performance issues.
+         * See the UnsafeNewApiCall lint rule for more details.
+         */
+        @RequiresApi(24)
+        static class Api24Impl {
+            private Api24Impl() { }
+
+            @DoNotInline
+            static Notification.DecoratedCustomViewStyle createDecoratedCustomViewStyle() {
+                return new Notification.DecoratedCustomViewStyle();
+            }
+
+        }
     }
 
     /**
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 3f42105..1b71ae1 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -49,10 +49,6 @@
 # Each directory's spelling must match the library's directory in
 # frameworks/support.
 readonly javaLibraryDirsThatDontUseDackka=(
-  "androidx/camera"
-  "androidx/car"
-  "androidx/concurrent"
-  "androidx/contentpager"
   "androidx/datastore"
   "androidx/documentfile"
   "androidx/draganddrop"
@@ -62,19 +58,9 @@
   "androidx/gridlayout"
   "androidx/heifwriter"
   "androidx/leanback"
-  "androidx/media"
-  "androidx/media2"
-  "androidx/mediarouter"
-  "androidx/profileinstaller"
-  "androidx/recommendation"
-  "androidx/recyclerview"
 )
 readonly kotlinLibraryDirsThatDontUseDackka=(
   "androidx/benchmark"
-  "androidx/camera"
-  "androidx/car"
-  "androidx/concurrent"
-  "androidx/contentpager"
   "androidx/datastore"
   "androidx/documentfile"
   "androidx/dynamicanimation"
@@ -85,12 +71,6 @@
   "androidx/gridlayout"
   "androidx/heifwriter"
   "androidx/leanback"
-  "androidx/media"
-  "androidx/media2"
-  "androidx/mediarouter"
-  "androidx/profileinstaller"
-  "androidx/recommendation"
-  "androidx/recyclerview"
 )
 
 # Change directory to this script's location and store the directory
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ProviderCallbackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ProviderCallbackTest.kt
index 5e750b6..32577fa 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/ProviderCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/ProviderCallbackTest.kt
@@ -78,6 +78,40 @@
         }
     }
 
+    @Test
+    fun onConfigurationChangedNestedFragmentsOnBackStack() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val parent = StrictViewFragment(R.layout.fragment_container_view)
+            val child = CallbackFragment()
+            val replacementChild = CallbackFragment()
+
+            withActivity {
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, parent)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                parent.childFragmentManager.beginTransaction()
+                    .replace(R.id.fragment_container_view, child)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, replacementChild)
+                    .setReorderingAllowed(true)
+                    .addToBackStack(null)
+                    .commit()
+                supportFragmentManager.executePendingTransactions()
+
+                val newConfig = Configuration(resources.configuration)
+                onConfigurationChanged(newConfig)
+            }
+
+            assertThat(child.configChangedCount).isEqualTo(0)
+            assertThat(replacementChild.configChangedCount).isEqualTo(1)
+        }
+    }
+
     @SdkSuppress(minSdkVersion = 26)
     @Test
     fun onMultiWindowModeChanged() {
@@ -121,6 +155,41 @@
     }
 
     @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun onMultiWindowModeChangedNestedFragmentsOnBackStack() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val parent = StrictViewFragment(R.layout.fragment_container_view)
+            val child = CallbackFragment()
+            val replacementChild = CallbackFragment()
+
+            withActivity {
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, parent)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                parent.childFragmentManager.beginTransaction()
+                    .replace(R.id.fragment_container_view, child)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, replacementChild)
+                    .setReorderingAllowed(true)
+                    .addToBackStack(null)
+                    .commit()
+                supportFragmentManager.executePendingTransactions()
+
+                val newConfig = Configuration(resources.configuration)
+                onMultiWindowModeChanged(true, newConfig)
+            }
+
+            assertThat(child.multiWindowChangedCount).isEqualTo(0)
+            assertThat(replacementChild.multiWindowChangedCount).isEqualTo(1)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
     @Suppress("DEPRECATION")
     @Test
     fun onPictureInPictureModeChanged() {
@@ -163,6 +232,41 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun onPictureInPictureModeChangedNestedFragmentsOnBackStack() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val parent = StrictViewFragment(R.layout.fragment_container_view)
+            val child = CallbackFragment()
+            val replacementChild = CallbackFragment()
+
+            withActivity {
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, parent)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                parent.childFragmentManager.beginTransaction()
+                    .replace(R.id.fragment_container_view, child)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, replacementChild)
+                    .setReorderingAllowed(true)
+                    .addToBackStack(null)
+                    .commit()
+                supportFragmentManager.executePendingTransactions()
+
+                val newConfig = Configuration(resources.configuration)
+                onPictureInPictureModeChanged(true, newConfig)
+            }
+
+            assertThat(child.pictureModeChangedCount).isEqualTo(0)
+            assertThat(replacementChild.pictureModeChangedCount).isEqualTo(1)
+        }
+    }
+
     @Suppress("DEPRECATION")
     @Test
     fun onLowMemory() {
@@ -201,6 +305,39 @@
             assertThat(child.onLowMemoryCount).isEqualTo(1)
         }
     }
+
+    @Test
+    fun onLowMemoryNestedFragmentsOnBackStack() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val parent = StrictViewFragment(R.layout.fragment_container_view)
+            val child = CallbackFragment()
+            val replacementChild = CallbackFragment()
+
+            withActivity {
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, parent)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                parent.childFragmentManager.beginTransaction()
+                    .replace(R.id.fragment_container_view, child)
+                    .setReorderingAllowed(true)
+                    .commitNow()
+
+                supportFragmentManager.beginTransaction()
+                    .replace(R.id.content, replacementChild)
+                    .setReorderingAllowed(true)
+                    .addToBackStack(null)
+                    .commit()
+                supportFragmentManager.executePendingTransactions()
+
+                onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE)
+            }
+
+            assertThat(child.onLowMemoryCount).isEqualTo(0)
+            assertThat(replacementChild.onLowMemoryCount).isEqualTo(1)
+        }
+    }
 }
 
 class CallbackFragment : StrictViewFragment() {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 6d51810..f3ced3e 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -447,18 +447,27 @@
             new CopyOnWriteArrayList<>();
 
     private final Consumer<Configuration> mOnConfigurationChangedListener = newConfig -> {
-        dispatchConfigurationChanged(newConfig);
+        if (isParentAdded()) {
+            dispatchConfigurationChanged(newConfig);
+        }
     };
     private final Consumer<Integer> mOnTrimMemoryListener = level -> {
-        if (level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
+        if (isParentAdded() && level == ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
             dispatchLowMemory();
         }
     };
     private final Consumer<MultiWindowModeChangedInfo> mOnMultiWindowModeChangedListener =
-            info -> dispatchMultiWindowModeChanged(info.isInMultiWindowMode());
+            info -> {
+                if (isParentAdded()) {
+                    dispatchMultiWindowModeChanged(info.isInMultiWindowMode());
+                }
+            };
     private final Consumer<PictureInPictureModeChangedInfo>
-            mOnPictureInPictureModeChangedListener = info -> dispatchPictureInPictureModeChanged(
-                    info.isInPictureInPictureMode());
+            mOnPictureInPictureModeChangedListener = info -> {
+                if (isParentAdded()) {
+                    dispatchPictureInPictureModeChanged(info.isInPictureInPictureMode());
+                }
+            };
 
     private final MenuProvider mMenuProvider = new MenuProvider() {
         @Override
@@ -3316,6 +3325,14 @@
         }
     }
 
+    private boolean isParentAdded() {
+        // The root fragment manager is always considered added
+        if (mParent == null) {
+            return true;
+        }
+        return mParent.isAdded() && mParent.getParentFragmentManager().isParentAdded();
+    }
+
     static int reverseTransit(int transit) {
         int rev = 0;
         switch (transit) {
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
index e296260..6e46615 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/AndroidManifest.xml
@@ -96,20 +96,6 @@
         </receiver>
 
         <receiver
-            android:name="androidx.glance.appwidget.demos.SingleEntityWidgetReceiver"
-            android:enabled="@bool/glance_appwidget_available"
-            android:exported="false"
-            android:label="@string/single_entity_widget_name">
-            <intent-filter>
-                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
-                <action android:name="android.intent.action.LOCALE_CHANGED" />
-            </intent-filter>
-            <meta-data
-                android:name="android.appwidget.provider"
-                android:resource="@xml/default_app_widget_info" />
-        </receiver>
-
-        <receiver
             android:name="androidx.glance.appwidget.demos.ActionAppWidgetReceiver"
             android:enabled="@bool/glance_appwidget_available"
             android:exported="false"
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
deleted file mode 100644
index ede3b8a..0000000
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/SingleEntityWidget.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.glance.appwidget.demos
-
-import android.content.Context
-import androidx.compose.runtime.Composable
-import androidx.datastore.preferences.core.Preferences
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.glance.GlanceId
-import androidx.glance.ImageProvider
-import androidx.glance.action.ActionParameters
-import androidx.glance.appwidget.GlanceAppWidget
-import androidx.glance.appwidget.GlanceAppWidgetReceiver
-import androidx.glance.appwidget.SizeMode
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
-import androidx.glance.appwidget.state.updateAppWidgetState
-import androidx.glance.appwidget.template.GlanceTemplateAppWidget
-import androidx.glance.appwidget.template.SingleEntityTemplate
-import androidx.glance.currentState
-import androidx.glance.template.SingleEntityTemplateData
-import androidx.glance.template.TemplateImageWithDescription
-import androidx.glance.template.TemplateText
-import androidx.glance.template.TemplateTextButton
-import androidx.glance.template.TextType
-
-private val PressedKey = booleanPreferencesKey("pressedKey")
-
-class ButtonAction : ActionCallback {
-    override suspend fun onAction(
-        context: Context,
-        glanceId: GlanceId,
-        parameters: ActionParameters
-    ) {
-        // Toggle the "pressed" state
-        updateAppWidgetState(context, glanceId) { state ->
-            state[PressedKey] = state[PressedKey] != true
-        }
-        SingleEntityWidget().update(context, glanceId)
-    }
-}
-
-class SingleEntityWidget : GlanceTemplateAppWidget() {
-    override val sizeMode = SizeMode.Exact
-
-    @Composable
-    override fun TemplateContent() {
-        SingleEntityTemplate(
-            createData(getHeader(currentState<Preferences>()[PressedKey] == true))
-        )
-    }
-}
-
-class SingleEntityWidgetReceiver : GlanceAppWidgetReceiver() {
-    override val glanceAppWidget: GlanceAppWidget = SingleEntityWidget()
-}
-
-private fun createData(title: String) = SingleEntityTemplateData(
-    header = TemplateText("Single Entity demo", TextType.Title),
-    headerIcon = TemplateImageWithDescription(
-        ImageProvider(R.drawable.compose),
-        "Header icon"
-    ),
-    text1 = TemplateText(title, TextType.Title),
-    text2 = TemplateText("Subtitle test", TextType.Title),
-    button = TemplateTextButton(actionRunCallback<ButtonAction>(), "toggle"),
-    image = TemplateImageWithDescription(
-        ImageProvider(R.drawable.compose),
-        "Compose image"
-    )
-)
-
-private fun getHeader(pressed: Boolean) = if (pressed) "header2" else "header1"
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
index e7d93f4..bc22d16 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/DemoOverrideWidget.kt
@@ -29,11 +29,14 @@
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Column
 import androidx.glance.layout.fillMaxSize
+import androidx.glance.template.HeaderBlock
+import androidx.glance.template.ImageBlock
 import androidx.glance.template.LocalTemplateMode
 import androidx.glance.template.SingleEntityTemplateData
 import androidx.glance.template.TemplateImageWithDescription
 import androidx.glance.template.TemplateMode
 import androidx.glance.template.TemplateText
+import androidx.glance.template.TextBlock
 import androidx.glance.template.TextType
 import androidx.glance.text.Text
 import androidx.glance.text.TextAlign
@@ -53,18 +56,28 @@
         } else {
             SingleEntityTemplate(
                 SingleEntityTemplateData(
-                    header = TemplateText("Single Entity Demo", TextType.Title),
-                    headerIcon = TemplateImageWithDescription(
-                        ImageProvider(R.drawable.compose),
-                        "icon"
+                    headerBlock = HeaderBlock(
+                        text = TemplateText("Single Entity Demo", TextType.Title),
+                        icon = TemplateImageWithDescription(
+                            ImageProvider(R.drawable.compose),
+                            "icon"
+                        ),
                     ),
-                    text1 = TemplateText("title", TextType.Title),
-                    text2 = TemplateText("Subtitle", TextType.Label),
-                    text3 = TemplateText(
-                        "Body Lorem ipsum dolor sit amet, consectetur adipiscing",
-                        TextType.Label
+                    textBlock = TextBlock(
+                        text1 = TemplateText("title", TextType.Title),
+                        text2 = TemplateText("Subtitle", TextType.Label),
+                        text3 = TemplateText(
+                            "Body Lorem ipsum dolor sit amet, consectetur adipiscing",
+                            TextType.Label
+                        ),
+                        priority = 0,
                     ),
-                    image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image")
+                    imageBlock = ImageBlock(
+                        images = listOf(
+                            TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image")
+                        ),
+                        priority = 1,
+                    ),
                 )
             )
         }
diff --git a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
index aad35b6..ed80d62 100644
--- a/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
+++ b/glance/glance-appwidget/integration-tests/template-demos/src/main/java/androidx/glance/appwidget/template/demos/SingleEntityDemoWidget.kt
@@ -32,10 +32,14 @@
 import androidx.glance.appwidget.template.GlanceTemplateAppWidget
 import androidx.glance.appwidget.template.SingleEntityTemplate
 import androidx.glance.currentState
+import androidx.glance.template.ActionBlock
+import androidx.glance.template.HeaderBlock
+import androidx.glance.template.ImageBlock
 import androidx.glance.template.SingleEntityTemplateData
 import androidx.glance.template.TemplateImageWithDescription
 import androidx.glance.template.TemplateText
 import androidx.glance.template.TemplateTextButton
+import androidx.glance.template.TextBlock
 import androidx.glance.template.TextType
 
 /**
@@ -48,27 +52,41 @@
     override fun TemplateContent() {
         SingleEntityTemplate(
             SingleEntityTemplateData(
-                header = TemplateText("Single Entity Demo", TextType.Title),
-                headerIcon = TemplateImageWithDescription(
-                    ImageProvider(R.drawable.compose),
-                    "Header icon"
+                headerBlock = HeaderBlock(
+                    text = TemplateText("Single Entity Demo", TextType.Title),
+                    icon = TemplateImageWithDescription(
+                        ImageProvider(R.drawable.compose),
+                        "Header icon"
+                    ),
                 ),
-                text1 = TemplateText(
-                    getTitle(currentState<Preferences>()[ToggleKey] == true), TextType.Title
+                textBlock = TextBlock(
+                    text1 = TemplateText(
+                        getTitle(currentState<Preferences>()[ToggleKey] == true), TextType.Title
+                    ),
+                    text2 = TemplateText("Subtitle", TextType.Label),
+                    text3 = TemplateText(
+                        "Body Lorem ipsum dolor sit amet, consectetur adipiscing elit",
+                        TextType.Body
+                    ),
+                    priority = 0,
                 ),
-                text2 = TemplateText("Subtitle", TextType.Label),
-                text3 = TemplateText(
-                    "Body Lorem ipsum dolor sit amet, consectetur adipiscing elit",
-                    TextType.Body
+                imageBlock = ImageBlock(
+                    images = listOf(
+                        TemplateImageWithDescription(
+                            ImageProvider(R.drawable.compose),
+                            "Compose image"
+                        )
+                    ),
+                    priority = 1,
                 ),
-                button = TemplateTextButton(
-                    actionRunCallback<SEButtonAction>(),
-                    "Toggle title"
+                actionBlock = ActionBlock(
+                    actionButtons = listOf(
+                        TemplateTextButton(
+                            actionRunCallback<SEButtonAction>(),
+                            "Toggle title"
+                        ),
+                    ),
                 ),
-                image = TemplateImageWithDescription(
-                    ImageProvider(R.drawable.compose),
-                    "Compose image"
-                )
             )
         )
     }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
index 27cc583..8dd4b4e 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/SingleEntityTemplateLayouts.kt
@@ -19,8 +19,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.unit.dp
 import androidx.glance.GlanceModifier
-import androidx.glance.Image
 import androidx.glance.LocalSize
+import androidx.glance.appwidget.cornerRadius
 import androidx.glance.background
 import androidx.glance.layout.Column
 import androidx.glance.layout.ContentScale
@@ -57,81 +57,60 @@
 
 @Composable
 private fun WidgetLayoutCollapsed(data: SingleEntityTemplateData) {
-    var modifier = GlanceModifier
-        .fillMaxSize().padding(16.dp).background(LocalTemplateColors.current.surface)
-
-    data.image?.let { image ->
-        modifier = modifier.background(image.image, ContentScale.Crop)
-    }
-    Column(modifier = modifier) {
-        data.headerIcon?.let { AppWidgetTemplateHeader(it, data.header) }
+    Column(modifier = createTopLevelModifier(data, true)) {
+        HeaderBlockTemplate(data.headerBlock)
         Spacer(modifier = GlanceModifier.defaultWeight())
-        AppWidgetTextSection(textList(data.text1, data.text2))
+        data.textBlock?.let { AppWidgetTextSection(textList(it.text1, it.text2)) }
     }
 }
 
 @Composable
 private fun WidgetLayoutVertical(data: SingleEntityTemplateData) {
 
-    Column(modifier = GlanceModifier
-        .fillMaxSize()
-        .padding(16.dp)
-        .background(LocalTemplateColors.current.surface)) {
-        data.headerIcon?.let { AppWidgetTemplateHeader(it, data.header) }
-        Spacer(modifier = GlanceModifier.height(16.dp))
-        data.image?.let { image ->
-            Image(
-                provider = image.image,
-                contentDescription = image.description,
-                modifier = GlanceModifier.fillMaxWidth().defaultWeight(),
-                contentScale = ContentScale.Crop
-            )
+    Column(modifier = createTopLevelModifier(data)) {
+        data.headerBlock?.let {
+            HeaderBlockTemplate(data.headerBlock)
+            Spacer(modifier = GlanceModifier.height(16.dp))
+        }
+
+        data.imageBlock?.let {
+            SingleImageBlockTemplate(it, GlanceModifier.fillMaxWidth().defaultWeight())
             Spacer(modifier = GlanceModifier.height(16.dp))
         }
         Row(modifier = GlanceModifier.fillMaxWidth()) {
-            AppWidgetTextSection(textList(data.text1, data.text2))
+            data.textBlock?.let { AppWidgetTextSection(textList(it.text1, it.text2)) }
             Spacer(modifier = GlanceModifier.defaultWeight())
-            data.button?.let { button -> AppWidgetTemplateButton(button) }
+            data.actionBlock?.let { ActionBlockTemplate(it) }
         }
     }
 }
 
 @Composable
 private fun WidgetLayoutHorizontal(data: SingleEntityTemplateData) {
-    Row(modifier = GlanceModifier
-        .fillMaxSize()
-        .padding(16.dp)
-        .background(LocalTemplateColors.current.surface)) {
-
+    Row(modifier = createTopLevelModifier(data)) {
         Column(
-            modifier =
-            GlanceModifier.defaultWeight().fillMaxHeight()
+            modifier = GlanceModifier.defaultWeight().fillMaxHeight()
         ) {
-            data.headerIcon?.let { AppWidgetTemplateHeader(it, data.header) }
-            Spacer(modifier = GlanceModifier.height(16.dp))
+            data.headerBlock?.let {
+                HeaderBlockTemplate(data.headerBlock)
+                Spacer(modifier = GlanceModifier.height(16.dp))
+            }
             Spacer(modifier = GlanceModifier.defaultWeight())
 
             // TODO: Extract small height as template constant
-            val body =
-                if (LocalSize.current.height > 240.dp) {
-                    data.text3
-                } else {
-                    null
-                }
-            AppWidgetTextSection(textList(data.text1, data.text2, body))
-            data.button?.let { button ->
+            data.textBlock?.let {
+                val body = if (LocalSize.current.height > 240.dp) it.text3 else null
+                AppWidgetTextSection(textList(it.text1, it.text2, body))
+            }
+            data.actionBlock?.let {
                 Spacer(modifier = GlanceModifier.height(16.dp))
-                AppWidgetTemplateButton(button)
+                ActionBlockTemplate(it)
             }
         }
-        data.image?.let { image ->
+
+        data.imageBlock?.let {
             Spacer(modifier = GlanceModifier.width(16.dp))
-            Image(
-                provider = image.image,
-                contentDescription = image.description,
-                modifier = GlanceModifier.fillMaxHeight().defaultWeight(),
-                contentScale = ContentScale.Crop
-            )
+            SingleImageBlockTemplate(it, GlanceModifier.fillMaxHeight().defaultWeight())
         }
     }
 }
@@ -148,3 +127,19 @@
 
     return result
 }
+
+@Composable
+private fun createTopLevelModifier(
+    data: SingleEntityTemplateData,
+    isImmersive: Boolean = false
+): GlanceModifier {
+    var modifier = GlanceModifier
+        .fillMaxSize().padding(16.dp).cornerRadius(16.dp)
+        .background(LocalTemplateColors.current.primaryContainer)
+    if (isImmersive && data.imageBlock?.images?.isNotEmpty() == true) {
+        val mainImage = data.imageBlock!!.images[0]
+        modifier = modifier.background(mainImage.image, ContentScale.Crop)
+    }
+
+    return modifier
+}
diff --git a/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt b/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
index 7a85f81..7b45c92 100644
--- a/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
+++ b/glance/glance-wear-tiles/integration-tests/template-demos/src/main/java/androidx/glance/wear/tiles/template/demos/DemoTile.kt
@@ -18,9 +18,12 @@
 
 import androidx.compose.runtime.Composable
 import androidx.glance.ImageProvider
+import androidx.glance.template.HeaderBlock
+import androidx.glance.template.ImageBlock
 import androidx.glance.template.SingleEntityTemplateData
 import androidx.glance.template.TemplateImageWithDescription
 import androidx.glance.template.TemplateText
+import androidx.glance.template.TextBlock
 import androidx.glance.template.TextType
 import androidx.glance.wear.tiles.GlanceTileService
 import androidx.glance.wear.tiles.template.SingleEntityTemplate
@@ -32,15 +35,25 @@
     override fun Content() {
         SingleEntityTemplate(
             SingleEntityTemplateData(
-                header = TemplateText("Single Entity Demo", TextType.Title),
-                headerIcon = TemplateImageWithDescription(
-                    ImageProvider(R.drawable.compose),
-                    "image"
+                headerBlock = HeaderBlock(
+                    text = TemplateText("Single Entity Demo", TextType.Title),
+                    icon = TemplateImageWithDescription(
+                        ImageProvider(R.drawable.compose),
+                        "image"
+                    ),
                 ),
-                text1 = TemplateText("Title", TextType.Title),
-                text2 = TemplateText("Subtitle", TextType.Label),
-                text3 = TemplateText("Here's the body", TextType.Body),
-                image = TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image"),
+                textBlock = TextBlock(
+                    text1 = TemplateText("Title", TextType.Title),
+                    text2 = TemplateText("Subtitle", TextType.Label),
+                    text3 = TemplateText("Here's the body", TextType.Body),
+                    priority = 0,
+                ),
+                imageBlock = ImageBlock(
+                    images = listOf(
+                        TemplateImageWithDescription(ImageProvider(R.drawable.compose), "image")
+                    ),
+                    priority = 1,
+                ),
             )
         )
     }
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
index 10d131d..8d5016b 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/template/SingleEntityTemplateLayouts.kt
@@ -71,10 +71,16 @@
                 .padding(horizontal = 16.dp, vertical = 8.dp),
             horizontalAlignment = Alignment.CenterHorizontally
         ) {
-            data.headerIcon?.let { TemplateHeader(it) }
+            data.headerBlock?.icon?.let { TemplateHeader(it) }
             Spacer(modifier = GlanceModifier.height(4.dp))
-            TextSection(textList(data.text1, data.text2, data.text3))
-            data.image?.let {
+            TextSection(
+                textList(
+                    data.textBlock?.text1,
+                    data.textBlock?.text2,
+                    data.textBlock?.text3
+                )
+            )
+            data.imageBlock?.images?.firstOrNull()?.let {
                 Spacer(modifier = GlanceModifier.height(4.dp))
                 Image(
                     it.image,
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index 2c9aae5..5218a74 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -552,23 +552,17 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.TemplateImageWithDescription? headerIcon, optional androidx.glance.template.TemplateText? header, optional androidx.glance.template.TemplateText? text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional androidx.glance.template.TemplateButton? button, optional androidx.glance.template.TemplateImageWithDescription? image);
-    method public androidx.glance.template.TemplateButton? getButton();
+    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    method public androidx.glance.template.ActionBlock? getActionBlock();
     method public boolean getDisplayHeader();
-    method public androidx.glance.template.TemplateText? getHeader();
-    method public androidx.glance.template.TemplateImageWithDescription? getHeaderIcon();
-    method public androidx.glance.template.TemplateImageWithDescription? getImage();
-    method public androidx.glance.template.TemplateText? getText1();
-    method public androidx.glance.template.TemplateText? getText2();
-    method public androidx.glance.template.TemplateText? getText3();
-    property public final androidx.glance.template.TemplateButton? button;
+    method public androidx.glance.template.HeaderBlock? getHeaderBlock();
+    method public androidx.glance.template.ImageBlock? getImageBlock();
+    method public androidx.glance.template.TextBlock? getTextBlock();
+    property public final androidx.glance.template.ActionBlock? actionBlock;
     property public final boolean displayHeader;
-    property public final androidx.glance.template.TemplateText? header;
-    property public final androidx.glance.template.TemplateImageWithDescription? headerIcon;
-    property public final androidx.glance.template.TemplateImageWithDescription? image;
-    property public final androidx.glance.template.TemplateText? text1;
-    property public final androidx.glance.template.TemplateText? text2;
-    property public final androidx.glance.template.TemplateText? text3;
+    property public final androidx.glance.template.HeaderBlock? headerBlock;
+    property public final androidx.glance.template.ImageBlock? imageBlock;
+    property public final androidx.glance.template.TextBlock? textBlock;
   }
 
   public abstract sealed class TemplateButton {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index 2c9aae5..5218a74 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -552,23 +552,17 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.TemplateImageWithDescription? headerIcon, optional androidx.glance.template.TemplateText? header, optional androidx.glance.template.TemplateText? text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional androidx.glance.template.TemplateButton? button, optional androidx.glance.template.TemplateImageWithDescription? image);
-    method public androidx.glance.template.TemplateButton? getButton();
+    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    method public androidx.glance.template.ActionBlock? getActionBlock();
     method public boolean getDisplayHeader();
-    method public androidx.glance.template.TemplateText? getHeader();
-    method public androidx.glance.template.TemplateImageWithDescription? getHeaderIcon();
-    method public androidx.glance.template.TemplateImageWithDescription? getImage();
-    method public androidx.glance.template.TemplateText? getText1();
-    method public androidx.glance.template.TemplateText? getText2();
-    method public androidx.glance.template.TemplateText? getText3();
-    property public final androidx.glance.template.TemplateButton? button;
+    method public androidx.glance.template.HeaderBlock? getHeaderBlock();
+    method public androidx.glance.template.ImageBlock? getImageBlock();
+    method public androidx.glance.template.TextBlock? getTextBlock();
+    property public final androidx.glance.template.ActionBlock? actionBlock;
     property public final boolean displayHeader;
-    property public final androidx.glance.template.TemplateText? header;
-    property public final androidx.glance.template.TemplateImageWithDescription? headerIcon;
-    property public final androidx.glance.template.TemplateImageWithDescription? image;
-    property public final androidx.glance.template.TemplateText? text1;
-    property public final androidx.glance.template.TemplateText? text2;
-    property public final androidx.glance.template.TemplateText? text3;
+    property public final androidx.glance.template.HeaderBlock? headerBlock;
+    property public final androidx.glance.template.ImageBlock? imageBlock;
+    property public final androidx.glance.template.TextBlock? textBlock;
   }
 
   public abstract sealed class TemplateButton {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index 2c9aae5..5218a74 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -552,23 +552,17 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.TemplateImageWithDescription? headerIcon, optional androidx.glance.template.TemplateText? header, optional androidx.glance.template.TemplateText? text1, optional androidx.glance.template.TemplateText? text2, optional androidx.glance.template.TemplateText? text3, optional androidx.glance.template.TemplateButton? button, optional androidx.glance.template.TemplateImageWithDescription? image);
-    method public androidx.glance.template.TemplateButton? getButton();
+    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    method public androidx.glance.template.ActionBlock? getActionBlock();
     method public boolean getDisplayHeader();
-    method public androidx.glance.template.TemplateText? getHeader();
-    method public androidx.glance.template.TemplateImageWithDescription? getHeaderIcon();
-    method public androidx.glance.template.TemplateImageWithDescription? getImage();
-    method public androidx.glance.template.TemplateText? getText1();
-    method public androidx.glance.template.TemplateText? getText2();
-    method public androidx.glance.template.TemplateText? getText3();
-    property public final androidx.glance.template.TemplateButton? button;
+    method public androidx.glance.template.HeaderBlock? getHeaderBlock();
+    method public androidx.glance.template.ImageBlock? getImageBlock();
+    method public androidx.glance.template.TextBlock? getTextBlock();
+    property public final androidx.glance.template.ActionBlock? actionBlock;
     property public final boolean displayHeader;
-    property public final androidx.glance.template.TemplateText? header;
-    property public final androidx.glance.template.TemplateImageWithDescription? headerIcon;
-    property public final androidx.glance.template.TemplateImageWithDescription? image;
-    property public final androidx.glance.template.TemplateText? text1;
-    property public final androidx.glance.template.TemplateText? text2;
-    property public final androidx.glance.template.TemplateText? text3;
+    property public final androidx.glance.template.HeaderBlock? headerBlock;
+    property public final androidx.glance.template.ImageBlock? imageBlock;
+    property public final androidx.glance.template.TextBlock? textBlock;
   }
 
   public abstract sealed class TemplateButton {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
index d068a97..c4a2b5f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
@@ -21,35 +21,26 @@
  * a header, text section with up to three text items, main image, and single action button.
  *
  * @param displayHeader True if the glanceable header should be displayed
- * @param headerIcon Header logo icon, image corner radius is ignored in default layouts
- * @param header Main header text
- * @param text1 Text section first text item
- * @param text2 Text section second text item
- * @param text3 Text section third text item
- * @param button Action button
- * @param image Main image content
+ * @param headerBlock The header block of the entity by [HeaderBlock].
+ * @param textBlock The text block for up to three types of texts for the entity.
+ * @param imageBlock The image block for the entity main image by [ImageBlock].
+ * @param actionBlock The entity single action button by [ActionBlock].
  */
 
 class SingleEntityTemplateData(
     val displayHeader: Boolean = true,
-    val headerIcon: TemplateImageWithDescription? = null,
-    val header: TemplateText? = null,
-    val text1: TemplateText? = null,
-    val text2: TemplateText? = null,
-    val text3: TemplateText? = null,
-    val button: TemplateButton? = null,
-    val image: TemplateImageWithDescription? = null
+    val headerBlock: HeaderBlock? = null,
+    val textBlock: TextBlock? = null,
+    val imageBlock: ImageBlock? = null,
+    val actionBlock: ActionBlock? = null,
 ) {
 
     override fun hashCode(): Int {
         var result = displayHeader.hashCode()
-        result = 31 * result + (headerIcon?.hashCode() ?: 0)
-        result = 31 * result + (header?.hashCode() ?: 0)
-        result = 31 * result + (text1?.hashCode() ?: 0)
-        result = 31 * result + (text2?.hashCode() ?: 0)
-        result = 31 * result + (text3?.hashCode() ?: 0)
-        result = 31 * result + (button?.hashCode() ?: 0)
-        result = 31 * result + (image?.hashCode() ?: 0)
+        result = 31 * result + (headerBlock?.hashCode() ?: 0)
+        result = 31 * result + (textBlock?.hashCode() ?: 0)
+        result = 31 * result + (imageBlock?.hashCode() ?: 0)
+        result = 31 * result + (actionBlock?.hashCode() ?: 0)
         return result
     }
 
@@ -60,13 +51,10 @@
         other as SingleEntityTemplateData
 
         if (displayHeader != other.displayHeader) return false
-        if (headerIcon != other.headerIcon) return false
-        if (header != other.header) return false
-        if (text1 != other.text1) return false
-        if (text2 != other.text2) return false
-        if (text3 != other.text3) return false
-        if (button != other.button) return false
-        if (image != other.image) return false
+        if (headerBlock != other.headerBlock) return false
+        if (textBlock != other.textBlock) return false
+        if (imageBlock != other.imageBlock) return false
+        if (actionBlock != other.actionBlock) return false
 
         return true
     }
diff --git a/health/connect/connect-client-proto/src/main/proto/request.proto b/health/connect/connect-client-proto/src/main/proto/request.proto
index 9ccbd99..ef46550 100644
--- a/health/connect/connect-client-proto/src/main/proto/request.proto
+++ b/health/connect/connect-client-proto/src/main/proto/request.proto
@@ -106,3 +106,12 @@
 message UnregisterFromDataNotificationsRequest {
   optional string notificationIntentAction = 1;
 }
+
+message UpsertExerciseRouteRequest {
+  optional string sessionUid = 1;
+  optional DataPoint exerciseRoute = 2;
+}
+
+message ReadExerciseRouteRequest {
+  optional string sessionUid = 1;
+}
diff --git a/health/connect/connect-client-proto/src/main/proto/response.proto b/health/connect/connect-client-proto/src/main/proto/response.proto
index 38082f0..e1787ee 100644
--- a/health/connect/connect-client-proto/src/main/proto/response.proto
+++ b/health/connect/connect-client-proto/src/main/proto/response.proto
@@ -40,6 +40,10 @@
   optional string page_token = 2;
 }
 
+message ReadExerciseRouteResponse {
+  optional DataPoint data_point = 1;
+}
+
 message AggregateDataResponse {
   repeated AggregateDataRow rows = 1;
 }
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/ReadExerciseRouteRequest.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/ReadExerciseRouteRequest.aidl
new file mode 100644
index 0000000..9a33b9b
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/ReadExerciseRouteRequest.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 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.health.platform.client.request;
+
+parcelable ReadExerciseRouteRequest;
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/UpsertExerciseRouteRequest.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/UpsertExerciseRouteRequest.aidl
new file mode 100644
index 0000000..249f48d
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/request/UpsertExerciseRouteRequest.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 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.health.platform.client.request;
+
+parcelable UpsertExerciseRouteRequest;
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/response/ReadExerciseRouteResponse.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/response/ReadExerciseRouteResponse.aidl
new file mode 100644
index 0000000..09adaeb
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/response/ReadExerciseRouteResponse.aidl
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 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.health.platform.client.response;
+
+parcelable ReadExerciseRouteResponse;
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
index 263585a..da62ba3 100644
--- a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
@@ -22,8 +22,10 @@
 import androidx.health.platform.client.request.GetChangesTokenRequest;
 import androidx.health.platform.client.request.GetChangesRequest;
 import androidx.health.platform.client.request.UpsertDataRequest;
+import androidx.health.platform.client.request.UpsertExerciseRouteRequest;
 import androidx.health.platform.client.request.ReadDataRequest;
 import androidx.health.platform.client.request.ReadDataRangeRequest;
+import androidx.health.platform.client.request.ReadExerciseRouteRequest;
 import androidx.health.platform.client.request.RegisterForDataNotificationsRequest;
 import androidx.health.platform.client.request.RequestContext;
 import androidx.health.platform.client.request.UnregisterFromDataNotificationsRequest;
@@ -34,8 +36,10 @@
 import androidx.health.platform.client.service.IDeleteDataRangeCallback;
 import androidx.health.platform.client.service.IReadDataRangeCallback;
 import androidx.health.platform.client.service.IUpdateDataCallback;
+import androidx.health.platform.client.service.IUpsertExerciseRouteCallback;
 import androidx.health.platform.client.service.IInsertDataCallback;
 import androidx.health.platform.client.service.IReadDataCallback;
+import androidx.health.platform.client.service.IReadExerciseRouteCallback;
 import androidx.health.platform.client.service.IRevokeAllPermissionsCallback;
 import androidx.health.platform.client.service.IAggregateDataCallback;
 import androidx.health.platform.client.service.IRegisterForDataNotificationsCallback;
@@ -46,11 +50,11 @@
    * API version of the AIDL interface. Should be incremented every time a new
    * method is added.
    */
-  const int CURRENT_API_VERSION = 2;
+  const int CURRENT_API_VERSION = 3;
 
   const int MIN_API_VERSION = 1;
 
-  // Next Id: 20
+  // Next Id: 22
 
   /**
    * Returns version of this AIDL interface.
@@ -85,4 +89,9 @@
   void registerForDataNotifications(in RequestContext context, in RegisterForDataNotificationsRequest request, in IRegisterForDataNotificationsCallback callback) = 18;
 
   void unregisterFromDataNotifications(in RequestContext context, in UnregisterFromDataNotificationsRequest request, in IUnregisterFromDataNotificationsCallback callback) = 19;
+
+  void upsertExerciseRoute(in RequestContext context, in UpsertExerciseRouteRequest request, in IUpsertExerciseRouteCallback callback) = 20;
+
+  void readExerciseRoute(in RequestContext context, in ReadExerciseRouteRequest request, in IReadExerciseRouteCallback callback) = 21;
+
 }
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IReadExerciseRouteCallback.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IReadExerciseRouteCallback.aidl
new file mode 100644
index 0000000..6971e617
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IReadExerciseRouteCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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.health.platform.client.service;
+
+import androidx.health.platform.client.error.ErrorStatus;
+import androidx.health.platform.client.response.ReadExerciseRouteResponse;
+
+oneway interface IReadExerciseRouteCallback {
+  void onSuccess(in ReadExerciseRouteResponse response) = 0;
+  void onError(in ErrorStatus status) = 1;
+}
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IUpsertExerciseRouteCallback.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IUpsertExerciseRouteCallback.aidl
new file mode 100644
index 0000000..0113ecd1
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IUpsertExerciseRouteCallback.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 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.health.platform.client.service;
+
+import androidx.health.platform.client.error.ErrorStatus;
+
+oneway interface IUpsertExerciseRouteCallback {
+  void onSuccess() = 0;
+  void onError(in ErrorStatus status) = 1;
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 5f8c4d4..7ce2be5 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -283,6 +283,8 @@
      * registered, either in the `AndroidManifest.xml` file or at runtime. The registered
      * `BroadcastReceiver` must have an [IntentFilter][android.content.IntentFilter] specified with
      * the same action as in [notificationIntentAction] argument.
+     * [DataNotification][androidx.health.connect.client.datanotification.DataNotification] can be
+     * used to extract data from the received [Intent].
      *
      * @param notificationIntentAction an action to be used for broadcast messages.
      * @param recordTypes specifies [Record] types of interest.
@@ -293,6 +295,7 @@
      * @throws IllegalStateException If service is not available.
      *
      * @see unregisterFromDataNotifications
+     * @see androidx.health.connect.client.datanotification.DataNotification
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
     suspend fun registerForDataNotifications(
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/datanotification/DataNotification.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/datanotification/DataNotification.kt
new file mode 100644
index 0000000..4e7119b
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/datanotification/DataNotification.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.health.connect.client.datanotification
+
+import android.content.Intent
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.impl.converters.datatype.getRecordType
+import androidx.health.connect.client.records.Record
+import kotlin.reflect.KClass
+
+/**
+ * Contains information about the changed data.
+ *
+ * @param dataTypes a set of changed [Record] classes.
+ * @see androidx.health.connect.client.HealthConnectClient.registerForDataNotifications
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+class DataNotification private constructor(
+    val dataTypes: Set<KClass<out Record>>,
+) {
+
+    companion object {
+        private const val EXTRA_DATA_TYPE_IDS = "com.google.android.healthdata.extra.DATA_TYPE_IDS"
+
+        /**
+         * Extracts the notification data from the given [intent]. The [Intent] is usually received
+         * via a [BroadcastReceiver][android.content.BroadcastReceiver].
+         *
+         * @param intent an [Intent] received in a
+         * [BroadcastReceiver][android.content.BroadcastReceiver].
+         * @return [DataNotification] if the notification data was successfully extracted, `null`
+         * otherwise.
+         * @see androidx.health.connect.client.HealthConnectClient.registerForDataNotifications
+         */
+        @JvmStatic
+        fun from(intent: Intent): DataNotification? {
+            val dataTypeIds = intent.getIntArrayExtra(EXTRA_DATA_TYPE_IDS) ?: return null
+
+            return DataNotification(
+                dataTypes = dataTypeIds.mapTo(HashSet(), ::getRecordType),
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordType.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordType.kt
new file mode 100644
index 0000000..1be6358
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordType.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.health.connect.client.impl.converters.datatype
+
+import androidx.health.connect.client.records.ActiveCaloriesBurnedRecord
+import androidx.health.connect.client.records.BasalBodyTemperatureRecord
+import androidx.health.connect.client.records.BasalMetabolicRateRecord
+import androidx.health.connect.client.records.BloodGlucoseRecord
+import androidx.health.connect.client.records.BloodPressureRecord
+import androidx.health.connect.client.records.BodyFatRecord
+import androidx.health.connect.client.records.BodyTemperatureRecord
+import androidx.health.connect.client.records.BodyWaterMassRecord
+import androidx.health.connect.client.records.BoneMassRecord
+import androidx.health.connect.client.records.CervicalMucusRecord
+import androidx.health.connect.client.records.CyclingPedalingCadenceRecord
+import androidx.health.connect.client.records.DistanceRecord
+import androidx.health.connect.client.records.ElevationGainedRecord
+import androidx.health.connect.client.records.ExerciseEventRecord
+import androidx.health.connect.client.records.ExerciseLapRecord
+import androidx.health.connect.client.records.ExerciseRepetitionsRecord
+import androidx.health.connect.client.records.ExerciseSessionRecord
+import androidx.health.connect.client.records.FloorsClimbedRecord
+import androidx.health.connect.client.records.HeartRateRecord
+import androidx.health.connect.client.records.HeartRateVariabilityDifferentialIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySd2Record
+import androidx.health.connect.client.records.HeartRateVariabilitySdannRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnIndexRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdnnRecord
+import androidx.health.connect.client.records.HeartRateVariabilitySdsdRecord
+import androidx.health.connect.client.records.HeartRateVariabilityTinnRecord
+import androidx.health.connect.client.records.HeightRecord
+import androidx.health.connect.client.records.HipCircumferenceRecord
+import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.LeanBodyMassRecord
+import androidx.health.connect.client.records.MenstruationFlowRecord
+import androidx.health.connect.client.records.NutritionRecord
+import androidx.health.connect.client.records.OvulationTestRecord
+import androidx.health.connect.client.records.OxygenSaturationRecord
+import androidx.health.connect.client.records.PowerRecord
+import androidx.health.connect.client.records.Record
+import androidx.health.connect.client.records.RespiratoryRateRecord
+import androidx.health.connect.client.records.RestingHeartRateRecord
+import androidx.health.connect.client.records.SexualActivityRecord
+import androidx.health.connect.client.records.SleepSessionRecord
+import androidx.health.connect.client.records.SleepStageRecord
+import androidx.health.connect.client.records.SpeedRecord
+import androidx.health.connect.client.records.StepsCadenceRecord
+import androidx.health.connect.client.records.StepsRecord
+import androidx.health.connect.client.records.SwimmingStrokesRecord
+import androidx.health.connect.client.records.TotalCaloriesBurnedRecord
+import androidx.health.connect.client.records.Vo2MaxRecord
+import androidx.health.connect.client.records.WaistCircumferenceRecord
+import androidx.health.connect.client.records.WeightRecord
+import androidx.health.connect.client.records.WheelchairPushesRecord
+import kotlin.reflect.KClass
+
+internal val TYPE_ID_TO_RECORD_TYPE_MAP: Map<Int, KClass<out Record>> =
+    mapOf(
+        3 to ExerciseEventRecord::class,
+        55 to ExerciseLapRecord::class,
+        4 to ExerciseSessionRecord::class,
+        6 to DistanceRecord::class,
+        7 to ElevationGainedRecord::class,
+        8 to FloorsClimbedRecord::class,
+        9 to HydrationRecord::class,
+        10 to NutritionRecord::class,
+        11 to SleepStageRecord::class,
+        12 to SleepSessionRecord::class,
+        13 to StepsRecord::class,
+        14 to SwimmingStrokesRecord::class,
+        16 to BasalMetabolicRateRecord::class,
+        17 to BloodGlucoseRecord::class,
+        18 to BloodPressureRecord::class,
+        19 to BodyFatRecord::class,
+        20 to BodyTemperatureRecord::class,
+        21 to BoneMassRecord::class,
+        22 to CervicalMucusRecord::class,
+        28 to HeightRecord::class,
+        29 to HipCircumferenceRecord::class,
+        30 to HeartRateVariabilityDifferentialIndexRecord::class,
+        31 to HeartRateVariabilityRmssdRecord::class,
+        32 to HeartRateVariabilitySRecord::class,
+        33 to HeartRateVariabilitySd2Record::class,
+        34 to HeartRateVariabilitySdannRecord::class,
+        35 to HeartRateVariabilitySdnnIndexRecord::class,
+        36 to HeartRateVariabilitySdnnRecord::class,
+        37 to HeartRateVariabilitySdsdRecord::class,
+        38 to HeartRateVariabilityTinnRecord::class,
+        39 to LeanBodyMassRecord::class,
+        41 to MenstruationFlowRecord::class,
+        42 to OvulationTestRecord::class,
+        43 to OxygenSaturationRecord::class,
+        46 to RespiratoryRateRecord::class,
+        47 to RestingHeartRateRecord::class,
+        48 to SexualActivityRecord::class,
+        51 to Vo2MaxRecord::class,
+        52 to WaistCircumferenceRecord::class,
+        53 to WeightRecord::class,
+        54 to ExerciseRepetitionsRecord::class,
+        56 to HeartRateRecord::class,
+        58 to CyclingPedalingCadenceRecord::class,
+        60 to PowerRecord::class,
+        61 to SpeedRecord::class,
+        62 to StepsCadenceRecord::class,
+        63 to WheelchairPushesRecord::class,
+        64 to BodyWaterMassRecord::class,
+        65 to BasalBodyTemperatureRecord::class,
+        66 to TotalCaloriesBurnedRecord::class,
+        67 to ActiveCaloriesBurnedRecord::class,
+    )
+
+internal fun getRecordType(id: Int): KClass<out Record> =
+    requireNotNull(TYPE_ID_TO_RECORD_TYPE_MAP[id]) { "Unknown data type id: $id" }
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/ReadExerciseRouteRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/ReadExerciseRouteRequest.kt
new file mode 100644
index 0000000..2ea2f9b
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/ReadExerciseRouteRequest.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 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.health.platform.client.request
+
+import android.os.Parcelable
+import androidx.health.platform.client.impl.data.ProtoParcelable
+import androidx.health.platform.client.proto.RequestProto
+
+/**
+ * Internal parcelable for IPC calls.
+ *
+ * @suppress
+ */
+class ReadExerciseRouteRequest(override val proto: RequestProto.ReadExerciseRouteRequest) :
+  ProtoParcelable<RequestProto.ReadExerciseRouteRequest>() {
+
+  companion object {
+    @JvmField
+    val CREATOR: Parcelable.Creator<ReadExerciseRouteRequest> =
+      ProtoParcelable.newCreator {
+        val proto = RequestProto.ReadExerciseRouteRequest.parseFrom(it)
+        ReadExerciseRouteRequest(proto)
+      }
+  }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UpsertExerciseRouteRequest.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UpsertExerciseRouteRequest.kt
new file mode 100644
index 0000000..0e6031e
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/request/UpsertExerciseRouteRequest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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.health.platform.client.request
+
+import android.os.Parcelable
+import androidx.health.platform.client.impl.data.ProtoParcelable
+import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.proto.RequestProto
+
+/**
+ * Internal parcelable for IPC calls.
+ *
+ * @suppress
+ */
+class UpsertExerciseRouteRequest(val sessionUid: String, val route: DataProto.DataPoint) :
+  ProtoParcelable<RequestProto.UpsertExerciseRouteRequest>() {
+  override val proto: RequestProto.UpsertExerciseRouteRequest
+    get() {
+      val obj = this
+      return RequestProto.UpsertExerciseRouteRequest.newBuilder()
+        .setSessionUid(obj.sessionUid)
+        .setExerciseRoute(obj.route)
+        .build()
+    }
+
+  companion object {
+    @JvmField
+    val CREATOR: Parcelable.Creator<UpsertExerciseRouteRequest> =
+      ProtoParcelable.newCreator {
+        val proto = RequestProto.UpsertExerciseRouteRequest.parseFrom(it)
+        fromProto(proto)
+      }
+
+    internal fun fromProto(
+      proto: RequestProto.UpsertExerciseRouteRequest,
+    ): UpsertExerciseRouteRequest {
+      return UpsertExerciseRouteRequest(proto.sessionUid, proto.exerciseRoute)
+    }
+  }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/response/ReadExerciseRouteResponse.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/response/ReadExerciseRouteResponse.kt
new file mode 100644
index 0000000..106a5c5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/response/ReadExerciseRouteResponse.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 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.health.platform.client.response
+
+import android.os.Parcelable
+import androidx.health.platform.client.impl.data.ProtoParcelable
+import androidx.health.platform.client.proto.ResponseProto
+
+/** @suppress */
+class ReadExerciseRouteResponse(override val proto: ResponseProto.ReadExerciseRouteResponse) :
+    ProtoParcelable<ResponseProto.ReadExerciseRouteResponse>() {
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<ReadExerciseRouteResponse> =
+            ProtoParcelable.newCreator {
+                val proto = ResponseProto.ReadExerciseRouteResponse.parseFrom(it)
+                ReadExerciseRouteResponse(proto)
+            }
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
new file mode 100644
index 0000000..5ffb521
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/ClassFinder.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.health.connect.client
+
+import java.io.File
+import java.net.URL
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+import kotlin.reflect.KClass
+
+val RECORD_CLASSES: List<KClass<*>> by lazy {
+    findClasses("androidx.health.connect.client.records")
+        .filterNot { it.java.isInterface }
+        .filter { it.simpleName.orEmpty().endsWith("Record") }
+}
+
+fun findClasses(packageName: String): Set<KClass<*>> {
+    val resources =
+        requireNotNull(Thread.currentThread().contextClassLoader)
+            .getResources(packageName.replace('.', '/'))
+
+    return buildSet {
+        while (resources.hasMoreElements()) {
+            val classNames = findClasses(resources.nextElement().file, packageName)
+            for (className in classNames) {
+                add(Class.forName(className).kotlin)
+            }
+        }
+    }
+}
+
+private fun findClasses(directory: String, packageName: String): Set<String> =
+    buildSet {
+        if (directory.startsWith("file:") && ('!' in directory)) {
+            addAll(unzipClasses(path = directory, packageName = packageName))
+        }
+
+        for (file in File(directory).takeIf(File::exists)?.listFiles() ?: emptyArray()) {
+            if (file.isDirectory) {
+                addAll(findClasses(file.absolutePath, "$packageName.${file.name}"))
+            } else if (file.name.endsWith(".class")) {
+                add("$packageName.${file.name.dropLast(6)}")
+            }
+        }
+    }
+
+private fun unzipClasses(path: String, packageName: String): Set<String> =
+    ZipInputStream(URL(path.substringBefore('!')).openStream()).use { zip ->
+        buildSet {
+            while (true) {
+                val entry = zip.nextEntry ?: break
+                val className = entry.formatClassName()
+                if ((className != null) && className.startsWith(packageName)) {
+                    add(className)
+                }
+            }
+        }
+    }
+
+private fun ZipEntry.formatClassName(): String? =
+    name
+        .takeIf { it.endsWith(".class") }
+        ?.replace("[$].*".toRegex(), "")
+        ?.replace("[.]class".toRegex(), "")
+        ?.replace('/', '.')
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordTypeTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordTypeTest.kt
new file mode 100644
index 0000000..a319bc3
--- /dev/null
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/datatype/TypeIdToRecordTypeTest.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.impl.converters.datatype
+
+import androidx.health.connect.client.RECORD_CLASSES
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class TypeIdToRecordTypeTest {
+
+    @Test
+    fun containsAllRecordClasses() {
+        assertThat(TYPE_ID_TO_RECORD_TYPE_MAP.values).containsExactlyElementsIn(RECORD_CLASSES)
+    }
+}
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
index 3495d9e..d1361c5 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
@@ -26,15 +26,18 @@
 import androidx.health.platform.client.request.GetChangesTokenRequest
 import androidx.health.platform.client.request.ReadDataRangeRequest
 import androidx.health.platform.client.request.ReadDataRequest
+import androidx.health.platform.client.request.ReadExerciseRouteRequest
 import androidx.health.platform.client.request.RegisterForDataNotificationsRequest
 import androidx.health.platform.client.request.RequestContext
 import androidx.health.platform.client.request.UpsertDataRequest
+import androidx.health.platform.client.request.UpsertExerciseRouteRequest
 import androidx.health.platform.client.response.AggregateDataResponse
 import androidx.health.platform.client.response.GetChangesResponse
 import androidx.health.platform.client.response.GetChangesTokenResponse
 import androidx.health.platform.client.response.InsertDataResponse
 import androidx.health.platform.client.response.ReadDataRangeResponse
 import androidx.health.platform.client.response.ReadDataResponse
+import androidx.health.platform.client.response.ReadExerciseRouteResponse
 import androidx.health.platform.client.service.IAggregateDataCallback
 import androidx.health.platform.client.service.IDeleteDataCallback
 import androidx.health.platform.client.service.IDeleteDataRangeCallback
@@ -45,10 +48,12 @@
 import androidx.health.platform.client.service.IInsertDataCallback
 import androidx.health.platform.client.service.IReadDataCallback
 import androidx.health.platform.client.service.IReadDataRangeCallback
+import androidx.health.platform.client.service.IReadExerciseRouteCallback
 import androidx.health.platform.client.service.IRegisterForDataNotificationsCallback
 import androidx.health.platform.client.service.IRevokeAllPermissionsCallback
 import androidx.health.platform.client.service.IUnregisterFromDataNotificationsCallback
 import androidx.health.platform.client.service.IUpdateDataCallback
+import androidx.health.platform.client.service.IUpsertExerciseRouteCallback
 import androidx.health.platform.client.request.UnregisterFromDataNotificationsRequest
 
 /** Fake {@link IHealthDataService} implementation for unit testing. */
@@ -59,8 +64,10 @@
 
     /** State retaining last requested parameters. */
     var lastUpsertDataRequest: UpsertDataRequest? = null
+    var lastUpsertExerciseRouteRequest: UpsertExerciseRouteRequest? = null
     var lastReadDataRequest: ReadDataRequest? = null
     var lastReadDataRangeRequest: ReadDataRangeRequest? = null
+    var lastReadExerciseRouteRequest: ReadExerciseRouteRequest? = null
     var lastDeleteDataRequest: DeleteDataRequest? = null
     var lastDeleteDataRangeRequest: DeleteDataRangeRequest? = null
     var lastAggregateRequest: AggregateDataRequest? = null
@@ -73,6 +80,7 @@
     var insertDataResponse: InsertDataResponse? = null
     var readDataResponse: ReadDataResponse? = null
     var readDataRangeResponse: ReadDataRangeResponse? = null
+    var readExerciseRouteResponse: ReadExerciseRouteResponse? = null
     var aggregateDataResponse: AggregateDataResponse? = null
     var changesTokenResponse: GetChangesTokenResponse? = null
     var changesResponse: GetChangesResponse? = null
@@ -256,4 +264,30 @@
         }
         callback.onSuccess()
     }
+
+    override fun upsertExerciseRoute(
+        context: RequestContext,
+        request: UpsertExerciseRouteRequest,
+        callback: IUpsertExerciseRouteCallback,
+     ) {
+        lastUpsertExerciseRouteRequest = request
+        errorCode?.let {
+            callback.onError(ErrorStatus.create(it, "" + it))
+            return@upsertExerciseRoute
+        }
+        callback.onSuccess()
+    }
+
+    override fun readExerciseRoute(
+        context: RequestContext,
+        request: ReadExerciseRouteRequest,
+        callback: IReadExerciseRouteCallback,
+    ) {
+        lastReadExerciseRouteRequest = request
+        errorCode?.let {
+            callback.onError(ErrorStatus.create(it, "" + it))
+            return@readExerciseRoute
+        }
+        callback.onSuccess(checkNotNull(readExerciseRouteResponse))
+    }
 }
diff --git a/health/health-services-client/api/1.0.0-beta01.txt b/health/health-services-client/api/1.0.0-beta01.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/1.0.0-beta01.txt
+++ b/health/health-services-client/api/1.0.0-beta01.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/api/restricted_1.0.0-beta01.txt b/health/health-services-client/api/restricted_1.0.0-beta01.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta01.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta01.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index 9d7e99c..77426bf 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -292,12 +292,13 @@
   }
 
   public final class ExerciseConfig {
-    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
     property public final java.util.Set<androidx.health.services.client.data.DataType<?,?>> dataTypes;
@@ -306,7 +307,9 @@
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
+    property public final float swimmingPoolLengthMeters;
     field public static final androidx.health.services.client.data.ExerciseConfig.Companion Companion;
+    field public static final float SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f;
   }
 
   public static final class ExerciseConfig.Builder {
@@ -317,6 +320,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLength(float swimmingPoolLength);
   }
 
   public static final class ExerciseConfig.Companion {
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IVersionApiService.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IVersionApiService.aidl
index ef115d6..3541485 100644
--- a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IVersionApiService.aidl
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IVersionApiService.aidl
@@ -39,7 +39,7 @@
      * Version of the SDK as a whole. Should be incremented on each release,
      * regardless of whether the API surface has changed.
      */
-    const int CANONICAL_SDK_VERSION = 25;
+    const int CANONICAL_SDK_VERSION = 26;
 
     /**
      * Returns the version of _this_ AIDL interface.
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
index 11ce0c8..e5764ae 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
@@ -17,6 +17,7 @@
 package androidx.health.services.client.data
 
 import android.os.Bundle
+import androidx.annotation.FloatRange
 import androidx.health.services.client.ExerciseClient
 import androidx.health.services.client.proto.DataProto
 
@@ -36,6 +37,8 @@
  * [DataType.STEPS_TOTAL] / [DataType.STEPS].
  * @property exerciseParams [Bundle] bundle for specifying exercise presets, the values of an
  * on-going exercise which can be used to pre-populate a new exercise.
+ * @property swimmingPoolLengthMeters length (in meters) of the swimming pool, or 0 if not relevant to
+ * this exercise
  */
 @Suppress("ParcelCreator")
 class ExerciseConfig(
@@ -45,6 +48,7 @@
     val isGpsEnabled: Boolean,
     val exerciseGoals: List<ExerciseGoal<*>> = listOf(),
     val exerciseParams: Bundle = Bundle(),
+    @FloatRange(from = 0.0) val swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
 ) {
 
     internal constructor(
@@ -56,7 +60,12 @@
         proto.isAutoPauseAndResumeEnabled,
         proto.isGpsUsageEnabled,
         proto.exerciseGoalsList.map { ExerciseGoal.fromProto(it) },
-        BundlesUtil.fromProto(proto.exerciseParams)
+        BundlesUtil.fromProto(proto.exerciseParams),
+        if (proto.hasSwimmingPoolLength()) {
+          proto.swimmingPoolLength
+        } else {
+          SWIMMING_POOL_LENGTH_UNSPECIFIED
+        }
     )
 
     init {
@@ -66,7 +75,7 @@
         }
 
         if (exerciseType == ExerciseType.SWIMMING_POOL) {
-          require(exerciseParams.containsKey("swimming_pool_length_m")) {
+          require(swimmingPoolLengthMeters != 0.0f) {
               "If exercise type is SWIMMING_POOL, then swimming pool length must also be specified"
           }
         }
@@ -88,6 +97,7 @@
         private var isGpsEnabled: Boolean = false
         private var exerciseGoals: List<ExerciseGoal<*>> = emptyList()
         private var exerciseParams: Bundle = Bundle.EMPTY
+        private var swimmingPoolLength: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED
 
         /**
          * Sets the requested [DataType]s that should be tracked during this exercise. If not
@@ -159,6 +169,13 @@
             return this
         }
 
+        /** Sets the swimming pool length (in m). */
+        @Suppress("MissingGetterMatchingBuilder")
+        public fun setSwimmingPoolLength(swimmingPoolLength: Float): Builder {
+          this.swimmingPoolLength = swimmingPoolLength
+          return this
+        }
+
         /** Returns the built [ExerciseConfig]. */
         fun build(): ExerciseConfig {
             return ExerciseConfig(
@@ -167,7 +184,8 @@
                 isAutoPauseAndResumeEnabled,
                 isGpsEnabled,
                 exerciseGoals,
-                exerciseParams
+                exerciseParams,
+                swimmingPoolLength
             )
         }
     }
@@ -178,7 +196,8 @@
             "dataTypes=$dataTypes, " +
             "isAutoPauseAndResumeEnabled=$isAutoPauseAndResumeEnabled, " +
             "isGpsEnabled=$isGpsEnabled, " +
-            "exerciseGoals=$exerciseGoals)"
+            "exerciseGoals=$exerciseGoals, " +
+            "swimmingPoolLengthMeters=$swimmingPoolLengthMeters)"
 
     internal fun toProto(): DataProto.ExerciseConfig =
         DataProto.ExerciseConfig.newBuilder()
@@ -189,6 +208,7 @@
             .setIsGpsUsageEnabled(isGpsEnabled)
             .addAllExerciseGoals(exerciseGoals.map { it.proto })
             .setExerciseParams(BundlesUtil.toProto(exerciseParams))
+            .setSwimmingPoolLength(swimmingPoolLengthMeters)
             .build()
 
     companion object {
@@ -199,5 +219,7 @@
           */
         @JvmStatic
         fun builder(exerciseType: ExerciseType): Builder = Builder(exerciseType)
+
+        public const val SWIMMING_POOL_LENGTH_UNSPECIFIED = 0.0f
     }
 }
diff --git a/health/health-services-client/src/main/proto/data.proto b/health/health-services-client/src/main/proto/data.proto
index b3838a5..0a0a05b 100644
--- a/health/health-services-client/src/main/proto/data.proto
+++ b/health/health-services-client/src/main/proto/data.proto
@@ -177,11 +177,12 @@
   optional ExerciseType exercise_type = 1;
   repeated DataType data_types = 2;
   repeated DataType aggregate_data_types = 3;
-  optional bool is_auto_pause_and_resume_enabled = 4;
+  optional bool is_auto_pause_and_resume_enabled = 4; // TODO(sarakato): Move to dynamicExericseConfig
   optional bool is_gps_usage_enabled = 5;
   repeated ExerciseGoal exercise_goals = 6;
-  optional Bundle exercise_params = 7;
-  reserved 8 to max;  // Next ID
+  optional Bundle exercise_params = 7; // TODO(b/241015676): Deprecate
+  optional float swimming_pool_length = 8;
+  reserved 9 to max;  // Next ID
 }
 
 message ExerciseInfo {
diff --git a/hilt/hilt-compiler/build.gradle b/hilt/hilt-compiler/build.gradle
index 903b3be..fd43c06 100644
--- a/hilt/hilt-compiler/build.gradle
+++ b/hilt/hilt-compiler/build.gradle
@@ -24,6 +24,8 @@
     id("kotlin-kapt")
 }
 
+androidx.enableAarAsJarForJvmTest()
+
 dependencies {
     implementation(libs.kotlinStdlib)
     compileOnly(libs.autoServiceAnnotations)
@@ -38,22 +40,12 @@
     testImplementation(libs.truth)
     testImplementation(libs.googleCompileTesting)
     testImplementation(libs.hiltCore)
-    testImplementation(fileTree(
-            dir: provider {
-                // Replace with AGP API once it is added b/228109260
-                // Wrapping in a provider as a workaround as we access buildDir before this project is configured
-                "${new File(project(":hilt:hilt-work").buildDir, "libJar")}"
-            },
-            include : "*.jar"))
+    testAarAsJar(project(":hilt:hilt-work"))
     testImplementation(fileTree(
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include : "android.jar"))
 }
 
-tasks.named("compileKotlin").configure {
-    dependsOn(":hilt:hilt-work:jarRelease")
-}
-
 androidx {
     name = "AndroidX Hilt Extension Compiler"
     type = LibraryType.ANNOTATION_PROCESSOR
diff --git a/hilt/hilt-work/build.gradle b/hilt/hilt-work/build.gradle
index ec2e644..da8f26b 100644
--- a/hilt/hilt-work/build.gradle
+++ b/hilt/hilt-work/build.gradle
@@ -36,18 +36,6 @@
     annotationProcessor(libs.hiltCompiler)
 }
 
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-
-    // Create jar<variant> task for testImplementation in hilt-compiler.
-    project.tasks.register("jar${suffix}", Jar).configure {
-        dependsOn(variant.javaCompileProvider)
-        from(variant.javaCompileProvider.map { task -> task.destinationDir})
-        destinationDirectory.fileValue(new File(project.buildDir, "libJar"))
-    }
-}
-
 androidx {
     name = "Android Lifecycle WorkManager Hilt Extension"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
index 8a3544c..2f5146a 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-savedstate/build.gradle
@@ -14,9 +14,7 @@
  * limitations under the License.
  */
 
-
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -61,19 +59,6 @@
     }
 }
 
-//used by testImplementation safe-args-generator
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-    project.tasks.register("jar${suffix}", Copy).configure {
-        dependsOn ("assemble$suffix")
-        from(zipTree("${project.buildDir}/outputs/aar/lifecycle-viewmodel-savedstate-${name}.aar")) {
-            include "classes.jar"
-        }
-        destinationDir new File(project.buildDir, "libJar")
-    }
-}
-
 androidx {
     name = "Android Lifecycle ViewModel with SavedState"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 8bb9c03..1428843 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -15,7 +15,6 @@
  */
 
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -60,19 +59,6 @@
     lintPublish(project(':navigation:navigation-common-lint'))
 }
 
-//used by testImplementation safe-args-generator
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-    project.tasks.register("jar${suffix}", Copy).configure {
-        dependsOn("assemble$suffix")
-        from(zipTree("${project.buildDir}/outputs/aar/navigation-common-${name}.aar")) {
-            include("classes.jar")
-        }
-        destinationDir(new File(project.buildDir, "libJar"))
-    }
-}
-
 androidx {
     name = "Android Navigation Common"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/navigation/navigation-safe-args-generator/build.gradle b/navigation/navigation-safe-args-generator/build.gradle
index 0a72090..1b6930c 100644
--- a/navigation/navigation-safe-args-generator/build.gradle
+++ b/navigation/navigation-safe-args-generator/build.gradle
@@ -23,6 +23,8 @@
     id("kotlin")
 }
 
+androidx.enableAarAsJarForJvmTest()
+
 dependencies {
     implementation(libs.xpp3)
     implementation(libs.xmlpull)
@@ -40,22 +42,8 @@
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include : "android.jar"
     ))
-    testImplementation(fileTree(
-            dir: provider {
-                // Wrapping in a provider as a workaround as we access buildDir before this project is configured
-                // Replace with AGP API once it is added b/228109260
-                "${new File(project(":navigation:navigation-common").buildDir, "libJar")}"
-            },
-            include : "*.jar"
-    ))
-    testImplementation(fileTree(
-            dir: provider {
-                // Wrapping in a provider as a workaround as we access buildDir before this project is configured
-                // Replace with AGP API once it is added b/228109260
-                "${new File(project(":lifecycle:lifecycle-viewmodel-savedstate").buildDir, "libJar")}"
-            },
-            include : "*.jar"
-    ))
+    testAarAsJar(project(":navigation:navigation-common"))
+    testAarAsJar(project(":lifecycle:lifecycle-viewmodel-savedstate"))
 }
 
 tasks.findByName("test").doFirst {
@@ -66,9 +54,6 @@
     it.classpath = files(classpath.minus(androidJar).plus(androidJar))
 }
 
-tasks.findByName("compileKotlin").dependsOn(":navigation:navigation-common:jarRelease")
-tasks.findByName("compileKotlin").dependsOn(":lifecycle:lifecycle-viewmodel-savedstate:jarRelease")
-
 androidx {
     name = "Android Navigation TypeSafe Arguments Generator"
     type = LibraryType.OTHER_CODE_PROCESSOR
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 84fd8f6..7da64c1 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -3,8 +3,10 @@
 
   public final class TestPager<Key, Value> {
     ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+    method public suspend Object? append(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
     method public suspend Object? getPages(kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
+    method public suspend Object? prepend(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? refresh(optional Key? initialKey, optional kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
   }
 
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index 84fd8f6..7da64c1 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -3,8 +3,10 @@
 
   public final class TestPager<Key, Value> {
     ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+    method public suspend Object? append(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
     method public suspend Object? getPages(kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
+    method public suspend Object? prepend(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? refresh(optional Key? initialKey, optional kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
   }
 
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 84fd8f6..7da64c1 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -3,8 +3,10 @@
 
   public final class TestPager<Key, Value> {
     ctor public TestPager(androidx.paging.PagingSource<Key,Value> pagingSource, androidx.paging.PagingConfig config);
+    method public suspend Object? append(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? getLastLoadedPage(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult.Page<Key,Value>>);
     method public suspend Object? getPages(kotlin.coroutines.Continuation<? super java.util.List<? extends androidx.paging.PagingSource.LoadResult.Page<Key,Value>>>);
+    method public suspend Object? prepend(kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
     method public suspend Object? refresh(optional Key? initialKey, optional kotlin.coroutines.Continuation<? super androidx.paging.PagingSource.LoadResult<Key,Value>>);
   }
 
diff --git a/paging/paging-testing/src/main/java/androidx/paging/TestPager.kt b/paging/paging-testing/src/main/java/androidx/paging/TestPager.kt
index 982a264..03f99f7 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/TestPager.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/TestPager.kt
@@ -15,6 +15,9 @@
  */
 
 import androidx.paging.LoadType
+import androidx.paging.LoadType.APPEND
+import androidx.paging.LoadType.PREPEND
+import androidx.paging.LoadType.REFRESH
 import androidx.paging.PagingConfig
 import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadParams
@@ -45,19 +48,17 @@
 
     private val lock = Mutex()
 
-    private var nextKey: Key? = null
-    private var prevKey: Key? = null
     private val pages = ArrayDeque<LoadResult.Page<Key, Value>>()
 
-    // TODO add instruction that refresh() must be called before either append() or prepend()
     /**
      * Performs a load of [LoadType.REFRESH] on the PagingSource.
      *
      * If initialKey != null, refresh will start loading from the supplied key.
      *
      * Since Paging's first load is always of [LoadType.REFRESH], this method must be the very
-     * first load operation to be called on the TestPager. For example, you can call
-     * [getLastLoadedPage] before any load operations.
+     * first load operation to be called on the TestPager before either [append] or [prepend]
+     * can be called. However, other non-loading operations can still be invoked. For example,
+     * you can call [getLastLoadedPage] before any load operations.
      *
      * Returns the LoadResult upon refresh on the [PagingSource].
      *
@@ -70,22 +71,89 @@
      * such as [getLastLoadedPage].
      */
     public suspend fun refresh(initialKey: Key? = null): LoadResult<Key, Value> {
-        ensureValidPagingSource()
         if (!hasRefreshed.compareAndSet(false, true)) {
             pagingSource.invalidate()
             throw IllegalStateException("TestPager does not support multi-generational access " +
                 "and refresh() can only be called once per TestPager. To start a new generation," +
                 "create a new TestPager with a new PagingSource.")
         }
+        return doInitialLoad(initialKey)
+    }
 
+    /**
+     * Performs a load of [LoadType.APPEND] on the PagingSource.
+     *
+     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
+     * first before this [append] is called.
+     *
+     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
+     * such as when there is no more data to append, this append will be no-op by returning null.
+     */
+    public suspend fun append(): LoadResult<Key, Value>? {
+        return doLoad(APPEND)
+    }
+
+    /**
+     * Performs a load of [LoadType.PREPEND] on the PagingSource.
+     *
+     * Since Paging's first load is always of [LoadType.REFRESH], [refresh] must always be called
+     * first before this [prepend] is called.
+     *
+     * Returns the [LoadResult] from calling [PagingSource.load]. If the [LoadParams.key] is null,
+     * such as when there is no more data to prepend, this prepend will be no-op by returning null.
+     */
+    public suspend fun prepend(): LoadResult<Key, Value>? {
+        return doLoad(PREPEND)
+    }
+
+    /**
+     * Helper to perform REFRESH loads.
+     */
+    private suspend fun doInitialLoad(initialKey: Key?): LoadResult<Key, Value> {
         return lock.withLock {
+            ensureValidPagingSource()
             pagingSource.load(
                 LoadParams.Refresh(initialKey, config.initialLoadSize, config.enablePlaceholders)
             ).also { result ->
                 if (result is LoadResult.Page) {
                     pages.addLast(result)
-                    nextKey = result.nextKey
-                    prevKey = result.prevKey
+                }
+            }
+        }
+    }
+
+    /**
+     * Helper to perform APPEND or PREPEND loads.
+     */
+    private suspend fun doLoad(loadType: LoadType): LoadResult<Key, Value>? {
+        return lock.withLock {
+            ensureValidPagingSource()
+            if (!hasRefreshed.get()) {
+                throw IllegalStateException("TestPager's first load operation must be a refresh. " +
+                    "Please call refresh() once before calling ${loadType.name.lowercase()}().")
+            }
+            when (loadType) {
+                REFRESH -> throw IllegalArgumentException(
+                    "For LoadType.REFRESH use doInitialLoad()"
+                )
+                APPEND -> {
+                    val key = pages.lastOrNull()?.nextKey ?: return null
+                    pagingSource.load(
+                        LoadParams.Append(key, config.pageSize, config.enablePlaceholders)
+                    ).also { result ->
+                        if (result is LoadResult.Page) {
+                            pages.addLast(result)
+                        }
+                    }
+                } PREPEND -> {
+                    val key = pages.firstOrNull()?.prevKey ?: return null
+                    pagingSource.load(
+                        LoadParams.Prepend(key, config.pageSize, config.enablePlaceholders)
+                    ).also { result ->
+                        if (result is LoadResult.Page) {
+                            pages.addFirst(result)
+                        }
+                    }
                 }
             }
         }
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/TestPagerTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/TestPagerTest.kt
index 50d75d9..ae04046 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/TestPagerTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/TestPagerTest.kt
@@ -21,6 +21,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.assertFailsWith
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertTrue
 import org.junit.Test
@@ -32,13 +34,13 @@
 class TestPagerTest {
 
     @Test
-    fun refresh_returnPage() {
+    fun refresh_nullKey() {
         val source = TestPagingSource()
         val pager = TestPager(source, CONFIG)
 
         runTest {
             val result = pager.run {
-                refresh()
+                refresh(null)
             } as LoadResult.Page
 
             assertThat(result.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
@@ -114,7 +116,7 @@
     }
 
     @Test
-    fun refresh_getlastPage() {
+    fun refresh_getLastLoadedPage() {
         val source = TestPagingSource()
         val pager = TestPager(source, CONFIG)
 
@@ -129,6 +131,24 @@
     }
 
     @Test
+    fun getLastLoadedPage_afterInvalidPagingSource() {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        runTest {
+            val page = pager.run {
+                refresh()
+                append() // page should be this appended page
+                source.invalidate()
+                assertTrue(source.invalid)
+                getLastLoadedPage()
+            }
+            assertThat(page).isNotNull()
+            assertThat(page?.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
+        }
+    }
+
+    @Test
     fun refresh_getPages() {
         val source = TestPagingSource()
         val pager = TestPager(source, CONFIG)
@@ -154,6 +174,40 @@
     }
 
     @Test
+    fun getPages_afterInvalidPagingSource() {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        runTest {
+            val pages = pager.run {
+                refresh()
+                append()
+                source.invalidate()
+                assertTrue(source.invalid)
+                getPages()
+            }
+            assertThat(pages).containsExactlyElementsIn(
+                listOf(
+                    LoadResult.Page(
+                        data = listOf(0, 1, 2, 3, 4),
+                        prevKey = null,
+                        nextKey = 5,
+                        itemsBefore = 0,
+                        itemsAfter = 95
+                    ),
+                    LoadResult.Page(
+                        data = listOf(5, 6, 7),
+                        prevKey = 4,
+                        nextKey = 8,
+                        itemsBefore = 5,
+                        itemsAfter = 92
+                    )
+                )
+            ).inOrder()
+        }
+    }
+
+    @Test
     fun multipleRefresh_onSinglePager_throws() {
         val source = TestPagingSource()
         val pager = TestPager(source, CONFIG)
@@ -197,6 +251,435 @@
         assertThat(result2.data).containsExactlyElementsIn(listOf(0, 1, 2, 3, 4)).inOrder()
     }
 
+    @Test
+    fun simpleAppend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        val result = pager.run {
+            refresh(null)
+            append()
+        } as LoadResult.Page
+
+        assertThat(result.data).containsExactlyElementsIn(listOf(5, 6, 7)).inOrder()
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                LoadResult.Page(
+                    data = listOf(0, 1, 2, 3, 4),
+                    prevKey = null,
+                    nextKey = 5,
+                    itemsBefore = 0,
+                    itemsAfter = 95
+                ),
+                LoadResult.Page(
+                    data = listOf(5, 6, 7),
+                    prevKey = 4,
+                    nextKey = 8,
+                    itemsBefore = 5,
+                    itemsAfter = 92
+                )
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun simplePrepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        val result = pager.run {
+            refresh(30)
+            prepend()
+        } as LoadResult.Page
+
+        assertThat(result.data).containsExactlyElementsIn(listOf(27, 28, 29)).inOrder()
+        // prepended pages should be inserted before refresh
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                LoadResult.Page(
+                    data = listOf(27, 28, 29),
+                    prevKey = 26,
+                    nextKey = 30,
+                    itemsBefore = 27,
+                    itemsAfter = 70
+                ),
+                // refresh
+                LoadResult.Page(
+                    data = listOf(30, 31, 32, 33, 34),
+                    prevKey = 29,
+                    nextKey = 35,
+                    itemsBefore = 30,
+                    itemsAfter = 65
+                ),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_beforeRefresh_throws() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+        assertFailsWith<IllegalStateException> {
+            pager.run {
+                append()
+            }
+        }
+    }
+
+    @Test
+    fun prepend_beforeRefresh_throws() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+        assertFailsWith<IllegalStateException> {
+            pager.run {
+                prepend()
+            }
+        }
+    }
+
+    @Test
+    fun append_invalidPagingSource() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        assertFailsWith<IllegalStateException> {
+            pager.run {
+                refresh()
+                source.invalidate()
+                append()
+            }
+        }
+    }
+
+    @Test
+    fun prepend_invalidPagingSource() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        assertFailsWith<IllegalStateException> {
+            pager.run {
+                refresh()
+                source.invalidate()
+                prepend()
+            }
+        }
+    }
+
+    @Test
+    fun consecutive_append() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        pager.run {
+            refresh(20)
+            append()
+            append()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                LoadResult.Page(
+                    data = listOf(20, 21, 22, 23, 24),
+                    prevKey = 19,
+                    nextKey = 25,
+                    itemsBefore = 20,
+                    itemsAfter = 75
+                ),
+                LoadResult.Page(
+                    data = listOf(25, 26, 27),
+                    prevKey = 24,
+                    nextKey = 28,
+                    itemsBefore = 25,
+                    itemsAfter = 72
+                ),
+                LoadResult.Page(
+                    data = listOf(28, 29, 30),
+                    prevKey = 27,
+                    nextKey = 31,
+                    itemsBefore = 28,
+                    itemsAfter = 69
+                )
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun consecutive_prepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        pager.run {
+            refresh(20)
+            prepend()
+            prepend()
+        } as LoadResult.Page
+
+        // prepended pages should be ordered before the refresh
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // 2nd prepend
+                LoadResult.Page(
+                    data = listOf(14, 15, 16),
+                    prevKey = 13,
+                    nextKey = 17,
+                    itemsBefore = 14,
+                    itemsAfter = 83
+                ),
+                // 1st prepend
+                LoadResult.Page(
+                    data = listOf(17, 18, 19),
+                    prevKey = 16,
+                    nextKey = 20,
+                    itemsBefore = 17,
+                    itemsAfter = 80
+                ),
+                // refresh
+                LoadResult.Page(
+                    data = listOf(20, 21, 22, 23, 24),
+                    prevKey = 19,
+                    nextKey = 25,
+                    itemsBefore = 20,
+                    itemsAfter = 75
+                ),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun append_then_prepend() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        pager.run {
+            refresh(20)
+            append()
+            prepend()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                LoadResult.Page(
+                    data = listOf(17, 18, 19),
+                    prevKey = 16,
+                    nextKey = 20,
+                    itemsBefore = 17,
+                    itemsAfter = 80
+                ),
+                // refresh
+                LoadResult.Page(
+                    data = listOf(20, 21, 22, 23, 24),
+                    prevKey = 19,
+                    nextKey = 25,
+                    itemsBefore = 20,
+                    itemsAfter = 75
+                ),
+                // append
+                LoadResult.Page(
+                    data = listOf(25, 26, 27),
+                    prevKey = 24,
+                    nextKey = 28,
+                    itemsBefore = 25,
+                    itemsAfter = 72
+                ),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun prepend_then_append() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+
+        pager.run {
+            refresh(20)
+            prepend()
+            append()
+        } as LoadResult.Page
+
+        assertThat(pager.getPages()).containsExactlyElementsIn(
+            listOf(
+                // prepend
+                LoadResult.Page(
+                    data = listOf(17, 18, 19),
+                    prevKey = 16,
+                    nextKey = 20,
+                    itemsBefore = 17,
+                    itemsAfter = 80
+                ),
+                // refresh
+                LoadResult.Page(
+                    data = listOf(20, 21, 22, 23, 24),
+                    prevKey = 19,
+                    nextKey = 25,
+                    itemsBefore = 20,
+                    itemsAfter = 75
+                ),
+                // append
+                LoadResult.Page(
+                    data = listOf(25, 26, 27),
+                    prevKey = 24,
+                    nextKey = 28,
+                    itemsBefore = 25,
+                    itemsAfter = 72
+                ),
+            )
+        ).inOrder()
+    }
+
+    @Test
+    fun multiThread_loads() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+        // load operations upon completion add an int to the list.
+        // after all loads complete, we assert the order that the ints were added.
+        val loadOrder = mutableListOf<Int>()
+
+        val job = launch {
+            pager.run {
+                refresh(20).also { loadOrder.add(1) } // first load
+                prepend().also { loadOrder.add(3) } // third load
+                append().also { loadOrder.add(5) } // fifth load
+            }
+        }
+        job.start()
+        assertTrue(job.isActive)
+
+        pager.run {
+            // give some time for job to start
+            delay(200)
+            append().also { loadOrder.add(2) } // second load
+            prepend().also { loadOrder.add(4) } // fourth load
+        }
+
+        job.invokeOnCompletion {
+            launch {
+                assertThat(loadOrder).containsExactlyElementsIn(listOf(1, 2, 3, 4, 5)).inOrder()
+                assertThat(pager.getPages()).containsExactlyElementsIn(
+                    listOf(
+                        LoadResult.Page(
+                            data = listOf(14, 15, 16),
+                            prevKey = 13,
+                            nextKey = 17,
+                            itemsBefore = 14,
+                            itemsAfter = 83
+                        ),
+                        LoadResult.Page(
+                            data = listOf(17, 18, 19),
+                            prevKey = 16,
+                            nextKey = 20,
+                            itemsBefore = 17,
+                            itemsAfter = 80
+                        ),
+                        LoadResult.Page(
+                            data = listOf(20, 21, 22, 23, 24),
+                            prevKey = 19,
+                            nextKey = 25,
+                            itemsBefore = 20,
+                            itemsAfter = 75
+                        ),
+                        LoadResult.Page(
+                            data = listOf(25, 26, 27),
+                            prevKey = 24,
+                            nextKey = 28,
+                            itemsBefore = 25,
+                            itemsAfter = 72
+                        ),
+                        LoadResult.Page(
+                            data = listOf(28, 29, 30),
+                            prevKey = 27,
+                            nextKey = 31,
+                            itemsBefore = 28,
+                            itemsAfter = 69
+                        ),
+                    )
+                ).inOrder()
+            }
+        }
+    }
+
+    @Test
+    fun multiThread_operations() = runTest {
+        val source = TestPagingSource()
+        val pager = TestPager(source, CONFIG)
+        // operations upon completion add an int to the list.
+        // after all operations complete, we assert the order that the ints were added.
+        val loadOrder = mutableListOf<Int>()
+
+        var lastLoadedPage: LoadResult.Page<Int, Int>? = null
+        val job = launch {
+            pager.run {
+                refresh(20).also { loadOrder.add(1) } // first operation
+                // third operation, should return first appended page
+                lastLoadedPage = getLastLoadedPage().also { loadOrder.add(3) }
+                append().also { loadOrder.add(5) } // fifth operation
+                prepend().also { loadOrder.add(7) } // last operation
+            }
+        }
+        job.start()
+        assertTrue(job.isActive)
+
+        val pages = pager.run {
+            // give some time for job to start first
+            delay(200)
+            append().also { loadOrder.add(2) } // second operation
+            prepend().also { loadOrder.add(4) } // fourth operation
+            // sixth operation, should return 4 pages
+            getPages().also { loadOrder.add(6) }
+        }
+
+        job.invokeOnCompletion {
+            launch {
+                assertThat(loadOrder).containsExactlyElementsIn(
+                    listOf(1, 2, 3, 4, 5, 6, 7)
+                ).inOrder()
+                assertThat(lastLoadedPage).isEqualTo(
+                    LoadResult.Page(
+                        data = listOf(25, 26, 27),
+                        prevKey = 24,
+                        nextKey = 28,
+                        itemsBefore = 25,
+                        itemsAfter = 72
+                    ),
+                )
+                // should not contain the second prepend, with a total of 4 pages
+                assertThat(pages).containsExactlyElementsIn(
+                    listOf(
+                        LoadResult.Page( // first prepend
+                            data = listOf(17, 18, 19),
+                            prevKey = 16,
+                            nextKey = 20,
+                            itemsBefore = 17,
+                            itemsAfter = 80
+                        ),
+                        LoadResult.Page( // refresh
+                            data = listOf(20, 21, 22, 23, 24),
+                            prevKey = 19,
+                            nextKey = 25,
+                            itemsBefore = 20,
+                            itemsAfter = 75
+                        ),
+                        LoadResult.Page( // first append
+                            data = listOf(25, 26, 27),
+                            prevKey = 24,
+                            nextKey = 28,
+                            itemsBefore = 25,
+                            itemsAfter = 72
+                        ),
+                        LoadResult.Page( // second append
+                            data = listOf(28, 29, 30),
+                            prevKey = 27,
+                            nextKey = 31,
+                            itemsBefore = 28,
+                            itemsAfter = 69
+                        ),
+                    )
+                ).inOrder()
+            }
+        }
+    }
+
     private val CONFIG = PagingConfig(
         pageSize = 3,
         initialLoadSize = 5,
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
index 5f845f9..8061a82 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/AbstractSdkProviderGenerator.kt
@@ -67,7 +67,7 @@
             .addModifiers(KModifier.OVERRIDE)
             .addParameter("params", BUNDLE_CLASS)
             .returns(SANDBOXED_SDK_CLASS)
-            .addStatement("val sdk = ${getCreateServiceFunctionName(service())}(getContext())")
+            .addStatement("val sdk = ${getCreateServiceFunctionName(service())}(context!!)")
             .addStatement(
                 "return ${SANDBOXED_SDK_CLASS.simpleName}" +
                     "(${service().stubDelegateName()}(sdk))"
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 98ae0cc..2155f10 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -77,7 +77,7 @@
                     |
                     |public abstract class AbstractSandboxedSdkProvider : SandboxedSdkProvider() {
                     |  public override fun onLoadSdk(params: Bundle): SandboxedSdk {
-                    |    val sdk = createMySdk(getContext())
+                    |    val sdk = createMySdk(context!!)
                     |    return SandboxedSdk(MySdkStubDelegate(sdk))
                     |  }
                     |
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index 0add76f..48efc91 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -843,17 +843,22 @@
                         ): SupportSQLiteOpenHelper {
                             val helperDelegate = factoryDelegate.create(configuration)
                             return object : SupportSQLiteOpenHelper by helperDelegate {
-                                override fun getWritableDatabase(): SupportSQLiteDatabase {
-                                    val databaseDelegate = helperDelegate.writableDatabase
-                                    return object : SupportSQLiteDatabase by databaseDelegate {
-                                        override fun beginTransaction() {
-                                            throw RuntimeException("Error beginning transaction.")
-                                        }
-                                        override fun beginTransactionNonExclusive() {
-                                            throw RuntimeException("Error beginning transaction.")
+                                override val writableDatabase: SupportSQLiteDatabase
+                                    get() {
+                                        val databaseDelegate = helperDelegate.writableDatabase
+                                        return object : SupportSQLiteDatabase by databaseDelegate {
+                                            override fun beginTransaction() {
+                                                throw RuntimeException(
+                                                    "Error beginning transaction."
+                                                )
+                                            }
+                                            override fun beginTransactionNonExclusive() {
+                                                throw RuntimeException(
+                                                    "Error beginning transaction."
+                                                )
+                                            }
                                         }
                                     }
-                                }
                             }
                         }
                     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
index 8f8b31d..d220629 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspFieldElement.kt
@@ -59,7 +59,11 @@
                 emptyList()
             }
             declaration.isPrivate() -> emptyList()
-
+            declaration.modifiers.contains(Modifier.CONST) -> {
+                // No accessors are needed for const properties:
+                // https://kotlinlang.org/docs/java-to-kotlin-interop.html#static-fields
+                emptyList()
+            }
             else -> {
                 sequenceOf(declaration.getter, declaration.setter)
                     .filterNotNull()
@@ -68,16 +72,6 @@
                         // them out.
                         it.modifiers.contains(Modifier.PRIVATE)
                     }
-                    .filter {
-                        if (isStatic()) {
-                            // static fields are the properties that are coming from the
-                            // companion. Whether we'll generate method for it or not depends on
-                            // the JVMStatic annotation
-                            it.hasJvmStaticAnnotation() || declaration.hasJvmStaticAnnotation()
-                        } else {
-                            true
-                        }
-                    }
                     .map { accessor ->
                         KspSyntheticPropertyMethodElement.create(
                             env = env,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 71c4d02..bf0c668 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -192,8 +192,23 @@
     }
 
     private val syntheticGetterSetterMethods: List<XMethodElement> by lazy {
-        _declaredProperties.flatMap { field ->
-            field.syntheticAccessors
+        if (declaration.isCompanionObject) {
+            _declaredProperties.flatMap { field ->
+                field.syntheticAccessors
+            }
+        } else {
+            _declaredProperties.flatMap { field ->
+                // static fields are the properties that are coming from the
+                // companion. Whether we'll generate method for it or not depends on
+                // the JVMStatic annotation
+                if (field.isStatic() && !field.declaration.hasJvmStaticAnnotation()) {
+                    field.syntheticAccessors.filter {
+                        it.accessor.hasJvmStaticAnnotation()
+                    }
+                } else {
+                    field.syntheticAccessors
+                }
+            }
         }.filterMethodsByConfig(env)
     }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index 1f1a834..0c28901 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -712,7 +712,10 @@
                     assertThat(fields.map { it.name }).containsExactly(
                         "property",
                         "companionObjectProperty",
-                        "companionObjectPropertyJvmStatic"
+                        "companionObjectPropertyJvmStatic",
+                        "companionObjectPropertyJvmField",
+                        "companionObjectPropertyLateinit",
+                        "companionObjectPropertyConst"
                     )
                     fields.forEach {
                         if (it.name.startsWith("companion")) {
@@ -726,7 +729,10 @@
                         "Companion",
                         "property",
                         "companionObjectProperty",
-                        "companionObjectPropertyJvmStatic"
+                        "companionObjectPropertyJvmStatic",
+                        "companionObjectPropertyJvmField",
+                        "companionObjectPropertyLateinit",
+                        "companionObjectPropertyConst"
                     )
                     fields.forEach {
                         assertThat(it.enclosingElement).isEqualTo(enclosingElement)
@@ -753,7 +759,10 @@
                 if (invocation.isKsp) {
                     assertThat(fields.map { it.name }).containsExactly(
                         "companionObjectProperty",
-                        "companionObjectPropertyJvmStatic"
+                        "companionObjectPropertyJvmStatic",
+                        "companionObjectPropertyJvmField",
+                        "companionObjectPropertyLateinit",
+                        "companionObjectPropertyConst"
                     )
                     fields.forEach {
                         assertThat(it.enclosingElement).isEqualTo(companionObj)
@@ -770,15 +779,20 @@
 
                 if (invocation.isKsp) {
                     assertThat(methods.map { it.name }).containsExactly(
+                        "getCompanionObjectProperty",
+                        "getCompanionObjectPropertyJvmStatic",
+                        "getCompanionObjectPropertyLateinit",
+                        "setCompanionObjectPropertyLateinit",
                         "companionObjectFunction",
                         "companionObjectFunctionJvmStatic",
-                        "getCompanionObjectPropertyJvmStatic"
                     )
                 } else {
                     if (precompiled) {
                         assertThat(methods.map { it.name }).containsExactly(
                             "getCompanionObjectProperty",
                             "getCompanionObjectPropertyJvmStatic",
+                            "getCompanionObjectPropertyLateinit",
+                            "setCompanionObjectPropertyLateinit",
                             "companionObjectFunction",
                             "companionObjectFunctionJvmStatic"
                         )
@@ -787,6 +801,8 @@
                             "getCompanionObjectProperty",
                             "getCompanionObjectPropertyJvmStatic",
                             "getCompanionObjectPropertyJvmStatic\$annotations",
+                            "getCompanionObjectPropertyLateinit",
+                            "setCompanionObjectPropertyLateinit",
                             "companionObjectFunction",
                             "companionObjectFunctionJvmStatic"
                         )
@@ -990,6 +1006,9 @@
                 val companionObjectProperty: String = "hello"
                 @JvmStatic
                 val companionObjectPropertyJvmStatic: String = "hello"
+                @JvmField val companionObjectPropertyJvmField: String = "hello"
+                lateinit var companionObjectPropertyLateinit: String
+                const val companionObjectPropertyConst: String = "hello"
                 fun companionObjectFunction(companionFunctionParam: String) {}
                 @JvmStatic
                 fun companionObjectFunctionJvmStatic(companionFunctionParam: String) {}
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 3edc9a10..6ead8ab 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -84,6 +84,8 @@
     }
 }
 
+androidx.enableAarAsJarForJvmTest()
+
 dependencies {
     implementation(project(":room:room-common"))
     implementation(project(":room:room-migration"))
@@ -113,22 +115,8 @@
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include : "android.jar"
     ))
-    testImplementation(fileTree(
-            dir: provider {
-                // Wrapping in a provider as we access buildDir before this project is configured
-                // Replace with AGP API once it is added b/228109260
-                "${new File(project(":room:room-runtime").buildDir, "intermediates/runtime_library_classes_jar/release/")}"
-            },
-            include : "*.jar"
-    ))
-    testImplementation(fileTree(
-            dir: provider {
-                // Wrapping in a provider as we access buildDir before this project is configured
-                // Replace with AGP API once it is added b/228109260
-                "${new File(project(":sqlite:sqlite").buildDir, "intermediates/compile_library_classes_jar/release/")}"
-            },
-            include : "*.jar"
-    ))
+    testAarAsJar(project(":room:room-runtime"))
+    testAarAsJar(project(":sqlite:sqlite"))
     testImplementation(project(":internal-testutils-common"))
 }
 
@@ -284,8 +272,6 @@
 
 tasks.findByName("compileKotlin").dependsOn(generateAntlrTask)
 tasks.findByName("sourceJar").dependsOn(generateAntlrTask)
-tasks.findByName("compileKotlin").dependsOn(":room:room-runtime:bundleLibRuntimeToJarRelease")
-tasks.findByName("compileKotlin").dependsOn(":sqlite:sqlite:jarRelease")
 
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
index 867bca9..a618ad5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/util/SchemaDiffer.kt
@@ -33,10 +33,9 @@
 import androidx.room.vo.AutoMigration
 
 /**
- * This exception should be thrown to abandon processing an @AutoMigration.
+ * This RuntimeException should be thrown to abandon processing an @AutoMigration.
  *
  * @param errorMessage Error message to be thrown with the exception
- * @return RuntimeException with the provided error message
  */
 class DiffException(val errorMessage: String) : RuntimeException(errorMessage)
 
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index 56968c37..8eb962e 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -69,18 +69,6 @@
 
 }
 
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-
-    // Create jar<variant> task for testImplementation in room-compiler.
-    project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn(variant.javaCompileProvider.get())
-        from(variant.javaCompileProvider.get().destinationDir)
-        destinationDirectory.fileValue(new File(project.buildDir, "libJar"))
-    }
-}
-
 androidx {
     name = "Android Room-Runtime"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
index 76c4ba8..925914a 100644
--- a/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/AutoClosingRoomOpenHelper.kt
@@ -51,21 +51,21 @@
         )
     }
 
-    @RequiresApi(api = Build.VERSION_CODES.N)
-    override fun getWritableDatabase(): SupportSQLiteDatabase {
-        // Note we don't differentiate between writable db and readable db
-        // We try to open the db so the open callbacks run
-        autoClosingDb.pokeOpen()
-        return autoClosingDb
-    }
+    @get:RequiresApi(api = Build.VERSION_CODES.N)
+    override val writableDatabase: SupportSQLiteDatabase
+        get() {
+            autoClosingDb.pokeOpen()
+            return autoClosingDb
+        }
 
-    @RequiresApi(api = Build.VERSION_CODES.N)
-    override fun getReadableDatabase(): SupportSQLiteDatabase {
-        // Note we don't differentiate between writable db and readable db
-        // We try to open the db so the open callbacks run
-        autoClosingDb.pokeOpen()
-        return autoClosingDb
-    }
+    @get:RequiresApi(api = Build.VERSION_CODES.N)
+    override val readableDatabase: SupportSQLiteDatabase
+        get() {
+            // Note we don't differentiate between writable db and readable db
+            // We try to open the db so the open callbacks run
+            autoClosingDb.pokeOpen()
+            return autoClosingDb
+        }
 
     override fun close() {
         autoClosingDb.close()
diff --git a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt
index 1acb5d7..4f36a1f 100644
--- a/room/room-runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/QueryInterceptorOpenHelper.kt
@@ -26,19 +26,17 @@
     private val queryCallbackExecutor: Executor,
     private val queryCallback: RoomDatabase.QueryCallback
 ) : SupportSQLiteOpenHelper by delegate, DelegatingOpenHelper {
-    override fun getWritableDatabase(): SupportSQLiteDatabase {
-        return QueryInterceptorDatabase(
+    override val writableDatabase: SupportSQLiteDatabase
+        get() = QueryInterceptorDatabase(
             delegate.writableDatabase,
             queryCallbackExecutor,
             queryCallback
         )
-    }
 
-    override fun getReadableDatabase(): SupportSQLiteDatabase {
-        return QueryInterceptorDatabase(
+    override val readableDatabase: SupportSQLiteDatabase
+        get() = QueryInterceptorDatabase(
             delegate.readableDatabase,
             queryCallbackExecutor,
             queryCallback
         )
-    }
 }
diff --git a/room/room-runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt
index 834ecec..dceaab0 100644
--- a/room/room-runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/SQLiteCopyOpenHelper.kt
@@ -54,32 +54,31 @@
     private lateinit var databaseConfiguration: DatabaseConfiguration
     private var verified = false
 
-    override fun getDatabaseName(): String? {
-        return delegate.databaseName
-    }
+    override val databaseName: String?
+        get() = delegate.databaseName
 
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
     override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
         delegate.setWriteAheadLoggingEnabled(enabled)
     }
 
-    @Synchronized
-    override fun getWritableDatabase(): SupportSQLiteDatabase {
-        if (!verified) {
-            verifyDatabaseFile(true)
-            verified = true
+    override val writableDatabase: SupportSQLiteDatabase
+        get() {
+            if (!verified) {
+                verifyDatabaseFile(true)
+                verified = true
+            }
+            return delegate.writableDatabase
         }
-        return delegate.writableDatabase
-    }
 
-    @Synchronized
-    override fun getReadableDatabase(): SupportSQLiteDatabase {
-        if (!verified) {
-            verifyDatabaseFile(false)
-            verified = true
+    override val readableDatabase: SupportSQLiteDatabase
+        get() {
+            if (!verified) {
+                verifyDatabaseFile(false)
+                verified = true
+            }
+            return delegate.readableDatabase
         }
-        return delegate.readableDatabase
-    }
 
     @Synchronized
     override fun close() {
diff --git a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
index 6857ab0..501180e 100644
--- a/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
+++ b/sqlite/sqlite-framework/src/main/java/androidx/sqlite/db/framework/FrameworkSQLiteOpenHelper.kt
@@ -88,9 +88,8 @@
     // because context.getNoBackupFilesDir() does File I/O :(
     private val delegate: OpenHelper by lazyDelegate
 
-    override fun getDatabaseName(): String? {
-        return name
-    }
+    override val databaseName: String?
+        get() = name
 
     @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
     override fun setWriteAheadLoggingEnabled(enabled: Boolean) {
@@ -101,13 +100,11 @@
         writeAheadLoggingEnabled = enabled
     }
 
-    override fun getWritableDatabase(): SupportSQLiteDatabase {
-        return delegate.getSupportDatabase(true)
-    }
+    override val writableDatabase: SupportSQLiteDatabase
+        get() = delegate.getSupportDatabase(true)
 
-    override fun getReadableDatabase(): SupportSQLiteDatabase {
-        return delegate.getSupportDatabase(false)
-    }
+    override val readableDatabase: SupportSQLiteDatabase
+        get() = delegate.getSupportDatabase(false)
 
     override fun close() {
         if (lazyDelegate.isInitialized()) {
diff --git a/sqlite/sqlite/api/current.txt b/sqlite/sqlite/api/current.txt
index 53bbd20..d49da58 100644
--- a/sqlite/sqlite/api/current.txt
+++ b/sqlite/sqlite/api/current.txt
@@ -65,22 +65,27 @@
     method public String? getDatabaseName();
     method public androidx.sqlite.db.SupportSQLiteDatabase getReadableDatabase();
     method public androidx.sqlite.db.SupportSQLiteDatabase getWritableDatabase();
-    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean enabled);
+    property public abstract String? databaseName;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase readableDatabase;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase writableDatabase;
   }
 
   public abstract static class SupportSQLiteOpenHelper.Callback {
-    ctor public SupportSQLiteOpenHelper.Callback(int);
-    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
-    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
+    ctor public SupportSQLiteOpenHelper.Callback(int version);
+    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
+    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
     field public final int version;
   }
 
-  public static class SupportSQLiteOpenHelper.Configuration {
-    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+  public static final class SupportSQLiteOpenHelper.Configuration {
+    ctor public SupportSQLiteOpenHelper.Configuration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback, optional boolean useNoBackupDirectory, optional boolean allowDataLossOnRecovery);
+    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+    field public static final androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Companion Companion;
     field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
@@ -89,15 +94,19 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean allowDataLossOnRecovery);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String? name);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean useNoBackupDirectory);
   }
 
-  public static interface SupportSQLiteOpenHelper.Factory {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration);
+  public static final class SupportSQLiteOpenHelper.Configuration.Companion {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+  }
+
+  public static fun interface SupportSQLiteOpenHelper.Factory {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration configuration);
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
@@ -118,15 +127,20 @@
   }
 
   public final class SupportSQLiteQueryBuilder {
-    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder! builder(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! columns(String![]!);
-    method public androidx.sqlite.db.SupportSQLiteQuery! create();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! distinct();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! groupBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! having(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! limit(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! orderBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! selection(String!, Object![]!);
+    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder columns(String![]? columns);
+    method public androidx.sqlite.db.SupportSQLiteQuery create();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder distinct();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder groupBy(String? groupBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder having(String? having);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder limit(String limit);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder orderBy(String? orderBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder selection(String? selection, Object![]? bindArgs);
+    field public static final androidx.sqlite.db.SupportSQLiteQueryBuilder.Companion Companion;
+  }
+
+  public static final class SupportSQLiteQueryBuilder.Companion {
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
   }
 
   public interface SupportSQLiteStatement extends androidx.sqlite.db.SupportSQLiteProgram {
diff --git a/sqlite/sqlite/api/public_plus_experimental_current.txt b/sqlite/sqlite/api/public_plus_experimental_current.txt
index 53bbd20..d49da58 100644
--- a/sqlite/sqlite/api/public_plus_experimental_current.txt
+++ b/sqlite/sqlite/api/public_plus_experimental_current.txt
@@ -65,22 +65,27 @@
     method public String? getDatabaseName();
     method public androidx.sqlite.db.SupportSQLiteDatabase getReadableDatabase();
     method public androidx.sqlite.db.SupportSQLiteDatabase getWritableDatabase();
-    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean enabled);
+    property public abstract String? databaseName;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase readableDatabase;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase writableDatabase;
   }
 
   public abstract static class SupportSQLiteOpenHelper.Callback {
-    ctor public SupportSQLiteOpenHelper.Callback(int);
-    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
-    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
+    ctor public SupportSQLiteOpenHelper.Callback(int version);
+    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
+    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
     field public final int version;
   }
 
-  public static class SupportSQLiteOpenHelper.Configuration {
-    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+  public static final class SupportSQLiteOpenHelper.Configuration {
+    ctor public SupportSQLiteOpenHelper.Configuration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback, optional boolean useNoBackupDirectory, optional boolean allowDataLossOnRecovery);
+    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+    field public static final androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Companion Companion;
     field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
@@ -89,15 +94,19 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean allowDataLossOnRecovery);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String? name);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean useNoBackupDirectory);
   }
 
-  public static interface SupportSQLiteOpenHelper.Factory {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration);
+  public static final class SupportSQLiteOpenHelper.Configuration.Companion {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+  }
+
+  public static fun interface SupportSQLiteOpenHelper.Factory {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration configuration);
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
@@ -118,15 +127,20 @@
   }
 
   public final class SupportSQLiteQueryBuilder {
-    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder! builder(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! columns(String![]!);
-    method public androidx.sqlite.db.SupportSQLiteQuery! create();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! distinct();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! groupBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! having(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! limit(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! orderBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! selection(String!, Object![]!);
+    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder columns(String![]? columns);
+    method public androidx.sqlite.db.SupportSQLiteQuery create();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder distinct();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder groupBy(String? groupBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder having(String? having);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder limit(String limit);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder orderBy(String? orderBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder selection(String? selection, Object![]? bindArgs);
+    field public static final androidx.sqlite.db.SupportSQLiteQueryBuilder.Companion Companion;
+  }
+
+  public static final class SupportSQLiteQueryBuilder.Companion {
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
   }
 
   public interface SupportSQLiteStatement extends androidx.sqlite.db.SupportSQLiteProgram {
diff --git a/sqlite/sqlite/api/restricted_current.txt b/sqlite/sqlite/api/restricted_current.txt
index 53bbd20..d49da58 100644
--- a/sqlite/sqlite/api/restricted_current.txt
+++ b/sqlite/sqlite/api/restricted_current.txt
@@ -65,22 +65,27 @@
     method public String? getDatabaseName();
     method public androidx.sqlite.db.SupportSQLiteDatabase getReadableDatabase();
     method public androidx.sqlite.db.SupportSQLiteDatabase getWritableDatabase();
-    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.JELLY_BEAN) public void setWriteAheadLoggingEnabled(boolean enabled);
+    property public abstract String? databaseName;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase readableDatabase;
+    property public abstract androidx.sqlite.db.SupportSQLiteDatabase writableDatabase;
   }
 
   public abstract static class SupportSQLiteOpenHelper.Callback {
-    ctor public SupportSQLiteOpenHelper.Callback(int);
-    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
-    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase);
-    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase, int, int);
+    ctor public SupportSQLiteOpenHelper.Callback(int version);
+    method public void onConfigure(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onCorruption(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onDowngrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
+    method public void onOpen(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onUpgrade(androidx.sqlite.db.SupportSQLiteDatabase db, int oldVersion, int newVersion);
     field public final int version;
   }
 
-  public static class SupportSQLiteOpenHelper.Configuration {
-    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context);
+  public static final class SupportSQLiteOpenHelper.Configuration {
+    ctor public SupportSQLiteOpenHelper.Configuration(android.content.Context context, String? name, androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback, optional boolean useNoBackupDirectory, optional boolean allowDataLossOnRecovery);
+    method public static androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+    field public static final androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Companion Companion;
     field public final boolean allowDataLossOnRecovery;
     field public final androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback;
     field public final android.content.Context context;
@@ -89,15 +94,19 @@
   }
 
   public static class SupportSQLiteOpenHelper.Configuration.Builder {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder allowDataLossOnRecovery(boolean allowDataLossOnRecovery);
     method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration build();
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String?);
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder callback(androidx.sqlite.db.SupportSQLiteOpenHelper.Callback callback);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder name(String? name);
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder noBackupDirectory(boolean useNoBackupDirectory);
   }
 
-  public static interface SupportSQLiteOpenHelper.Factory {
-    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration);
+  public static final class SupportSQLiteOpenHelper.Configuration.Companion {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration.Builder builder(android.content.Context context);
+  }
+
+  public static fun interface SupportSQLiteOpenHelper.Factory {
+    method public androidx.sqlite.db.SupportSQLiteOpenHelper create(androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration configuration);
   }
 
   public interface SupportSQLiteProgram extends java.io.Closeable {
@@ -118,15 +127,20 @@
   }
 
   public final class SupportSQLiteQueryBuilder {
-    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder! builder(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! columns(String![]!);
-    method public androidx.sqlite.db.SupportSQLiteQuery! create();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! distinct();
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! groupBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! having(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! limit(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! orderBy(String!);
-    method public androidx.sqlite.db.SupportSQLiteQueryBuilder! selection(String!, Object![]!);
+    method public static androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder columns(String![]? columns);
+    method public androidx.sqlite.db.SupportSQLiteQuery create();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder distinct();
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder groupBy(String? groupBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder having(String? having);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder limit(String limit);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder orderBy(String? orderBy);
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder selection(String? selection, Object![]? bindArgs);
+    field public static final androidx.sqlite.db.SupportSQLiteQueryBuilder.Companion Companion;
+  }
+
+  public static final class SupportSQLiteQueryBuilder.Companion {
+    method public androidx.sqlite.db.SupportSQLiteQueryBuilder builder(String tableName);
   }
 
   public interface SupportSQLiteStatement extends androidx.sqlite.db.SupportSQLiteProgram {
diff --git a/sqlite/sqlite/build.gradle b/sqlite/sqlite/build.gradle
index d3f6c60..ca04999 100644
--- a/sqlite/sqlite/build.gradle
+++ b/sqlite/sqlite/build.gradle
@@ -33,17 +33,6 @@
     namespace "androidx.sqlite.db"
 }
 
-// Used by testImplementation in room-compiler
-android.libraryVariants.all { variant ->
-    def name = variant.name
-    def suffix = name.capitalize()
-    def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
-        dependsOn(variant.javaCompileProvider.get())
-        from(variant.javaCompileProvider.get().destinationDir)
-        destinationDirectory.fileValue(new File(project.buildDir, "libJar"))
-    }
-}
-
 androidx {
     name = "Android DB"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
deleted file mode 100644
index 0e46c47..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.java
+++ /dev/null
@@ -1,478 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.sqlite.db;
-
-import android.content.ContentProvider;
-import android.content.Context;
-import android.database.sqlite.SQLiteException;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import java.io.Closeable;
-import java.io.File;
-import java.io.IOException;
-import java.util.List;
-
-/**
- * An interface to map the behavior of {@link android.database.sqlite.SQLiteOpenHelper}.
- * Note that since that class requires overriding certain methods, support implementation
- * uses {@link Factory#create(Configuration)} to create this and {@link Callback} to implement
- * the methods that should be overridden.
- */
-@SuppressWarnings("unused")
-public interface SupportSQLiteOpenHelper extends Closeable {
-    /**
-     * Return the name of the SQLite database being opened, as given to
-     * the constructor. {@code null} indicates an in-memory database.
-     */
-    @Nullable
-    String getDatabaseName();
-
-    /**
-     * Enables or disables the use of write-ahead logging for the database.
-     *
-     * Write-ahead logging cannot be used with read-only databases so the value of
-     * this flag is ignored if the database is opened read-only.
-     *
-     * @param enabled True if write-ahead logging should be enabled, false if it
-     *                should be disabled.
-     * @see SupportSQLiteDatabase#enableWriteAheadLogging()
-     */
-    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
-    void setWriteAheadLoggingEnabled(boolean enabled);
-
-    /**
-     * Create and/or open a database that will be used for reading and writing.
-     * The first time this is called, the database will be opened and
-     * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be
-     * called.
-     *
-     * <p>Once opened successfully, the database is cached, so you can
-     * call this method every time you need to write to the database.
-     * (Make sure to call {@link #close} when you no longer need the database.)
-     * Errors such as bad permissions or a full disk may cause this method
-     * to fail, but future attempts may succeed if the problem is fixed.</p>
-     *
-     * <p class="caution">Database upgrade may take a long time, you
-     * should not call this method from the application main thread, including
-     * from {@link ContentProvider#onCreate ContentProvider.onCreate()}.
-     *
-     * @return a read/write database object valid until {@link #close} is called
-     * @throws SQLiteException if the database cannot be opened for writing
-     */
-    @NonNull
-    SupportSQLiteDatabase getWritableDatabase();
-
-    /**
-     * Create and/or open a database.  This will be the same object returned by
-     * {@link #getWritableDatabase} unless some problem, such as a full disk,
-     * requires the database to be opened read-only.  In that case, a read-only
-     * database object will be returned.  If the problem is fixed, a future call
-     * to {@link #getWritableDatabase} may succeed, in which case the read-only
-     * database object will be closed and the read/write object will be returned
-     * in the future.
-     *
-     * <p class="caution">Like {@link #getWritableDatabase}, this method may
-     * take a long time to return, so you should not call it from the
-     * application main thread, including from
-     * {@link ContentProvider#onCreate ContentProvider.onCreate()}.
-     *
-     * @return a database object valid until {@link #getWritableDatabase}
-     * or {@link #close} is called.
-     * @throws SQLiteException if the database cannot be opened
-     */
-    @NonNull
-    SupportSQLiteDatabase getReadableDatabase();
-
-    /**
-     * Close any open database object.
-     */
-    @Override void close();
-
-    /**
-     * Handles various lifecycle events for the SQLite connection, similar to
-     * {@link SQLiteOpenHelper}.
-     */
-    @SuppressWarnings({"unused", "WeakerAccess"})
-    abstract class Callback {
-        private static final String TAG = "SupportSQLite";
-        /**
-         * Version number of the database (starting at 1); if the database is older,
-         * {@link Callback#onUpgrade(SupportSQLiteDatabase, int, int)}
-         * will be used to upgrade the database; if the database is newer,
-         * {@link Callback#onDowngrade(SupportSQLiteDatabase, int, int)}
-         * will be used to downgrade the database.
-         */
-        public final int version;
-
-        /**
-         * Creates a new Callback to get database lifecycle events.
-         * @param version The version for the database instance. See {@link #version}.
-         */
-        public Callback(int version) {
-            this.version = version;
-        }
-
-        /**
-         * Called when the database connection is being configured, to enable features such as
-         * write-ahead logging or foreign key support.
-         * <p>
-         * This method is called before {@link #onCreate}, {@link #onUpgrade}, {@link #onDowngrade},
-         * or {@link #onOpen} are called. It should not modify the database except to configure the
-         * database connection as required.
-         * </p>
-         * <p>
-         * This method should only call methods that configure the parameters of the database
-         * connection, such as {@link SupportSQLiteDatabase#enableWriteAheadLogging}
-         * {@link SupportSQLiteDatabase#setForeignKeyConstraintsEnabled},
-         * {@link SupportSQLiteDatabase#setLocale},
-         * {@link SupportSQLiteDatabase#setMaximumSize}, or executing PRAGMA statements.
-         * </p>
-         *
-         * @param db The database.
-         */
-        public void onConfigure(@NonNull SupportSQLiteDatabase db) {
-
-        }
-
-        /**
-         * Called when the database is created for the first time. This is where the
-         * creation of tables and the initial population of the tables should happen.
-         *
-         * @param db The database.
-         */
-        public abstract void onCreate(@NonNull SupportSQLiteDatabase db);
-
-        /**
-         * Called when the database needs to be upgraded. The implementation
-         * should use this method to drop tables, add tables, or do anything else it
-         * needs to upgrade to the new schema version.
-         *
-         * <p>
-         * The SQLite ALTER TABLE documentation can be found
-         * <a href="http://sqlite.org/lang_altertable.html">here</a>. If you add new columns
-         * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
-         * you can use ALTER TABLE to rename the old table, then create the new table and then
-         * populate the new table with the contents of the old table.
-         * </p><p>
-         * This method executes within a transaction.  If an exception is thrown, all changes
-         * will automatically be rolled back.
-         * </p>
-         *
-         * @param db         The database.
-         * @param oldVersion The old database version.
-         * @param newVersion The new database version.
-         */
-        public abstract void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion,
-                int newVersion);
-
-        /**
-         * Called when the database needs to be downgraded. This is strictly similar to
-         * {@link #onUpgrade} method, but is called whenever current version is newer than requested
-         * one.
-         * However, this method is not abstract, so it is not mandatory for a customer to
-         * implement it. If not overridden, default implementation will reject downgrade and
-         * throws SQLiteException
-         *
-         * <p>
-         * This method executes within a transaction.  If an exception is thrown, all changes
-         * will automatically be rolled back.
-         * </p>
-         *
-         * @param db         The database.
-         * @param oldVersion The old database version.
-         * @param newVersion The new database version.
-         */
-        public void onDowngrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) {
-            throw new SQLiteException("Can't downgrade database from version "
-                    + oldVersion + " to " + newVersion);
-        }
-
-        /**
-         * Called when the database has been opened.  The implementation
-         * should check {@link SupportSQLiteDatabase#isReadOnly} before updating the
-         * database.
-         * <p>
-         * This method is called after the database connection has been configured
-         * and after the database schema has been created, upgraded or downgraded as necessary.
-         * If the database connection must be configured in some way before the schema
-         * is created, upgraded, or downgraded, do it in {@link #onConfigure} instead.
-         * </p>
-         *
-         * @param db The database.
-         */
-        public void onOpen(@NonNull SupportSQLiteDatabase db) {
-
-        }
-
-        /**
-         * The method invoked when database corruption is detected. Default implementation will
-         * delete the database file.
-         *
-         * @param db the {@link SupportSQLiteDatabase} object representing the database on which
-         *           corruption is detected.
-         */
-        public void onCorruption(@NonNull SupportSQLiteDatabase db) {
-            // the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
-
-            Log.e(TAG, "Corruption reported by sqlite on database: " + db.getPath());
-            // is the corruption detected even before database could be 'opened'?
-            if (!db.isOpen()) {
-                // database files are not even openable. delete this database file.
-                // NOTE if the database has attached databases, then any of them could be corrupt.
-                // and not deleting all of them could cause corrupted database file to remain and
-                // make the application crash on database open operation. To avoid this problem,
-                // the application should provide its own {@link DatabaseErrorHandler} impl class
-                // to delete ALL files of the database (including the attached databases).
-                deleteDatabaseFile(db.getPath());
-                return;
-            }
-
-            List<Pair<String, String>> attachedDbs = null;
-            try {
-                // Close the database, which will cause subsequent operations to fail.
-                // before that, get the attached database list first.
-                try {
-                    attachedDbs = db.getAttachedDbs();
-                } catch (SQLiteException e) {
-                /* ignore */
-                }
-                try {
-                    db.close();
-                } catch (IOException e) {
-                /* ignore */
-                }
-            } finally {
-                // Delete all files of this corrupt database and/or attached databases
-                if (attachedDbs != null) {
-                    for (Pair<String, String> p : attachedDbs) {
-                        deleteDatabaseFile(p.second);
-                    }
-                } else {
-                    // attachedDbs = null is possible when the database is so corrupt that even
-                    // "PRAGMA database_list;" also fails. delete the main database file
-                    deleteDatabaseFile(db.getPath());
-                }
-            }
-        }
-
-        private void deleteDatabaseFile(String fileName) {
-            if (fileName.equalsIgnoreCase(":memory:") || fileName.trim().length() == 0) {
-                return;
-            }
-            Log.w(TAG, "deleting the database file: " + fileName);
-            try {
-                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
-                    SupportSQLiteCompat.Api16Impl.deleteDatabase(new File(fileName));
-                } else {
-                    try {
-                        final boolean deleted = new File(fileName).delete();
-                        if (!deleted) {
-                            Log.e(TAG, "Could not delete the database file " + fileName);
-                        }
-                    } catch (Exception error) {
-                        Log.e(TAG, "error while deleting corrupted database file", error);
-                    }
-                }
-            } catch (Exception e) {
-            /* print warning and ignore exception */
-                Log.w(TAG, "delete failed: ", e);
-            }
-        }
-    }
-
-    /**
-     * The configuration to create an SQLite open helper object using {@link Factory}.
-     */
-    class Configuration {
-        /**
-         * Context to use to open or create the database.
-         */
-        @NonNull
-        public final Context context;
-        /**
-         * Name of the database file, or null for an in-memory database.
-         */
-        @Nullable
-        public final String name;
-        /**
-         * The callback class to handle creation, upgrade and downgrade.
-         */
-        @NonNull
-        public final SupportSQLiteOpenHelper.Callback callback;
-        /**
-         * If {@code true} the database will be stored in the no-backup directory.
-         */
-        public final boolean useNoBackupDirectory;
-        /**
-         * If {@code true} the database will be delete and its data loss in the case that it
-         * cannot be opened.
-         */
-        public final boolean allowDataLossOnRecovery;
-
-        Configuration(
-                @NonNull Context context,
-                @Nullable String name,
-                @NonNull Callback callback) {
-            this(context, name, callback, false);
-        }
-
-        Configuration(
-                @NonNull Context context,
-                @Nullable String name,
-                @NonNull Callback callback,
-                boolean useNoBackupDirectory) {
-            this(context, name, callback, useNoBackupDirectory, false);
-        }
-
-        Configuration(
-                @NonNull Context context,
-                @Nullable String name,
-                @NonNull Callback callback,
-                boolean useNoBackupDirectory,
-                boolean allowDataLossOnRecovery) {
-            this.context = context;
-            this.name = name;
-            this.callback = callback;
-            this.useNoBackupDirectory = useNoBackupDirectory;
-            this.allowDataLossOnRecovery = allowDataLossOnRecovery;
-        }
-
-        /**
-         * Creates a new Configuration.Builder to create an instance of Configuration.
-         *
-         * @param context to use to open or create the database.
-         */
-        @NonNull
-        public static Builder builder(@NonNull Context context) {
-            return new Builder(context);
-        }
-
-        /**
-         * Builder class for {@link Configuration}.
-         */
-        public static class Builder {
-            Context mContext;
-            String mName;
-            SupportSQLiteOpenHelper.Callback mCallback;
-            boolean mUseNoBackupDirectory;
-            boolean mAllowDataLossOnRecovery;
-
-            /**
-             * <p>
-             * Throws an {@link IllegalArgumentException} if the {@link Callback} is {@code null}.
-             * <p>
-             * Throws an {@link IllegalArgumentException} if the {@link Context} is {@code null}.
-             * <p>
-             * Throws an {@link IllegalArgumentException} if the {@link String} database
-             * name is {@code null}. {@see Context#getNoBackupFilesDir()}
-             *
-             * @return The {@link Configuration} instance
-             */
-            @NonNull
-            public Configuration build() {
-                if (mCallback == null) {
-                    throw new IllegalArgumentException("Must set a callback to create the"
-                            + " configuration.");
-                }
-                if (mContext == null) {
-                    throw new IllegalArgumentException("Must set a non-null context to create"
-                            + " the configuration.");
-                }
-                if (mUseNoBackupDirectory && TextUtils.isEmpty(mName)) {
-                    throw new IllegalArgumentException(
-                            "Must set a non-null database name to a configuration that uses the "
-                                    + "no backup directory.");
-                }
-                return new Configuration(mContext, mName, mCallback, mUseNoBackupDirectory,
-                        mAllowDataLossOnRecovery);
-            }
-
-            Builder(@NonNull Context context) {
-                mContext = context;
-            }
-
-            /**
-             * @param name Name of the database file, or null for an in-memory database.
-             * @return This
-             */
-            @NonNull
-            public Builder name(@Nullable String name) {
-                mName = name;
-                return this;
-            }
-
-            /**
-             * @param callback The callback class to handle creation, upgrade and downgrade.
-             * @return this
-             */
-            @NonNull
-            public Builder callback(@NonNull Callback callback) {
-                mCallback = callback;
-                return this;
-            }
-
-            /**
-             * Sets whether to use a no backup directory or not.
-             * @param useNoBackupDirectory If {@code true} the database file will be stored in the
-             *                             no-backup directory.
-             * @return this
-             */
-            @NonNull
-            public Builder noBackupDirectory(boolean useNoBackupDirectory) {
-                mUseNoBackupDirectory = useNoBackupDirectory;
-                return this;
-            }
-
-            /**
-             * Sets whether to delete and recreate the database file in situations when the
-             * database file cannot be opened, thus allowing for its data to be lost.
-             * @param allowDataLossOnRecovery If {@code true} the database file might be recreated
-             *                                in the case that it cannot be opened.
-             * @return this
-             */
-            @NonNull
-            public Builder allowDataLossOnRecovery(boolean allowDataLossOnRecovery) {
-                mAllowDataLossOnRecovery = allowDataLossOnRecovery;
-                return this;
-            }
-        }
-    }
-
-    /**
-     * Factory class to create instances of {@link SupportSQLiteOpenHelper} using
-     * {@link Configuration}.
-     */
-    interface Factory {
-        /**
-         * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.
-         *
-         * @param configuration The configuration to use while creating the open helper.
-         *
-         * @return A SupportSQLiteOpenHelper which can be used to open a database.
-         */
-        @NonNull
-        SupportSQLiteOpenHelper create(@NonNull Configuration configuration);
-    }
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt
new file mode 100644
index 0000000..a2ca95f
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteOpenHelper.kt
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.sqlite.db
+
+import android.content.Context
+import android.database.sqlite.SQLiteException
+import android.os.Build
+import android.util.Log
+import android.util.Pair
+import androidx.annotation.RequiresApi
+import androidx.sqlite.db.SupportSQLiteCompat.Api16Impl.deleteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback
+import androidx.sqlite.db.SupportSQLiteOpenHelper.Factory
+import java.io.Closeable
+import java.io.File
+import java.io.IOException
+
+/**
+ * An interface to map the behavior of [android.database.sqlite.SQLiteOpenHelper].
+ * Note that since that class requires overriding certain methods, support implementation
+ * uses [Factory.create] to create this and [Callback] to implement
+ * the methods that should be overridden.
+ */
+interface SupportSQLiteOpenHelper : Closeable {
+    /**
+     * Return the name of the SQLite database being opened, as given to
+     * the constructor. `null` indicates an in-memory database.
+     */
+    val databaseName: String?
+
+    /**
+     * Enables or disables the use of write-ahead logging for the database.
+     *
+     * See [SupportSQLiteDatabase.enableWriteAheadLogging] for details.
+     *
+     * Write-ahead logging cannot be used with read-only databases so the value of
+     * this flag is ignored if the database is opened read-only.
+     *
+     * @param enabled True if write-ahead logging should be enabled, false if it
+     * should be disabled.
+     */
+    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
+    fun setWriteAheadLoggingEnabled(enabled: Boolean)
+
+    /**
+     * Create and/or open a database that will be used for reading and writing.
+     * The first time this is called, the database will be opened and
+     * [Callback.onCreate], [Callback.onUpgrade] and/or [Callback.onOpen] will be
+     * called.
+     *
+     * Once opened successfully, the database is cached, so you can
+     * call this method every time you need to write to the database.
+     * (Make sure to call [close] when you no longer need the database.)
+     * Errors such as bad permissions or a full disk may cause this method
+     * to fail, but future attempts may succeed if the problem is fixed.
+     *
+     * Database upgrade may take a long time, you
+     * should not call this method from the application main thread, including
+     * from [ContentProvider.onCreate()].
+     *
+     * @return a read/write database object valid until [close] is called
+     * @throws SQLiteException if the database cannot be opened for writing
+     */
+    val writableDatabase: SupportSQLiteDatabase
+
+    /**
+     * Create and/or open a database.  This will be the same object returned by
+     * [writableDatabase] unless some problem, such as a full disk,
+     * requires the database to be opened read-only.  In that case, a read-only
+     * database object will be returned.  If the problem is fixed, a future call
+     * to [writableDatabase] may succeed, in which case the read-only
+     * database object will be closed and the read/write object will be returned
+     * in the future.
+     *
+     * Like [writableDatabase], this method may
+     * take a long time to return, so you should not call it from the
+     * application main thread, including from
+     * [ContentProvider.onCreate()].
+     *
+     * @return a database object valid until [writableDatabase]
+     * or [close] is called.
+     * @throws SQLiteException if the database cannot be opened
+     */
+    val readableDatabase: SupportSQLiteDatabase
+
+    /**
+     * Close any open database object.
+     */
+    override fun close()
+
+    /**
+     * Creates a new Callback to get database lifecycle events.
+     *
+     * Handles various lifecycle events for the SQLite connection, similar to
+     * [room-runtime.SQLiteOpenHelper].
+     */
+    abstract class Callback(
+        /**
+         * Version number of the database (starting at 1); if the database is older,
+         * [Callback.onUpgrade] will be used to upgrade the database; if the database is newer,
+         * [Callback.onDowngrade] will be used to downgrade the database.
+         */
+        @JvmField
+        val version: Int
+    ) {
+        /**
+         * Called when the database connection is being configured, to enable features such as
+         * write-ahead logging or foreign key support.
+         *
+         * This method is called before [onCreate], [onUpgrade], [onDowngrade],
+         * or [onOpen] are called. It should not modify the database except to configure the
+         * database connection as required.
+         *
+         * This method should only call methods that configure the parameters of the database
+         * connection, such as [SupportSQLiteDatabase.enableWriteAheadLogging]
+         * [SupportSQLiteDatabase.setForeignKeyConstraintsEnabled],
+         * [SupportSQLiteDatabase.setLocale],
+         * [SupportSQLiteDatabase.setMaximumSize], or executing PRAGMA statements.
+         *
+         * @param db The database.
+         */
+        open fun onConfigure(db: SupportSQLiteDatabase) {}
+
+        /**
+         * Called when the database is created for the first time. This is where the
+         * creation of tables and the initial population of the tables should happen.
+         *
+         * @param db The database.
+         */
+        abstract fun onCreate(db: SupportSQLiteDatabase)
+
+        /**
+         * Called when the database needs to be upgraded. The implementation
+         * should use this method to drop tables, add tables, or do anything else it
+         * needs to upgrade to the new schema version.
+         *
+         * The SQLite ALTER TABLE documentation can be found
+         * [here](http://sqlite.org/lang_altertable.html). If you add new columns
+         * you can use ALTER TABLE to insert them into a live table. If you rename or remove columns
+         * you can use ALTER TABLE to rename the old table, then create the new table and then
+         * populate the new table with the contents of the old table.
+         *
+         * This method executes within a transaction.  If an exception is thrown, all changes
+         * will automatically be rolled back.
+         *
+         * @param db         The database.
+         * @param oldVersion The old database version.
+         * @param newVersion The new database version.
+         */
+        abstract fun onUpgrade(
+            db: SupportSQLiteDatabase,
+            oldVersion: Int,
+            newVersion: Int
+        )
+
+        /**
+         * Called when the database needs to be downgraded. This is strictly similar to
+         * [onUpgrade] method, but is called whenever current version is newer than requested
+         * one.
+         * However, this method is not abstract, so it is not mandatory for a customer to
+         * implement it. If not overridden, default implementation will reject downgrade and
+         * throws SQLiteException
+         *
+         * This method executes within a transaction.  If an exception is thrown, all changes
+         * will automatically be rolled back.
+         *
+         * @param db         The database.
+         * @param oldVersion The old database version.
+         * @param newVersion The new database version.
+         */
+        open fun onDowngrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
+            throw SQLiteException(
+                "Can't downgrade database from version $oldVersion to $newVersion"
+            )
+        }
+
+        /**
+         * Called when the database has been opened.  The implementation
+         * should check [SupportSQLiteDatabase.isReadOnly] before updating the
+         * database.
+         *
+         * This method is called after the database connection has been configured
+         * and after the database schema has been created, upgraded or downgraded as necessary.
+         * If the database connection must be configured in some way before the schema
+         * is created, upgraded, or downgraded, do it in [onConfigure] instead.
+         *
+         * @param db The database.
+         */
+        open fun onOpen(db: SupportSQLiteDatabase) {}
+
+        /**
+         * The method invoked when database corruption is detected. Default implementation will
+         * delete the database file.
+         *
+         * @param db the [SupportSQLiteDatabase] object representing the database on which
+         * corruption is detected.
+         */
+        open fun onCorruption(db: SupportSQLiteDatabase) {
+            // the following implementation is taken from {@link DefaultDatabaseErrorHandler}.
+            Log.e(TAG, "Corruption reported by sqlite on database: $db.path")
+            // is the corruption detected even before database could be 'opened'?
+            if (!db.isOpen) {
+                // database files are not even openable. delete this database file.
+                // NOTE if the database has attached databases, then any of them could be corrupt.
+                // and not deleting all of them could cause corrupted database file to remain and
+                // make the application crash on database open operation. To avoid this problem,
+                // the application should provide its own {@link DatabaseErrorHandler} impl class
+                // to delete ALL files of the database (including the attached databases).
+                db.path?.let { deleteDatabaseFile(it) }
+                return
+            }
+            var attachedDbs: List<Pair<String, String>>? = null
+            try {
+                // Close the database, which will cause subsequent operations to fail.
+                // before that, get the attached database list first.
+                try {
+                    attachedDbs = db.attachedDbs
+                } catch (e: SQLiteException) {
+                    /* ignore */
+                }
+                try {
+                    db.close()
+                } catch (e: IOException) {
+                    /* ignore */
+                }
+            } finally {
+                // Delete all files of this corrupt database and/or attached databases
+                // attachedDbs = null is possible when the database is so corrupt that even
+                // "PRAGMA database_list;" also fails. delete the main database file
+                attachedDbs?.forEach { p ->
+                    deleteDatabaseFile(p.second)
+                } ?: db.path?.let { deleteDatabaseFile(it) }
+            }
+        }
+
+        private fun deleteDatabaseFile(fileName: String) {
+            if (fileName.equals(":memory:", ignoreCase = true) ||
+                fileName.trim { it <= ' ' }.isEmpty()
+            ) {
+                return
+            }
+            Log.w(TAG, "deleting the database file: $fileName")
+            try {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+                    deleteDatabase(File(fileName))
+                } else {
+                    try {
+                        val deleted = File(fileName).delete()
+                        if (!deleted) {
+                            Log.e(TAG, "Could not delete the database file $fileName")
+                        }
+                    } catch (error: Exception) {
+                        Log.e(TAG, "error while deleting corrupted database file", error)
+                    }
+                }
+            } catch (e: Exception) {
+                /* print warning and ignore exception */
+                Log.w(TAG, "delete failed: ", e)
+            }
+        }
+
+        internal companion object {
+            private const val TAG = "SupportSQLite"
+        }
+    }
+
+    /**
+     * The configuration to create an SQLite open helper object using [Factory].
+     */
+    class Configuration
+    @Suppress("ExecutorRegistration") // For backwards compatibility
+    constructor(
+        /**
+         * Context to use to open or create the database.
+         */
+        @JvmField
+        val context: Context,
+        /**
+         * Name of the database file, or null for an in-memory database.
+         */
+        @JvmField
+        val name: String?,
+        /**
+         * The callback class to handle creation, upgrade and downgrade.
+         */
+        @JvmField
+        val callback: Callback,
+        /**
+         * If `true` the database will be stored in the no-backup directory.
+         */
+        @JvmField
+        @Suppress("ListenerLast")
+        val useNoBackupDirectory: Boolean = false,
+        /**
+         * If `true` the database will be delete and its data loss in the case that it
+         * cannot be opened.
+         */
+        @JvmField
+        @Suppress("ListenerLast")
+        val allowDataLossOnRecovery: Boolean = false
+    ) {
+
+        /**
+         * Builder class for [Configuration].
+         */
+        open class Builder internal constructor(context: Context) {
+            private val context: Context
+            private var name: String? = null
+            private var callback: Callback? = null
+            private var useNoBackupDirectory = false
+            private var allowDataLossOnRecovery = false
+
+            /**
+             * Throws an [IllegalArgumentException] if the [Callback] is `null`.
+             *
+             * Throws an [IllegalArgumentException] if the [Context] is `null`.
+             *
+             * Throws an [IllegalArgumentException] if the [String] database
+             * name is `null`. [Context.getNoBackupFilesDir]
+             *
+             * @return The [Configuration] instance
+             */
+            open fun build(): Configuration {
+                val callback = callback
+                requireNotNull(callback) {
+                    "Must set a callback to create the configuration."
+                }
+                require(!useNoBackupDirectory || !name.isNullOrEmpty()) {
+                    "Must set a non-null database name to a configuration that uses the " +
+                        "no backup directory."
+                }
+                return Configuration(
+                    context,
+                    name,
+                    callback,
+                    useNoBackupDirectory,
+                    allowDataLossOnRecovery
+                )
+            }
+
+            init {
+                this.context = context
+            }
+
+            /**
+             * @param name Name of the database file, or null for an in-memory database.
+             * @return This builder instance.
+             */
+            open fun name(name: String?): Builder = apply {
+                this.name = name
+            }
+
+            /**
+             * @param callback The callback class to handle creation, upgrade and downgrade.
+             * @return This builder instance.
+             */
+            open fun callback(callback: Callback): Builder = apply {
+                this.callback = callback
+            }
+
+            /**
+             * Sets whether to use a no backup directory or not.
+             *
+             * @param useNoBackupDirectory If `true` the database file will be stored in the
+             * no-backup directory.
+             * @return This builder instance.
+             */
+            open fun noBackupDirectory(useNoBackupDirectory: Boolean): Builder = apply {
+                this.useNoBackupDirectory = useNoBackupDirectory
+            }
+
+            /**
+             * Sets whether to delete and recreate the database file in situations when the
+             * database file cannot be opened, thus allowing for its data to be lost.
+             *
+             * @param allowDataLossOnRecovery If `true` the database file might be recreated
+             * in the case that it cannot be opened.
+             * @return this
+             */
+            open fun allowDataLossOnRecovery(allowDataLossOnRecovery: Boolean): Builder = apply {
+                this.allowDataLossOnRecovery = allowDataLossOnRecovery
+            }
+        }
+
+        companion object {
+            /**
+             * Creates a new Configuration.Builder to create an instance of Configuration.
+             *
+             * @param context to use to open or create the database.
+             */
+            @JvmStatic
+            fun builder(context: Context): Builder {
+                return Builder(context)
+            }
+        }
+    }
+
+    /**
+     * Factory class to create instances of [SupportSQLiteOpenHelper] using
+     * [Configuration].
+     */
+    fun interface Factory {
+        /**
+         * Creates an instance of [SupportSQLiteOpenHelper] using the given configuration.
+         *
+         * @param configuration The configuration to use while creating the open helper.
+         *
+         * @return A SupportSQLiteOpenHelper which can be used to open a database.
+         */
+        fun create(configuration: Configuration): SupportSQLiteOpenHelper
+    }
+}
\ No newline at end of file
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java
deleted file mode 100644
index a438fa8..0000000
--- a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.sqlite.db;
-
-import java.util.regex.Pattern;
-
-/**
- * A simple query builder to create SQL SELECT queries.
- */
-@SuppressWarnings("unused")
-public final class SupportSQLiteQueryBuilder {
-    private static final Pattern sLimitPattern =
-            Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?");
-
-    private boolean mDistinct = false;
-    private final String mTable;
-    private String[] mColumns = null;
-    private String mSelection;
-    private Object[] mBindArgs;
-    private String mGroupBy = null;
-    private String mHaving = null;
-    private String mOrderBy = null;
-    private String mLimit = null;
-
-    /**
-     * Creates a query for the given table name.
-     *
-     * @param tableName The table name(s) to query.
-     *
-     * @return A builder to create a query.
-     */
-    public static SupportSQLiteQueryBuilder builder(String tableName) {
-        return new SupportSQLiteQueryBuilder(tableName);
-    }
-
-    private SupportSQLiteQueryBuilder(String table) {
-        mTable = table;
-    }
-
-    /**
-     * Adds DISTINCT keyword to the query.
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder distinct() {
-        mDistinct = true;
-        return this;
-    }
-
-    /**
-     * Sets the given list of columns as the columns that will be returned.
-     *
-     * @param columns The list of column names that should be returned.
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder columns(String[] columns) {
-        mColumns = columns;
-        return this;
-    }
-
-    /**
-     * Sets the arguments for the WHERE clause.
-     *
-     * @param selection The list of selection columns
-     * @param bindArgs The list of bind arguments to match against these columns
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder selection(String selection, Object[] bindArgs) {
-        mSelection = selection;
-        mBindArgs = bindArgs;
-        return this;
-    }
-
-    /**
-     * Adds a GROUP BY statement.
-     *
-     * @param groupBy The value of the GROUP BY statement.
-     *
-     * @return this
-     */
-    @SuppressWarnings("WeakerAccess")
-    public SupportSQLiteQueryBuilder groupBy(String groupBy) {
-        mGroupBy = groupBy;
-        return this;
-    }
-
-    /**
-     * Adds a HAVING statement. You must also provide {@link #groupBy(String)} for this to work.
-     *
-     * @param having The having clause.
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder having(String having) {
-        mHaving = having;
-        return this;
-    }
-
-    /**
-     * Adds an ORDER BY statement.
-     *
-     * @param orderBy The order clause.
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder orderBy(String orderBy) {
-        mOrderBy = orderBy;
-        return this;
-    }
-
-    /**
-     * Adds a LIMIT statement.
-     *
-     * @param limit The limit value.
-     *
-     * @return this
-     */
-    public SupportSQLiteQueryBuilder limit(String limit) {
-        if (!isEmpty(limit) && !sLimitPattern.matcher(limit).matches()) {
-            throw new IllegalArgumentException("invalid LIMIT clauses:" + limit);
-        }
-        mLimit = limit;
-        return this;
-    }
-
-    /**
-     * Creates the {@link SupportSQLiteQuery} that can be passed into
-     * {@link SupportSQLiteDatabase#query(SupportSQLiteQuery)}.
-     *
-     * @return a new query
-     */
-    public SupportSQLiteQuery create() {
-        if (isEmpty(mGroupBy) && !isEmpty(mHaving)) {
-            throw new IllegalArgumentException(
-                    "HAVING clauses are only permitted when using a groupBy clause");
-        }
-        StringBuilder query = new StringBuilder(120);
-
-        query.append("SELECT ");
-        if (mDistinct) {
-            query.append("DISTINCT ");
-        }
-        if (mColumns != null && mColumns.length != 0) {
-            appendColumns(query, mColumns);
-        } else {
-            query.append(" * ");
-        }
-        query.append(" FROM ");
-        query.append(mTable);
-        appendClause(query, " WHERE ", mSelection);
-        appendClause(query, " GROUP BY ", mGroupBy);
-        appendClause(query, " HAVING ", mHaving);
-        appendClause(query, " ORDER BY ", mOrderBy);
-        appendClause(query, " LIMIT ", mLimit);
-
-        return new SimpleSQLiteQuery(query.toString(), mBindArgs);
-    }
-
-    private static void appendClause(StringBuilder s, String name, String clause) {
-        if (!isEmpty(clause)) {
-            s.append(name);
-            s.append(clause);
-        }
-    }
-
-    /**
-     * Add the names that are non-null in columns to s, separating
-     * them with commas.
-     */
-    private static void appendColumns(StringBuilder s, String[] columns) {
-        int n = columns.length;
-
-        for (int i = 0; i < n; i++) {
-            String column = columns[i];
-            if (i > 0) {
-                s.append(", ");
-            }
-            s.append(column);
-        }
-        s.append(' ');
-    }
-
-    private static boolean isEmpty(String input) {
-        return input == null || input.length() == 0;
-    }
-}
diff --git a/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt
new file mode 100644
index 0000000..9bf6265
--- /dev/null
+++ b/sqlite/sqlite/src/main/java/androidx/sqlite/db/SupportSQLiteQueryBuilder.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.sqlite.db
+
+import java.util.regex.Pattern
+
+/**
+ * A simple query builder to create SQL SELECT queries.
+ */
+class SupportSQLiteQueryBuilder private constructor(private val table: String) {
+    private var distinct = false
+    private var columns: Array<String>? = null
+    private var selection: String? = null
+    private var bindArgs: Array<Any?>? = null
+    private var groupBy: String? = null
+    private var having: String? = null
+    private var orderBy: String? = null
+    private var limit: String? = null
+
+    /**
+     * Adds DISTINCT keyword to the query.
+     *
+     * @return this
+     */
+    fun distinct(): SupportSQLiteQueryBuilder = apply {
+        this.distinct = true
+    }
+
+    /**
+     * Sets the given list of columns as the columns that will be returned.
+     *
+     * @param columns The list of column names that should be returned.
+     *
+     * @return this
+     */
+    fun columns(columns: Array<String>?): SupportSQLiteQueryBuilder = apply {
+        this.columns = columns
+    }
+
+    /**
+     * Sets the arguments for the WHERE clause.
+     *
+     * @param selection The list of selection columns
+     * @param bindArgs The list of bind arguments to match against these columns
+     *
+     * @return this
+     */
+    fun selection(selection: String?, bindArgs: Array<Any?>?): SupportSQLiteQueryBuilder = apply {
+        this.selection = selection
+        this.bindArgs = bindArgs
+    }
+
+    /**
+     * Adds a GROUP BY statement.
+     *
+     * @param groupBy The value of the GROUP BY statement.
+     *
+     * @return this
+     */
+    fun groupBy(groupBy: String?): SupportSQLiteQueryBuilder = apply {
+        this.groupBy = groupBy
+    }
+
+    /**
+     * Adds a HAVING statement. You must also provide [groupBy] for this to work.
+     *
+     * @param having The having clause.
+     *
+     * @return this
+     */
+    fun having(having: String?): SupportSQLiteQueryBuilder = apply {
+        this.having = having
+    }
+
+    /**
+     * Adds an ORDER BY statement.
+     *
+     * @param orderBy The order clause.
+     *
+     * @return this
+     */
+    fun orderBy(orderBy: String?): SupportSQLiteQueryBuilder = apply {
+        this.orderBy = orderBy
+    }
+
+    /**
+     * Adds a LIMIT statement.
+     *
+     * @param limit The limit value.
+     *
+     * @return this
+     */
+    fun limit(limit: String): SupportSQLiteQueryBuilder = apply {
+        val patternMatches = limitPattern.matcher(
+            limit
+        ).matches()
+        require(limit.isEmpty() || patternMatches) { "invalid LIMIT clauses:$limit" }
+        this.limit = limit
+    }
+
+    /**
+     * Creates the [SupportSQLiteQuery] that can be passed into
+     * [SupportSQLiteDatabase.query].
+     *
+     * @return a new query
+     */
+    fun create(): SupportSQLiteQuery {
+        require(groupBy?.isNotEmpty() == true || having?.isEmpty() == true) {
+            "HAVING clauses are only permitted when using a groupBy clause"
+        }
+        val query = buildString(120) {
+            append("SELECT ")
+            if (distinct) {
+                append("DISTINCT ")
+            }
+            if (columns?.size != 0) {
+                appendColumns(columns!!)
+            } else {
+                append(" * ")
+            }
+            append(" FROM ")
+            append(table)
+            appendClause(" WHERE ", selection)
+            appendClause(" GROUP BY ", groupBy)
+            appendClause(" HAVING ", having)
+            appendClause(" ORDER BY ", orderBy)
+            appendClause(" LIMIT ", limit)
+        }
+        return SimpleSQLiteQuery(query, bindArgs)
+    }
+
+    private fun StringBuilder.appendClause(name: String, clause: String?) {
+        if (clause?.isNotEmpty() == true) {
+            append(name)
+            append(clause)
+        }
+    }
+
+    /**
+     * Add the names that are non-null in columns to string, separating
+     * them with commas.
+     */
+    private fun StringBuilder.appendColumns(columns: Array<String>) {
+        val n = columns.size
+        for (i in 0 until n) {
+            val column = columns[i]
+            if (i > 0) {
+                append(", ")
+            }
+            append(column)
+        }
+        append(' ')
+    }
+
+    companion object {
+        private val limitPattern = Pattern.compile("\\s*\\d+\\s*(,\\s*\\d+\\s*)?")
+
+        /**
+         * Creates a query for the given table name.
+         *
+         * @param tableName The table name(s) to query.
+         *
+         * @return A builder to create a query.
+         */
+        @JvmStatic
+        fun builder(tableName: String): SupportSQLiteQueryBuilder {
+            return SupportSQLiteQueryBuilder(tableName)
+        }
+    }
+}
\ No newline at end of file
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index 7f42487..a50f706 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -26,7 +26,9 @@
 import android.widget.TextView;
 
 import androidx.test.filters.LargeTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.UiDevice;
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
@@ -110,20 +112,18 @@
         assertNotNull(mDevice.findObject(By.res(TEST_APP, "nested_elements")));
     }
 
+    @Test
+    public void testGetInstance() {
+        assertEquals(mDevice, UiDevice.getInstance());
+    }
+
+    @Test
+    public void testGetInstance_withInstrumentation() {
+        assertEquals(mDevice, UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()));
+    }
+
     /* TODO(b/235841020): Implement these tests, and the tests for exceptions of each tested method.
 
-    public void testGetInstance() {}
-
-    public void testGetInstance_withInstrumentation() {}
-
-    public void testGetDisplaySizeDp() {}
-
-    public void testGetProductName() {}
-
-    public void testGetLastTraversedText() {}
-
-    public void testClearLastTraversedText() {}
-
     public void testPressMenu() {}
 
     public void testPressBack() {}
diff --git a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
index a619406..5d46ca9 100644
--- a/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
+++ b/test/uiautomator/uiautomator/src/androidTest/java/androidx/test/uiautomator/UiDeviceTest.java
@@ -28,9 +28,12 @@
 
 import android.app.Instrumentation;
 import android.app.UiAutomation;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
 
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -71,6 +74,21 @@
     }
 
     @Test
+    public void testGetDisplaySizeDp() {
+        DisplayMetrics dm = new DisplayMetrics();
+        WindowManager wm = (WindowManager) mDevice.getUiContext().getSystemService(
+                Context.WINDOW_SERVICE);
+        wm.getDefaultDisplay().getRealMetrics(dm);
+        assertEquals(Math.round(dm.widthPixels / dm.density), mDevice.getDisplaySizeDp().x);
+        assertEquals(Math.round(dm.heightPixels / dm.density), mDevice.getDisplaySizeDp().y);
+    }
+
+    @Test
+    public void testGetProductName() {
+        assertEquals(Build.PRODUCT, mDevice.getProductName());
+    }
+
+    @Test
     @SdkSuppress(maxSdkVersion = Build.VERSION_CODES.M)
     public void testGetUiAutomation_withoutFlags() {
         mDevice.getUiAutomation();
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt
index 92d10dc..8086ac4 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/LayoutCompat.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.text.android
 
+import android.graphics.text.LineBreakConfig
 import android.graphics.text.LineBreaker
 import android.text.Layout
 import android.text.Layout.Alignment
@@ -79,6 +80,30 @@
     )
     internal annotation class BreakStrategy
 
+    const val LINE_BREAK_STYLE_NONE = LineBreakConfig.LINE_BREAK_STYLE_NONE
+    const val LINE_BREAK_STYLE_LOOSE = LineBreakConfig.LINE_BREAK_STYLE_LOOSE
+    const val LINE_BREAK_STYLE_NORMAL = LineBreakConfig.LINE_BREAK_STYLE_NORMAL
+    const val LINE_BREAK_STYLE_STRICT = LineBreakConfig.LINE_BREAK_STYLE_STRICT
+
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(
+        LINE_BREAK_STYLE_NONE,
+        LINE_BREAK_STYLE_LOOSE,
+        LINE_BREAK_STYLE_NORMAL,
+        LINE_BREAK_STYLE_STRICT
+    )
+    internal annotation class LineBreakStyle
+
+    const val LINE_BREAK_WORD_STYLE_NONE = LineBreakConfig.LINE_BREAK_WORD_STYLE_NONE
+    const val LINE_BREAK_WORD_STYLE_PHRASE = LineBreakConfig.LINE_BREAK_WORD_STYLE_PHRASE
+
+    @Retention(AnnotationRetention.SOURCE)
+    @IntDef(
+        LINE_BREAK_WORD_STYLE_NONE,
+        LINE_BREAK_WORD_STYLE_PHRASE
+    )
+    internal annotation class LineBreakWordStyle
+
     const val TEXT_DIRECTION_LTR = 0
     const val TEXT_DIRECTION_RTL = 1
     const val TEXT_DIRECTION_FIRST_STRONG_LTR = 2
@@ -111,6 +136,10 @@
 
     internal const val DEFAULT_BREAK_STRATEGY = BREAK_STRATEGY_SIMPLE
 
+    internal const val DEFAULT_LINE_BREAK_STYLE = LINE_BREAK_STYLE_NONE
+
+    internal const val DEFAULT_LINE_BREAK_WORD_STYLE = LINE_BREAK_WORD_STYLE_NONE
+
     internal const val DEFAULT_HYPHENATION_FREQUENCY = HYPHENATION_FREQUENCY_NONE
 
     const val DEFAULT_JUSTIFICATION_MODE = JUSTIFICATION_MODE_NONE
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
index 53d17d4..821635c 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/StaticLayoutFactory.kt
@@ -15,6 +15,7 @@
  */
 package androidx.compose.ui.text.android
 
+import android.graphics.text.LineBreakConfig
 import android.os.Build
 import android.text.Layout.Alignment
 import android.text.StaticLayout
@@ -30,6 +31,8 @@
 import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
 import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
 import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
+import androidx.compose.ui.text.android.LayoutCompat.LineBreakStyle
+import androidx.compose.ui.text.android.LayoutCompat.LineBreakWordStyle
 import androidx.core.os.BuildCompat
 import java.lang.reflect.Constructor
 import java.lang.reflect.InvocationTargetException
@@ -70,6 +73,10 @@
         useFallbackLineSpacing: Boolean = LayoutCompat.DEFAULT_FALLBACK_LINE_SPACING,
         @BreakStrategy
         breakStrategy: Int = LayoutCompat.DEFAULT_BREAK_STRATEGY,
+        @LineBreakStyle
+        lineBreakStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_STYLE,
+        @LineBreakWordStyle
+        lineBreakWordStyle: Int = LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE,
         @HyphenationFrequency
         hyphenationFrequency: Int = LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY,
         leftIndents: IntArray? = null,
@@ -93,6 +100,8 @@
                 includePadding = includePadding,
                 useFallbackLineSpacing = useFallbackLineSpacing,
                 breakStrategy = breakStrategy,
+                lineBreakStyle = lineBreakStyle,
+                lineBreakWordStyle = lineBreakWordStyle,
                 hyphenationFrequency = hyphenationFrequency,
                 leftIndents = leftIndents,
                 rightIndents = rightIndents
@@ -133,6 +142,8 @@
     val includePadding: Boolean,
     val useFallbackLineSpacing: Boolean,
     val breakStrategy: Int,
+    val lineBreakStyle: Int,
+    val lineBreakWordStyle: Int,
     val hyphenationFrequency: Int,
     val leftIndents: IntArray?,
     val rightIndents: IntArray?
@@ -181,6 +192,13 @@
                         params.useFallbackLineSpacing
                     )
                 }
+                if (Build.VERSION.SDK_INT >= 33) {
+                    StaticLayoutFactory33.setLineBreakConfig(
+                        this,
+                        params.lineBreakStyle,
+                        params.lineBreakWordStyle
+                    )
+                }
             }.build()
     }
 
@@ -224,6 +242,17 @@
     fun isFallbackLineSpacingEnabled(layout: StaticLayout): Boolean {
         return layout.isFallbackLineSpacingEnabled
     }
+
+    @JvmStatic
+    @DoNotInline
+    fun setLineBreakConfig(builder: Builder, lineBreakStyle: Int, lineBreakWordStyle: Int) {
+        val lineBreakConfig =
+            LineBreakConfig.Builder()
+                .setLineBreakStyle(lineBreakStyle)
+                .setLineBreakWordStyle(lineBreakWordStyle)
+                .build()
+        builder.setLineBreakConfig(lineBreakConfig)
+    }
 }
 
 private class StaticLayoutFactoryDefault : StaticLayoutFactoryImpl {
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
index 1a98308..eb3382b 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/TextLayout.kt
@@ -37,6 +37,8 @@
 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_OPPOSITE
 import androidx.compose.ui.text.android.LayoutCompat.ALIGN_RIGHT
 import androidx.compose.ui.text.android.LayoutCompat.BreakStrategy
+import androidx.compose.ui.text.android.LayoutCompat.LineBreakStyle
+import androidx.compose.ui.text.android.LayoutCompat.LineBreakWordStyle
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_ALIGNMENT
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_BREAK_STRATEGY
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_HYPHENATION_FREQUENCY
@@ -44,6 +46,8 @@
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_JUSTIFICATION_MODE
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_EXTRA
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINESPACING_MULTIPLIER
+import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_STYLE
+import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_LINE_BREAK_WORD_STYLE
 import androidx.compose.ui.text.android.LayoutCompat.DEFAULT_TEXT_DIRECTION
 import androidx.compose.ui.text.android.LayoutCompat.HyphenationFrequency
 import androidx.compose.ui.text.android.LayoutCompat.JustificationMode
@@ -115,6 +119,8 @@
     val fallbackLineSpacing: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
     @BreakStrategy breakStrategy: Int = DEFAULT_BREAK_STRATEGY,
+    @LineBreakStyle lineBreakStyle: Int = DEFAULT_LINE_BREAK_STYLE,
+    @LineBreakWordStyle lineBreakWordStyle: Int = DEFAULT_LINE_BREAK_WORD_STYLE,
     @HyphenationFrequency hyphenationFrequency: Int = DEFAULT_HYPHENATION_FREQUENCY,
     @JustificationMode justificationMode: Int = DEFAULT_JUSTIFICATION_MODE,
     leftIndents: IntArray? = null,
@@ -260,6 +266,8 @@
                     includePadding = includePadding,
                     useFallbackLineSpacing = fallbackLineSpacing,
                     breakStrategy = breakStrategy,
+                    lineBreakStyle = lineBreakStyle,
+                    lineBreakWordStyle = lineBreakWordStyle,
                     hyphenationFrequency = hyphenationFrequency,
                     leftIndents = leftIndents,
                     rightIndents = rightIndents
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
index 45907d8..edc6150 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ScrollableWithPivot.kt
@@ -218,7 +218,10 @@
         return computeDestination(localRect, oldSize, pivotOffsets)
     }
 
-    override suspend fun bringChildIntoView(localRect: Rect) {
+    override suspend fun bringChildIntoView(localRect: () -> Rect?) {
+        // TODO(b/241591211) Read the request's bounds lazily in case they change.
+        @Suppress("NAME_SHADOWING")
+        val localRect = localRect() ?: return
         performBringIntoView(localRect, calculateRectForParent(localRect))
     }
 
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index f162f99..1ffbd26 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -28,13 +28,14 @@
 dependencies {
 
     if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        api("androidx.compose.foundation:foundation: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.runtime:runtime:1.3.0-rc01")
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
 
         implementation(libs.kotlinStdlib)
-        implementation("androidx.compose.foundation:foundation-layout:1.3.0-rc01")
+        implementation(project(":compose:foundation:foundation-layout"))
+        implementation(project(":compose:ui:ui-util"))
         implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         testImplementation(libs.testRules)
@@ -70,6 +71,7 @@
 
                 implementation(libs.kotlinStdlib)
                 implementation(project(":compose:foundation:foundation-layout"))
+                implementation(project(":compose:ui:ui-util"))
             }
             jvmMain.dependencies {
                 implementation(libs.kotlinStdlib)
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
index f155594..b1bb57909 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedComposable.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.util.lerp
 import kotlin.math.PI
 import kotlin.math.asin
 import kotlin.math.cos
diff --git a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
index 969cc47..de9cfff 100644
--- a/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
+++ b/wear/compose/compose-foundation/src/commonMain/kotlin/androidx/wear/compose/foundation/CurvedLayout.kt
@@ -415,8 +415,5 @@
 internal fun Float.toRadians() = this * PI.toFloat() / 180f
 internal fun Float.toDegrees() = this * 180f / PI.toFloat()
 internal fun <T> Iterable<T>.sumOf(selector: (T) -> Float): Float = map(selector).sum()
-internal fun lerp(start: Float, stop: Float, fraction: Float): Float {
-    return (1 - fraction) * start + fraction * stop
-}
 internal fun offsetFromDistanceAndAngle(distance: Float, angle: Float) =
     Offset(distance * cos(angle), distance * sin(angle))
\ No newline at end of file
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index d60f59f..81dd3ed 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -482,6 +482,12 @@
     property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
   }
 
+  public final class ScrollAwayKt {
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+  }
+
   @androidx.compose.runtime.Immutable public final class Shapes {
     ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
     method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 773d83e..4451f42 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -508,6 +508,12 @@
     property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
   }
 
+  public final class ScrollAwayKt {
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+  }
+
   @androidx.compose.runtime.Immutable public final class Shapes {
     ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
     method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index d60f59f..81dd3ed 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -482,6 +482,12 @@
     property public abstract androidx.compose.animation.core.Easing scaleInterpolator;
   }
 
+  public final class ScrollAwayKt {
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.ScrollState scrollState, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.compose.foundation.lazy.LazyListState scrollState, optional int itemIndex, optional int offset);
+    method public static androidx.compose.ui.Modifier scrollAway(androidx.compose.ui.Modifier, androidx.wear.compose.material.ScalingLazyListState scrollState, optional int itemIndex, optional int offset);
+  }
+
   @androidx.compose.runtime.Immutable public final class Shapes {
     ctor public Shapes(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
     method public androidx.wear.compose.material.Shapes copy(optional androidx.compose.foundation.shape.CornerBasedShape small, optional androidx.compose.foundation.shape.CornerBasedShape medium, optional androidx.compose.foundation.shape.CornerBasedShape large);
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index feb103a..e67002e 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -29,16 +29,16 @@
 dependencies {
 
     if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        api("androidx.compose.foundation:foundation: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.runtime:runtime:1.3.0-rc01")
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
 
         implementation(libs.kotlinStdlib)
-        implementation("androidx.compose.animation:animation:1.3.0-rc01")
-        implementation("androidx.compose.material:material:1.3.0-rc01")
-        implementation("androidx.compose.material:material-ripple:1.3.0-rc01")
-        implementation("androidx.compose.ui:ui-util:1.3.0-rc01")
+        implementation(project(":compose:animation:animation"))
+        implementation(project(":compose:material:material"))
+        implementation(project(":compose:material:material-ripple"))
+        implementation(project(":compose:ui:ui-util"))
         implementation(project(":wear:compose:compose-foundation"))
         implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
new file mode 100644
index 0000000..81b971ae
--- /dev/null
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.compose.material
+
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.AlignmentLine
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.lerp
+
+/**
+ * Scroll an item vertically in/out of view based on a [ScrollState].
+ * Typically used to scroll a [TimeText] item out of view as the user starts to scroll a
+ * vertically scrollable [Column] of items upwards and bring additional items into view.
+ *
+ * @param scrollState The [ScrollState] to used as the basis for the scroll-away.
+ * @param offset Adjustment to the starting point for scrolling away. Positive values result in
+ * the scroll away starting later.
+ */
+public fun Modifier.scrollAway(
+    scrollState: ScrollState,
+    offset: Int = 0,
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "scrollAway"
+        properties["scrollState"] = scrollState
+        properties["offset"] = offset
+    }
+) {
+    val y = with(LocalDensity.current) {
+        (scrollState.value - offset).toDp()
+    }
+    scrollAway(y)
+}
+
+/**
+ * Scroll an item vertically in/out of view based on a [LazyListState].
+ * Typically used to scroll a [TimeText] item out of view as the user starts to scroll
+ * a [LazyColumn] of items upwards and bring additional items into view.
+ *
+ * @param scrollState The [LazyListState] to used as the basis for the scroll-away.
+ * @param itemIndex The item for which the scroll offset will trigger scrolling away.
+ * @param offset Adjustment to the starting point for scrolling away. Positive values result in
+ * the scroll away starting later.
+ */
+public fun Modifier.scrollAway(
+    scrollState: LazyListState,
+    itemIndex: Int = 0,
+    offset: Int = 0,
+): Modifier = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "scrollAway"
+        properties["scrollState"] = scrollState
+        properties["itemIndex"] = itemIndex
+        properties["offset"] = offset
+    }
+) {
+    val targetItem by remember {
+        derivedStateOf {
+            scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
+        }
+    }
+    if (targetItem != null) {
+        val y = with(LocalDensity.current) {
+            (-targetItem!!.offset - offset).toDp()
+        }
+        scrollAway(y)
+    } else {
+        ignore()
+    }
+}
+
+/**
+ * Scroll an item vertically in/out of view based on a [ScalingLazyListState].
+ * Typically used to scroll a [TimeText] item out of view as the user starts to scroll
+ * a [ScalingLazyColumn] of items upwards and bring additional items into view.
+ *
+ * @param scrollState The [ScalingLazyListState] to used as the basis for the scroll-away.
+ * @param itemIndex The item for which the scroll offset will trigger scrolling away.
+ * @param offset Adjustment to the starting point for scrolling away. Positive values result in
+ * the scroll away starting later, negative values start scrolling away earlier.
+ */
+public fun Modifier.scrollAway(
+    scrollState: ScalingLazyListState,
+    itemIndex: Int = 1,
+    offset: Int = 0,
+): Modifier =
+    composed(
+        inspectorInfo = debugInspectorInfo {
+            name = "scrollAway"
+            properties["scrollState"] = scrollState
+            properties["itemIndex"] = itemIndex
+            properties["offset"] = offset
+        }
+    ) {
+        val targetItem by remember {
+            derivedStateOf {
+                scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }
+            }
+        }
+        if (targetItem != null) {
+            val y = with(LocalDensity.current) {
+                (-targetItem!!.offset - offset).toDp()
+            }
+            scrollAway(y)
+        } else {
+            ignore()
+        }
+    }
+
+private fun Modifier.scrollAway(y: Dp): Modifier {
+    val progress: Float = (y / maxScrollOut).coerceIn(0f, 1f)
+    val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
+    val offset = -(maxOffset * progress)
+
+    return this
+        .offset(y = offset)
+        .graphicsLayer {
+            alpha = motionFraction
+            scaleX = motionFraction
+            scaleY = motionFraction
+            transformOrigin = TransformOrigin(pivotFractionX = 0.5f, pivotFractionY = 0.0f)
+        }
+}
+
+// Trivial modifier that neither measures nor places the content.
+private fun Modifier.ignore(): Modifier = this.then(
+    object : LayoutModifier {
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            return object : MeasureResult {
+                override val width = 0
+                override val height = 0
+                override val alignmentLines = mapOf<AlignmentLine, Int>()
+                override fun placeChildren() {}
+            }
+        }
+    }
+)
+
+// The scroll motion effects take place between 0dp and 36dp.
+internal val maxScrollOut = 36.dp
+
+// The max offset to apply.
+internal val maxOffset = 24.dp
+
+// Fade and scale motion effects are between 100% and 50%.
+internal const val minMotionOut = 1f
+internal const val maxMotionOut = 0.5f
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 76aa4d1..bc7a4e8 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -25,8 +25,8 @@
 
 dependencies {
 
-    api("androidx.compose.ui:ui:1.3.0-rc01")
-    api("androidx.compose.runtime:runtime:1.3.0-rc01")
+    api(project(":compose:ui:ui"))
+    api(project(":compose:runtime:runtime"))
     api("androidx.navigation:navigation-runtime:2.4.0")
     api(project(":wear:compose:compose-material"))
 
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index c8449e4..001d460 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -118,6 +118,31 @@
     "Material",
     listOf(
         DemoCategory(
+            "ScrollAway",
+            listOf(
+                ComposableDemo("Column") { ScrollAwayColumnDemo() },
+                ComposableDemo("Column (delay)") { ScrollAwayColumnDelayDemo() },
+                ComposableDemo("Lazy Column") { ScrollAwayLazyColumnDemo() },
+                ComposableDemo("Lazy Column offset<0") { ScrollAwayLazyColumnDemo2() },
+                ComposableDemo("Lazy Column offset>0") { ScrollAwayLazyColumnDelayDemo() },
+                ComposableDemo("SLC Cards") {
+                    ScrollAwayScalingLazyColumnCardDemo()
+                },
+                ComposableDemo("SLC Cards offset<0") {
+                    ScrollAwayScalingLazyColumnCardDemo2()
+                },
+                ComposableDemo("SLC Cards offset>0") {
+                    ScrollAwayScalingLazyColumnCardDemoMismatch()
+                },
+                ComposableDemo("SLC Chips") {
+                    ScrollAwayScalingLazyColumnChipDemo()
+                },
+                ComposableDemo("SLC Chips offset<0") {
+                    ScrollAwayScalingLazyColumnChipDemo2()
+                },
+            )
+        ),
+        DemoCategory(
             "Picker",
             if (Build.VERSION.SDK_INT > 25) {
                 listOf(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
new file mode 100644
index 0000000..2f9a6bf
--- /dev/null
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.compose.integration.demos
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.unit.dp
+import androidx.wear.compose.material.AutoCenteringParams
+import androidx.wear.compose.material.Card
+import androidx.wear.compose.material.Chip
+import androidx.wear.compose.material.ChipDefaults
+import androidx.wear.compose.material.ListHeader
+import androidx.wear.compose.material.MaterialTheme
+import androidx.wear.compose.material.PositionIndicator
+import androidx.wear.compose.material.Scaffold
+import androidx.wear.compose.material.ScalingLazyColumn
+import androidx.wear.compose.material.Text
+import androidx.wear.compose.material.TimeText
+import androidx.wear.compose.material.rememberScalingLazyListState
+import androidx.wear.compose.material.scrollAway
+
+@Composable
+fun ScrollAwayColumnDemo() { ColumnCardDemo(0) }
+
+@Composable
+fun ScrollAwayColumnDelayDemo() { ColumnCardDemo(offset = 36) }
+
+@Composable
+fun ScrollAwayLazyColumnDemo() {
+    LazyColumnCardDemo(offset = 0, itemIndex = 0, initialVisibleItemIndex = 0)
+}
+
+@Composable
+fun ScrollAwayLazyColumnDemo2() {
+    LazyColumnCardDemo(offset = -350, itemIndex = 2, initialVisibleItemIndex = 2)
+}
+
+@Composable
+fun ScrollAwayLazyColumnDelayDemo() {
+    LazyColumnCardDemo(offset = 36, itemIndex = 0, initialVisibleItemIndex = 0)
+}
+
+@Composable
+fun ScrollAwayScalingLazyColumnCardDemo() {
+    ScalingLazyColumnCardDemo(
+        itemIndex = 1,
+        offset = 0,
+        initialCenterItemIndex = 1,
+    )
+}
+
+@Composable
+fun ScrollAwayScalingLazyColumnCardDemo2() {
+    ScalingLazyColumnCardDemo(
+        itemIndex = 2,
+        offset = -180,
+        initialCenterItemIndex = 2,
+    )
+}
+
+@Composable
+fun ScrollAwayScalingLazyColumnCardDemoMismatch() {
+    ScalingLazyColumnCardDemo(
+        itemIndex = 0,
+        offset = 120,
+        initialCenterItemIndex = 1,
+    )
+}
+
+@Composable
+fun ScrollAwayScalingLazyColumnChipDemo() {
+    ScalingLazyColumnChipDemo(
+        itemIndex = 1,
+        offset = 20,
+        initialCenterItemIndex = 1,
+    )
+}
+
+@Composable
+fun ScrollAwayScalingLazyColumnChipDemo2() {
+    ScalingLazyColumnChipDemo(
+        itemIndex = 2,
+        offset = -100,
+        initialCenterItemIndex = 2,
+    )
+}
+
+@Composable
+private fun ColumnCardDemo(offset: Int) {
+    val scrollState = rememberScrollState()
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        timeText = {
+            TimeText(
+                modifier = Modifier.scrollAway(
+                    scrollState = scrollState,
+                    offset = offset,
+                )
+            )
+        },
+        positionIndicator = {
+            PositionIndicator(scrollState = scrollState)
+        }
+    ) {
+        Column(
+            modifier = Modifier
+                .verticalScroll(scrollState)
+        ) {
+            val modifier = Modifier.height(LocalConfiguration.current.screenHeightDp.dp / 2)
+            repeat(3) { i ->
+                ExampleCard(modifier, i)
+            }
+        }
+    }
+}
+
+@Composable
+private fun LazyColumnCardDemo(offset: Int, itemIndex: Int, initialVisibleItemIndex: Int) {
+    val scrollState = rememberLazyListState(initialFirstVisibleItemIndex = initialVisibleItemIndex)
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        timeText = {
+            TimeText(modifier = Modifier.scrollAway(
+                scrollState = scrollState,
+                offset = offset,
+                itemIndex = itemIndex
+            ))
+        },
+        positionIndicator = {
+            PositionIndicator(lazyListState = scrollState)
+        }
+    ) {
+        LazyColumn(
+            state = scrollState
+        ) {
+            items(5) { i ->
+                val modifier = Modifier.fillParentMaxHeight(0.5f)
+                ExampleCard(modifier = modifier, i = i)
+            }
+        }
+    }
+}
+
+@Composable
+private fun ScalingLazyColumnCardDemo(
+    offset: Int,
+    itemIndex: Int,
+    initialCenterItemIndex: Int,
+) {
+    val scrollState =
+        rememberScalingLazyListState(
+            initialCenterItemIndex = initialCenterItemIndex,
+            initialCenterItemScrollOffset = 0
+        )
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        timeText = {
+            TimeText(modifier =
+            Modifier.scrollAway(
+                scrollState = scrollState,
+                itemIndex = itemIndex,
+                offset = offset,
+            ))
+        },
+        positionIndicator = {
+            PositionIndicator(scalingLazyListState = scrollState)
+        }
+    ) {
+        ScalingLazyColumn(
+            contentPadding = PaddingValues(10.dp),
+            state = scrollState,
+            autoCentering = AutoCenteringParams(itemIndex = 1, itemOffset = 0)
+        ) {
+            item {
+                ListHeader { Text("Cards") }
+            }
+
+            items(5) { i ->
+                ExampleCard(Modifier.fillParentMaxHeight(0.5f), i)
+            }
+        }
+    }
+}
+
+@Composable
+private fun ScalingLazyColumnChipDemo(
+    offset: Int,
+    itemIndex: Int,
+    initialCenterItemIndex: Int,
+) {
+    val scrollState =
+        rememberScalingLazyListState(
+            initialCenterItemIndex = initialCenterItemIndex,
+            initialCenterItemScrollOffset = 0
+        )
+
+    Scaffold(
+        modifier = Modifier.fillMaxSize(),
+        timeText = {
+            TimeText(modifier =
+            Modifier.scrollAway(
+                scrollState = scrollState,
+                itemIndex = itemIndex,
+                offset = offset,
+            ))
+        },
+        positionIndicator = {
+            PositionIndicator(scalingLazyListState = scrollState)
+        }
+    ) {
+        ScalingLazyColumn(
+            contentPadding = PaddingValues(10.dp),
+            state = scrollState,
+        ) {
+            item {
+                ListHeader { Text("Chips") }
+            }
+
+            items(5) { i ->
+                ExampleChip(Modifier.fillMaxWidth(), i)
+            }
+        }
+    }
+}
+
+@Composable
+private fun ExampleCard(modifier: Modifier, i: Int) {
+    Card(
+        modifier = modifier,
+        onClick = { }
+    ) {
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .background(MaterialTheme.colors.surface),
+            contentAlignment = Alignment.Center
+        ) {
+            Text(text = "Card $i")
+        }
+    }
+}
+
+@Composable
+private fun ExampleChip(modifier: Modifier, i: Int) {
+    Chip(
+        modifier = modifier,
+        onClick = { },
+        colors = ChipDefaults.primaryChipColors(),
+        border = ChipDefaults.chipBorder()
+    ) {
+        Text(text = "Chip $i")
+    }
+}
diff --git a/wear/watchface/watchface-complications-data/proguard-rules.pro b/wear/watchface/watchface-complications-data/proguard-rules.pro
index de1e0af..187c816 100644
--- a/wear/watchface/watchface-complications-data/proguard-rules.pro
+++ b/wear/watchface/watchface-complications-data/proguard-rules.pro
@@ -22,6 +22,7 @@
 -keep public class android.support.wearable.complications.TimeDependentText { *; }
 -keep public class android.support.wearable.complications.TimeDifferenceText { *; }
 -keep public class android.support.wearable.complications.TimeFormatText { *; }
+-keep public class android.graphics.drawable.Icon { *; }
 
 # Ensure our sanitizing of EditorSession.usr_style doesn't break due to renames.
 -keep public class kotlinx.coroutines.flow.MutableStateFlow { *; }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
index ec2444f..e584635 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.java
@@ -536,7 +536,7 @@
 
     @RequiresApi(api = Build.VERSION_CODES.P)
     private static class SerializedForm implements Serializable {
-        private static final int VERSION_NUMBER = 15;
+        private static final int VERSION_NUMBER = 16;
 
         @NonNull
         ComplicationData mComplicationData;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
index 9b0bfa4..4dea811 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/IconSerializableHelper.java
@@ -16,16 +16,22 @@
 
 package android.support.wearable.complications;
 
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.os.Build;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.Serializable;
+import java.lang.reflect.Method;
 
 @RequiresApi(api = Build.VERSION_CODES.P)
 class IconSerializableHelper implements Serializable {
@@ -33,6 +39,9 @@
     String mResourcePackage;
     int mResourceId;
     String mUri;
+    byte[] mBitmap;
+
+    private static final String TAG = "IconSerializableHelper";
 
     @Nullable
     static IconSerializableHelper create(@Nullable Icon icon) {
@@ -61,22 +70,44 @@
                 break;
 
             case Icon.TYPE_URI:
+            case Icon.TYPE_URI_ADAPTIVE_BITMAP:
                 mUri = icon.getUri().toString();
                 break;
-        }
 
-        // We currently don't attempt to serialize any other type of icon. We could render to a
-        // bitmap, but the above covers the majority of complication icons.
+            case Icon.TYPE_BITMAP:
+            case Icon.TYPE_ADAPTIVE_BITMAP:
+                try {
+                    Method getBitmap = icon.getClass().getDeclaredMethod("getBitmap");
+                    @SuppressLint("BanUncheckedReflection")
+                    Bitmap bitmap = (Bitmap) getBitmap.invoke(icon);
+                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                    bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
+                    mBitmap = baos.toByteArray();
+                } catch (Exception e) {
+                    Log.e(TAG, "Failed to serialize bitmap", e);
+                }
+                break;
+
+            default:
+                Log.e(TAG, "Failed to serialize icon of type " + mType);
+        }
     }
 
-    @Nullable Icon toIcon() {
+    @Nullable
+    Icon toIcon() {
         switch (mType) {
             case Icon.TYPE_RESOURCE:
                 return Icon.createWithResource(mResourcePackage, mResourceId);
 
             case Icon.TYPE_URI:
+            case Icon.TYPE_URI_ADAPTIVE_BITMAP:
                 return Icon.createWithContentUri(mUri);
 
+            case Icon.TYPE_BITMAP:
+            case Icon.TYPE_ADAPTIVE_BITMAP:
+                return Icon.createWithBitmap(BitmapFactory.decodeByteArray(mBitmap, 0,
+                        mBitmap.length));
+
             default:
                 return null;
         }
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 37edec2..73c4f3b 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -16,10 +16,12 @@
 
 package androidx.wear.watchface.complications.data
 
+import android.annotation.SuppressLint
 import android.app.PendingIntent
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
+import android.graphics.Bitmap
 import android.graphics.Color
 import android.graphics.drawable.Icon
 import android.os.Build
@@ -1146,6 +1148,36 @@
 
     @RequiresApi(Build.VERSION_CODES.P)
     @Test
+    public fun smallImageComplicationData_with_BitmapIcon() {
+        val bitmapIcon =
+            Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
+        val image = SmallImage.Builder(bitmapIcon, SmallImageType.PHOTO).build()
+        val data = SmallImageComplicationData.Builder(
+            image, "content description".complicationText
+        ).setDataSource(dataSourceA).build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_SMALL_IMAGE)
+                    .setSmallImage(bitmapIcon)
+                    .setSmallImageStyle(WireComplicationData.IMAGE_STYLE_PHOTO)
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as SmallImageComplicationData
+
+        assertThat(deserialized.smallImage.image.type).isEqualTo(Icon.TYPE_BITMAP)
+        val getBitmap = deserialized.smallImage.image.javaClass.getDeclaredMethod("getBitmap")
+        @SuppressLint("BanUncheckedReflection")
+        val bitmap = getBitmap.invoke(deserialized.smallImage.image) as Bitmap
+
+        assertThat(bitmap.width).isEqualTo(100)
+        assertThat(bitmap.height).isEqualTo(100)
+    }
+
+    @RequiresApi(Build.VERSION_CODES.P)
+    @Test
     public fun backgroundImageComplicationData() {
         val photoImage = Icon.createWithContentUri("someuri")
         val data = PhotoImageComplicationData.Builder(
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 5bebc58..f53772d 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
@@ -1457,6 +1457,11 @@
             // list. NB we can't actually serialise complications anyway so that's just as well...
             params.idAndComplicationDataWireFormats = emptyList()
 
+            // Let wallpaper manager know the wallpaper has changed.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+                NotifyColorsChangedHelper.notifyColorsChanged(this)
+            }
+
             backgroundThreadCoroutineScope.launch {
                 writeDirectBootPrefs(_context, DIRECT_BOOT_PREFS, params)
             }
@@ -2712,6 +2717,13 @@
         fun extractFromWindowInsets(insets: WindowInsets?) =
             insets?.getInsets(WindowInsets.Type.systemBars())?.bottom ?: 0
     }
+
+    @RequiresApi(27)
+    private object NotifyColorsChangedHelper {
+        fun notifyColorsChanged(engine: Engine) {
+            engine.notifyColorsChanged()
+        }
+    }
 }
 
 /**
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index b1b5c15..6b7eeb8 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -26,7 +26,6 @@
   }
 
   @androidx.window.core.ExperimentalWindowApi public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
-    ctor @Deprecated public ActivityRule(java.util.Set<androidx.window.embedding.ActivityFilter> filters, optional boolean alwaysExpand);
     method public boolean getAlwaysExpand();
     method public java.util.Set<androidx.window.embedding.ActivityFilter> getFilters();
     property public final boolean alwaysExpand;
diff --git a/window/window/build.gradle b/window/window/build.gradle
index 8cde8eb..c8f0b4d 100644
--- a/window/window/build.gradle
+++ b/window/window/build.gradle
@@ -50,7 +50,7 @@
     implementation("androidx.core:core:1.8.0")
 
     compileOnly(project(":window:sidecar:sidecar"))
-    compileOnly(project(":window:extensions:extensions"))
+    compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02")
 
     testImplementation(libs.testCore)
     testImplementation(libs.testRunner)
@@ -61,7 +61,7 @@
     testImplementation(libs.mockitoKotlin)
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation(compileOnly(project(":window:sidecar:sidecar")))
-    testImplementation(compileOnly(project(":window:extensions:extensions")))
+    testImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02"))
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.kotlinTestJunit)
@@ -75,8 +75,8 @@
     androidTestImplementation(libs.multidex)
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.junit) // Needed for Assert.assertThrows
-    androidTestImplementation(compileOnly(project(":window:extensions:extensions")))
     androidTestImplementation(compileOnly(project(":window:sidecar:sidecar")))
+    androidTestImplementation(compileOnly("androidx.window.extensions:extensions:1.1.0-alpha02"))
     samples(project(":window:window-samples"))
 }
 
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
index 92c932e..a76c5e6 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
@@ -23,32 +23,20 @@
  * [SplitPairRule].
  */
 @ExperimentalWindowApi
-class ActivityRule : EmbeddingRule {
-
+class ActivityRule internal constructor(
     /**
      * Filters used to choose when to apply this rule. The rule may be used if any one of the
      * provided filters matches.
      */
-    val filters: Set<ActivityFilter>
+    val filters: Set<ActivityFilter>,
     /**
      * Whether the activity should always be expanded on launch. Some activities are supposed to
      * expand to the full task bounds, independent of the state of the split. An example is an
      * activity that blocks all user interactions, like a warning dialog.
      */
-    val alwaysExpand: Boolean
+    val alwaysExpand: Boolean = false
+) : EmbeddingRule() {
 
-    // TODO(b/229656253): Reduce visibility to remove from public API.
-    @Deprecated(
-        message = "Visibility of the constructor will be reduced.",
-        replaceWith = ReplaceWith("androidx.window.embedding.ActivityRule.Builder")
-    )
-    constructor(
-        filters: Set<ActivityFilter>,
-        alwaysExpand: Boolean = false
-    ) {
-        this.filters = filters.toSet()
-        this.alwaysExpand = alwaysExpand
-    }
     /**
      * Builder for [ActivityRule].
      * @param filters See [ActivityRule.filters].
@@ -65,7 +53,6 @@
         fun setAlwaysExpand(alwaysExpand: Boolean): Builder =
             apply { this.alwaysExpand = alwaysExpand }
 
-        @Suppress("DEPRECATION")
         fun build() = ActivityRule(filters, alwaysExpand)
     }
 
@@ -74,12 +61,8 @@
      * @see filters
      */
     internal operator fun plus(filter: ActivityFilter): ActivityRule {
-        val newSet = mutableSetOf<ActivityFilter>()
-        newSet.addAll(filters)
-        newSet.add(filter)
-        @Suppress("DEPRECATION")
         return ActivityRule(
-            newSet.toSet(),
+            filters + filter,
             alwaysExpand
         )
     }
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.kt
new file mode 100644
index 0000000..beb20e3
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityRuleTest.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.window.embedding
+
+import androidx.window.core.ActivityComponentInfo
+import androidx.window.core.ExperimentalWindowApi
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalWindowApi::class)
+class ActivityRuleTest {
+
+    @Test
+    fun testAddingActivityRule() {
+        val rule = ActivityRule.Builder(setOf()).build() + FILTER_WITH_ACTIVITY
+
+        assertEquals(setOf(FILTER_WITH_ACTIVITY), rule.filters)
+    }
+
+    @Test
+    fun testBuildActivityRule() {
+        val rule = ActivityRule.Builder(setOf(FILTER_WITH_ACTIVITY)).build()
+
+        assertEquals(setOf(FILTER_WITH_ACTIVITY), rule.filters)
+        assertFalse(rule.alwaysExpand)
+    }
+
+    @Test
+    fun testBuildActivityRule_enableAlwaysExpanded() {
+        val rule = ActivityRule.Builder(setOf(FILTER_WITH_ACTIVITY)).setAlwaysExpand(true).build()
+
+        assertEquals(setOf(FILTER_WITH_ACTIVITY), rule.filters)
+        assertTrue(rule.alwaysExpand)
+    }
+
+    @Test
+    fun equalsImpliesHashCode() {
+        val firstRule = ActivityRule.Builder(setOf(FILTER_WITH_ACTIVITY))
+            .setAlwaysExpand(true)
+            .build()
+        val secondRule = ActivityRule.Builder(setOf(FILTER_WITH_ACTIVITY))
+            .setAlwaysExpand(true)
+            .build()
+
+        assertEquals(firstRule, secondRule)
+        assertEquals(firstRule.hashCode(), secondRule.hashCode())
+    }
+
+    companion object {
+        val FILTER_WITH_ACTIVITY = ActivityFilter(
+            ActivityComponentInfo("package", "className"),
+            null
+        )
+    }
+}
\ No newline at end of file
diff --git a/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
new file mode 100644
index 0000000..e7833f8
--- /dev/null
+++ b/window/window/src/test/java/androidx/window/embedding/SplitInfoTest.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.window.embedding
+
+import android.app.Activity
+import androidx.window.core.ExperimentalWindowApi
+import com.nhaarman.mockitokotlin2.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalWindowApi::class)
+class SplitInfoTest {
+
+    @Test
+    fun testSplitInfoContainsActivityFirstStack() {
+        val activity = mock<Activity>()
+        val firstStack = ActivityStack(listOf(activity))
+        val secondStack = ActivityStack(emptyList())
+        val info = SplitInfo(firstStack, secondStack, 0.5f)
+
+        assertTrue(info.contains(activity))
+    }
+
+    @Test
+    fun testSplitInfoContainsActivitySecondStack() {
+        val activity = mock<Activity>()
+        val firstStack = ActivityStack(emptyList())
+        val secondStack = ActivityStack(listOf(activity))
+        val info = SplitInfo(firstStack, secondStack, 0.5f)
+
+        assertTrue(info.contains(activity))
+    }
+
+    @Test
+    fun testEqualsImpliesHashCode() {
+        val activity = mock<Activity>()
+        val firstStack = ActivityStack(emptyList())
+        val secondStack = ActivityStack(listOf(activity))
+        val firstInfo = SplitInfo(firstStack, secondStack, 0.5f)
+        val secondInfo = SplitInfo(firstStack, secondStack, 0.5f)
+
+        assertEquals(firstInfo, secondInfo)
+        assertEquals(firstInfo.hashCode(), secondInfo.hashCode())
+    }
+}
\ No newline at end of file
diff --git a/work/integration-tests/testapp/build.gradle b/work/integration-tests/testapp/build.gradle
index 72acc47..a44e9ab 100644
--- a/work/integration-tests/testapp/build.gradle
+++ b/work/integration-tests/testapp/build.gradle
@@ -50,15 +50,8 @@
 }
 
 dependencies {
-    // Using -Pandroidx.useMaxDepVersions does not use the right version of the annotation processor
-    // Remove this workaround after b/127495641 is fixed
-    if (project.hasProperty("androidx.useMaxDepVersions")) {
-        annotationProcessor(projectOrArtifact(":room:room-compiler"))
-        implementation(projectOrArtifact(":room:room-runtime"))
-    } else {
-        annotationProcessor("androidx.room:room-compiler:2.4.0-rc01")
-        implementation("androidx.room:room-runtime:2.4.0-rc01")
-    }
+    annotationProcessor(projectOrArtifact(":room:room-compiler"))
+    implementation(projectOrArtifact(":room:room-runtime"))
 
     implementation(libs.constraintLayout)
     // Fix for BuildCompat.isAtleastS()
diff --git a/work/work-benchmark/build.gradle b/work/work-benchmark/build.gradle
index 9a5d999..3dc9841 100644
--- a/work/work-benchmark/build.gradle
+++ b/work/work-benchmark/build.gradle
@@ -27,7 +27,7 @@
     androidTestImplementation(project(":work:work-runtime-ktx"))
     androidTestImplementation(project(":work:work-multiprocess"))
     androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
-    androidTestImplementation("androidx.room:room-runtime:2.4.0-rc01")
+    androidTestImplementation(projectOrArtifact(":room:room-runtime"))
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/work/work-gcm/build.gradle b/work/work-gcm/build.gradle
index 336375fb..332a4c4 100644
--- a/work/work-gcm/build.gradle
+++ b/work/work-gcm/build.gradle
@@ -32,17 +32,9 @@
 dependencies {
     api project(":work:work-runtime")
     implementation(libs.gcmNetworkManager)
-    // Using -Pandroidx.useMaxDepVersions does not use the right version of the annotation processor
-    // Remove this workaround after b/127495641 is fixed
-    if (project.hasProperty("androidx.useMaxDepVersions")) {
-        annotationProcessor(projectOrArtifact(":room:room-compiler"))
-        implementation(projectOrArtifact(":room:room-runtime"))
-        androidTestImplementation(projectOrArtifact(":room:room-testing"))
-    } else {
-        annotationProcessor("androidx.room:room-compiler:2.4.0-rc01")
-        implementation("androidx.room:room-runtime:2.4.0-rc01")
-        androidTestImplementation("androidx.room:room-testing:2.4.0-rc01")
-    }
+    annotationProcessor(projectOrArtifact(":room:room-compiler"))
+    implementation(projectOrArtifact(":room:room-runtime"))
+    androidTestImplementation(projectOrArtifact(":room:room-testing"))
 
     androidTestImplementation(project(":work:work-runtime-ktx"))
     androidTestImplementation(libs.testExtJunit)
diff --git a/work/work-inspection/build.gradle b/work/work-inspection/build.gradle
index 5f2ca92..6c66eb3 100644
--- a/work/work-inspection/build.gradle
+++ b/work/work-inspection/build.gradle
@@ -30,7 +30,7 @@
     compileOnly("androidx.inspection:inspection:1.0.0")
     compileOnly("androidx.lifecycle:lifecycle-runtime:2.2.0")
     compileOnly(project(":work:work-runtime"))
-    compileOnly("androidx.room:room-runtime:2.4.0-rc01")
+    compileOnly(projectOrArtifact(":room:room-runtime"))
     androidTestImplementation(project(":inspection:inspection-testing"))
     androidTestImplementation(project(":work:work-runtime"))
     androidTestImplementation(project(":work:work-runtime-ktx"))
diff --git a/work/work-multiprocess/build.gradle b/work/work-multiprocess/build.gradle
index f350aa2..fb72873 100644
--- a/work/work-multiprocess/build.gradle
+++ b/work/work-multiprocess/build.gradle
@@ -35,7 +35,7 @@
     api(libs.kotlinCoroutinesAndroid)
     api(libs.guavaListenableFuture)
     implementation("androidx.core:core:1.1.0")
-    implementation("androidx.room:room-runtime:2.4.0-rc01")
+    implementation(projectOrArtifact(":room:room-runtime"))
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/work/work-runtime-ktx/build.gradle b/work/work-runtime-ktx/build.gradle
index 0eaaf9c..e276de3 100644
--- a/work/work-runtime-ktx/build.gradle
+++ b/work/work-runtime-ktx/build.gradle
@@ -35,7 +35,7 @@
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has its own MockMaker
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has its own MockMaker
-    androidTestImplementation("androidx.room:room-testing:2.4.0-rc01")
+    androidTestImplementation(projectOrArtifact(":room:room-testing"))
     testImplementation(libs.junit)
 }
 
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index 26e34be..66fb8c2 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -57,10 +57,10 @@
 
 dependencies {
     implementation("androidx.core:core:1.6.0")
-    ksp("androidx.room:room-compiler:2.4.0-rc01")
-    implementation("androidx.room:room-runtime:2.4.0-rc01")
-    androidTestImplementation("androidx.room:room-testing:2.4.0-rc01")
-    implementation("androidx.sqlite:sqlite-framework:2.3.0-alpha05")
+    ksp(projectOrArtifact(":room:room-compiler"))
+    implementation(projectOrArtifact(":room:room-runtime"))
+    androidTestImplementation(projectOrArtifact(":room:room-testing"))
+    implementation(projectOrArtifact(":sqlite:sqlite-framework"))
     api("androidx.annotation:annotation-experimental:1.0.0")
     api(libs.guavaListenableFuture)
     api("androidx.lifecycle:lifecycle-livedata:2.1.0")
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index a917e8e..505e108 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -66,7 +67,6 @@
 @MediumTest
 @RunWith(AndroidJUnit4.class)
 public class ForceStopRunnableTest {
-
     private Context mContext;
     private WorkManagerImpl mWorkManager;
     private Scheduler mScheduler;
@@ -75,10 +75,13 @@
     private PreferenceUtils mPreferenceUtils;
     private ForceStopRunnable mRunnable;
 
+    private ActivityManager mActivityManager;
+
     @Before
     public void setUp() {
         mContext = ApplicationProvider.getApplicationContext().getApplicationContext();
         mWorkManager = mock(WorkManagerImpl.class);
+        mActivityManager = mock(ActivityManager.class);
         mPreferenceUtils = mock(PreferenceUtils.class);
         mScheduler = mock(Scheduler.class);
         Executor executor = new SynchronousExecutor();
@@ -204,6 +207,7 @@
     public void test_InitializationExceptionHandler_migrationFailures() {
         mContext = mock(Context.class);
         when(mContext.getApplicationContext()).thenReturn(mContext);
+        when(mContext.getSystemService(Context.ACTIVITY_SERVICE)).thenReturn(mActivityManager);
         mWorkDatabase = WorkDatabase.create(mContext, mConfiguration.getTaskExecutor(), true);
         when(mWorkManager.getWorkDatabase()).thenReturn(mWorkDatabase);
         mRunnable = new ForceStopRunnable(mContext, mWorkManager);
diff --git a/work/work-testing/build.gradle b/work/work-testing/build.gradle
index 928d6a1..d30aefa 100644
--- a/work/work-testing/build.gradle
+++ b/work/work-testing/build.gradle
@@ -25,7 +25,7 @@
 dependencies {
     api(project(":work:work-runtime-ktx"))
     implementation("androidx.lifecycle:lifecycle-livedata-core:2.1.0")
-    implementation("androidx.room:room-runtime:2.4.0-rc01")
+    implementation(projectOrArtifact(":room:room-runtime"))
 
     androidTestImplementation("androidx.arch.core:core-testing:2.1.0")
     androidTestImplementation(libs.testExtJunit)