Merge "Add convenience APIs for Float, Int and Boolean in SQLiteStatement" into androidx-main
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
index def0983..1d6776c 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/BackHandlerTest.kt
@@ -24,12 +24,12 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
index 4af3fc8..e7bf0f8 100644
--- a/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
+++ b/activity/activity-compose/src/androidTest/java/androidx/activity/compose/PredictiveBackHandlerTest.kt
@@ -29,12 +29,12 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.performClick
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 94d10c5..4068049 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -30,7 +30,7 @@
     api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
     api("androidx.savedstate:savedstate:1.2.1")
     api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     implementation("androidx.tracing:tracing:1.0.0")
     api(libs.kotlinStdlib)
 
diff --git a/activity/integration-tests/macrobenchmark/build.gradle b/activity/integration-tests/macrobenchmark/build.gradle
index a1b080f..f60cc3f 100644
--- a/activity/integration-tests/macrobenchmark/build.gradle
+++ b/activity/integration-tests/macrobenchmark/build.gradle
@@ -29,6 +29,11 @@
     experimentalProperties["android.experimental.self-instrumenting"] = true
 }
 
+// Create a release build type and make sure it's the only one enabled.
+// This is needed because we benchmark the release build type only.
+android.buildTypes { release {} }
+androidComponents { beforeVariants(selector().all()) { enabled = buildType == 'release' } }
+
 dependencies {
     implementation(project(":benchmark:benchmark-junit4"))
     implementation(project(":benchmark:benchmark-macro-junit4"))
diff --git a/activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartMacroBenchmark.kt b/activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartBenchmark.kt
similarity index 94%
rename from activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartMacroBenchmark.kt
rename to activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartBenchmark.kt
index 2bf9360..f28ba33 100644
--- a/activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartMacroBenchmark.kt
+++ b/activity/integration-tests/macrobenchmark/src/main/java/androidx/activity/integration/macrobenchmark/ActivityStartBenchmark.kt
@@ -27,14 +27,14 @@
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
-class ActivityStartMacroBenchmark {
+class ActivityStartBenchmark {
 
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
     fun startup() = benchmarkRule.measureStartup(
-        compilationMode = CompilationMode.Full(),
+        compilationMode = CompilationMode.DEFAULT,
         startupMode = StartupMode.COLD,
         packageName = "androidx.activity.integration.macrobenchmark.target",
         metrics = listOf(StartupTimingMetric()),
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index e423942..1387c96 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -32,7 +32,7 @@
     api("androidx.drawerlayout:drawerlayout:1.0.0")
     implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
     implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     implementation("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
     api("androidx.savedstate:savedstate:1.2.1")
 
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
index 08a4c83..d87b51a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Arguments.kt
@@ -261,6 +261,18 @@
         runOnMainDeadlineSeconds =
             arguments.getBenchmarkArgument("runOnMainDeadlineSeconds")?.toLong() ?: 30
         Log.d(BenchmarkState.TAG, "runOnMainDeadlineSeconds $runOnMainDeadlineSeconds")
+
+        if (arguments.getString("orchestratorService") != null) {
+            InstrumentationResults.scheduleIdeWarningOnNextReport(
+                """
+                    AndroidX Benchmark does not support running with the AndroidX Test Orchestrator.
+
+                    AndroidX benchmarks (micro and macro) produce one JSON file per test module,
+                    which together with Test Orchestrator restarting the process frequently causes
+                    benchmark output JSON files to be repeatedly overwritten during the test.
+                    """.trimIndent()
+            )
+        }
     }
 
     fun macrobenchMethodTracingEnabled(): Boolean {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
index 32e3e54..3956071 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/InstrumentationResults.kt
@@ -20,6 +20,7 @@
 import android.util.Log
 import androidx.annotation.RestrictTo
 import androidx.test.platform.app.InstrumentationRegistry
+import java.util.Locale
 import org.jetbrains.annotations.TestOnly
 
 /**
@@ -139,14 +140,14 @@
         // for readability, report nanos with 10ths only if less than 100
         var output = if (nanos >= 100.0) {
             // 13 alignment is enough for ~10 seconds
-            "%,13d   ns".format(nanos.toLong())
+            "%,13d   ns".format(Locale.US, nanos.toLong())
         } else {
             // 13 + 2(.X) to match alignment above
-            "%,15.1f ns".format(nanos)
+            "%,15.1f ns".format(Locale.US, nanos)
         }
         if (allocations != null) {
             // 9 alignment is enough for ~10 million allocations
-            output += "    %8d allocs".format(allocations.toInt())
+            output += "    %8d allocs".format(Locale.US, allocations.toInt())
         }
         profilerResults.forEach {
             output += "    [${it.label}](file://${it.sanitizedOutputRelativePath})"
@@ -234,7 +235,7 @@
 
             val allMetrics = measurements.singleMetrics + measurements.sampledMetrics
             val maxLabelLength = allMetrics.maxOf { it.name.length }
-            fun Double.toDisplayString() = "%,.1f".format(this)
+            fun Double.toDisplayString() = "%,.1f".format(Locale.US, this)
 
             // max string length of any printed min/med/max is the largest max value seen. used to pad.
             val maxValueLength = allMetrics
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
index b5f936a..ace5f2e 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
@@ -151,7 +151,11 @@
     ) {
         startMethodTracingSampling(path, bufferSize, Arguments.profilerSampleFrequency)
     } else {
-        Debug.startMethodTracing(path, bufferSize, 0)
+        // NOTE: 0x10 flag enables low-overhead wall clock timing when ART module version supports
+        // it. Note that this doesn't affect trace parsing, since this doesn't affect wall clock,
+        // it only removes the expensive thread time clock which our parser doesn't use.
+        // TODO: switch to platform-defined constant once available (b/329499422)
+        Debug.startMethodTracing(path, bufferSize, 0x10)
     }
 
     return Profiler.ResultFile(
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index a38320e..5715dd6 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -67,7 +67,7 @@
 
     implementation(project(":benchmark:benchmark-common"))
     implementation("androidx.core:core:1.9.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     implementation("androidx.tracing:tracing-ktx:1.1.0")
     implementation("androidx.tracing:tracing-perfetto:1.0.0")
     implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
index 5ed23ba..014480c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/perfetto/PerfettoTraceProcessorTest.kt
@@ -250,6 +250,27 @@
     }
 
     @Test
+    fun query_includeModule() {
+        assumeTrue(isAbiSupported())
+        val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
+        val startups = PerfettoTraceProcessor.runServer {
+            loadTrace(PerfettoTrace(traceFile.absolutePath)) {
+                query("""
+                    INCLUDE PERFETTO MODULE android.startup.startups;
+
+                    SELECT * FROM android_startups;
+                """.trimIndent()).toList()
+            }
+        }
+        // minimal validation, just verifying query worked
+        assertEquals(1, startups.size)
+        assertEquals(
+            "androidx.benchmark.integration.macrobenchmark.target",
+            startups.single().string("package")
+        )
+    }
+
+    @Test
     fun queryMetricsJson() {
         assumeTrue(isAbiSupported())
         val traceFile = createTempFileFromAsset("api31_startup_cold", ".perfetto-trace")
diff --git a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
index 48bc82c..b336d85 100644
--- a/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
+++ b/buildSrc-tests/src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt
@@ -91,6 +91,7 @@
         assertThat(buildInfo.dependencyConstraints.single().artifactId)
             .isEqualTo("core-ktx")
         assertThat(buildInfo.shouldPublishDocs).isFalse()
+        assertThat(buildInfo.isKmp).isFalse()
     }
 
     fun setupBuildInfoProject() {
@@ -135,6 +136,7 @@
                             null,
                             it.artifactId,
                             project.provider { "fakeSha" },
+                            false,
                             false
                         )
                     }
diff --git a/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java b/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java
index 993cec9..bf7a859 100644
--- a/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java
+++ b/buildSrc/jetpad-integration/src/main/java/androidx/build/jetpad/LibraryBuildInfoFile.java
@@ -51,6 +51,7 @@
     public ArrayList<Dependency> dependencies;
     public ArrayList<Dependency> dependencyConstraints;
     public Boolean shouldPublishDocs;
+    public Boolean isKmp;
     public ArrayList<Check> checks;
 
     /**
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 983c914..dea0fb1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -184,7 +184,7 @@
         project.configureTaskTimeouts()
         project.configureMavenArtifactUpload(
             androidXExtension, androidXKmpExtension, componentFactory) {
-            project.addCreateLibraryBuildInfoFileTasks(androidXExtension)
+            project.addCreateLibraryBuildInfoFileTasks(androidXExtension, androidXKmpExtension)
         }
         project.publishInspectionArtifacts()
         project.configureExternalDependencyLicenseCheck()
@@ -500,7 +500,6 @@
         }
         if (plugin is KotlinMultiplatformPluginWrapper) {
             KonanPrebuiltsSetup.configureKonanDirectory(project)
-            KmpLinkTaskWorkaround.serializeLinkTasks(project)
             project.afterEvaluate {
                 val libraryExtension = project.extensions.findByType<LibraryExtension>()
                 if (libraryExtension != null) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index 73da6d1..2f91d77 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -128,7 +128,6 @@
 
         registerOwnersServiceTasks()
 
-        project.configureRootProjectForKmpLink()
         // If useMaxDepVersions is set, iterate through all the project and substitute any androidx
         // artifact dependency with the local tip of tree version of the library.
         if (project.usingMaxDepVersions()) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt b/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt
deleted file mode 100644
index c8990fc..0000000
--- a/buildSrc/private/src/main/kotlin/androidx/build/KmpLinkTaskWorkaround.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.build
-
-import org.gradle.api.Project
-import org.gradle.api.services.BuildService
-import org.gradle.api.services.BuildServiceParameters
-import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
-
-/**
- * Name of the service we use to limit the number of concurrent kmp link tasks
- */
-public const val KMP_LINK_SERVICE_NAME = "androidxKmpLinkService"
-
-// service for limiting the number of concurrent kmp link tasks b/309990481
-interface AndroidXKmpLinkService : BuildService<BuildServiceParameters.None>
-
-fun Project.configureRootProjectForKmpLink() {
-    project.gradle.sharedServices.registerIfAbsent(
-        KMP_LINK_SERVICE_NAME,
-        AndroidXKmpLinkService::class.java,
-        { spec ->
-            spec.maxParallelUsages.set(1)
-        }
-    )
-}
-
-object KmpLinkTaskWorkaround {
-    // b/309990481
-    fun serializeLinkTasks(
-        project: Project
-    ) {
-        project.tasks.withType(
-            KotlinNativeLink::class.java
-        ).configureEach { task ->
-            task.usesService(
-                task.project.gradle.sharedServices.registrations
-                    .getByName(KMP_LINK_SERVICE_NAME).service
-            )
-        }
-    }
-}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
index 461b94a..3512027 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/buildInfo/CreateLibraryBuildInfoFileTask.kt
@@ -17,6 +17,7 @@
 package androidx.build.buildInfo
 
 import androidx.build.AndroidXExtension
+import androidx.build.AndroidXMultiplatformExtension
 import androidx.build.LibraryGroup
 import androidx.build.docs.CheckTipOfTreeDocsTask.Companion.requiresDocs
 import androidx.build.getBuildInfoDirectory
@@ -107,6 +108,9 @@
     /** Whether the project should be included in docs-public/build.gradle. */
     @get:Input abstract val shouldPublishDocs: Property<Boolean>
 
+    /** Whether the artifact is from a KMP project. */
+    @get:Input abstract val kmp: Property<Boolean>
+
     private fun writeJsonToFile(info: LibraryBuildInfoFile) {
         val resolvedOutputFile: File = outputFile.get()
         val outputDir = resolvedOutputFile.parentFile
@@ -145,6 +149,7 @@
             if (dependencyConstraintList.isPresent) ArrayList(dependencyConstraintList.get())
             else ArrayList()
         libraryBuildInfoFile.shouldPublishDocs = shouldPublishDocs.get()
+        libraryBuildInfoFile.isKmp = kmp.get()
         return libraryBuildInfoFile
     }
 
@@ -169,6 +174,7 @@
             variant: VariantPublishPlan,
             shaProvider: Provider<String>,
             shouldPublishDocs: Boolean,
+            isKmp: Boolean,
         ): TaskProvider<CreateLibraryBuildInfoFileTask> {
             return project.tasks.register(
                 TASK_NAME + variant.taskSuffix,
@@ -209,6 +215,7 @@
                     variant.dependencyConstraints.map { it.asBuildInfoDependencies() }
                 )
                 task.shouldPublishDocs.set(shouldPublishDocs)
+                task.kmp.set(isKmp)
             }
         }
 
@@ -249,8 +256,11 @@
 }
 
 // Tasks that create a json files of a project's variant's dependencies
-fun Project.addCreateLibraryBuildInfoFileTasks(extension: AndroidXExtension) {
-    extension.ifReleasing {
+fun Project.addCreateLibraryBuildInfoFileTasks(
+    androidXExtension: AndroidXExtension,
+    androidXKmpExtension: AndroidXMultiplatformExtension,
+) {
+    androidXExtension.ifReleasing {
         configure<PublishingExtension> {
             // Unfortunately, dependency information is only available through internal API
             // (See https://github.com/gradle/gradle/issues/21345).
@@ -259,10 +269,11 @@
                 // main publication.  We do not track these aliases.
                 if (!mavenPub.isAlias) {
                     createTaskForComponent(
-                        mavenPub,
-                        extension.mavenGroup,
-                        mavenPub.artifactId,
-                        extension.requiresDocs(),
+                        pub = mavenPub,
+                        libraryGroup = androidXExtension.mavenGroup,
+                        artifactId = mavenPub.artifactId,
+                        shouldPublishDocs = androidXExtension.requiresDocs(),
+                        isKmp = androidXKmpExtension.supportedPlatforms.isNotEmpty(),
                     )
                 }
             }
@@ -275,6 +286,7 @@
     libraryGroup: LibraryGroup?,
     artifactId: String,
     shouldPublishDocs: Boolean,
+    isKmp: Boolean,
 ) {
     val task =
         createBuildInfoTask(
@@ -283,6 +295,7 @@
             artifactId,
             getHeadShaProvider(project),
             shouldPublishDocs,
+            isKmp
         )
     rootProject.tasks.named(CreateLibraryBuildInfoFileTask.TASK_NAME).configure {
         it.dependsOn(task)
@@ -296,6 +309,7 @@
     artifactId: String,
     shaProvider: Provider<String>,
     shouldPublishDocs: Boolean,
+    isKmp: Boolean,
 ): TaskProvider<CreateLibraryBuildInfoFileTask> {
     val kmpTaskSuffix = computeTaskSuffix(name, artifactId)
     return CreateLibraryBuildInfoFileTask.setup(
@@ -320,6 +334,7 @@
         // There's a build_info file for each KMP platform, but only the artifact without a platform
         // suffix is listed in docs-public/build.gradle.
         shouldPublishDocs = shouldPublishDocs && kmpTaskSuffix == "",
+        isKmp = isKmp,
     )
 }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
index 50cde53..ebb8d55 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraGraph.kt
@@ -22,6 +22,7 @@
 import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -72,6 +73,14 @@
         setSurfaceResults[stream] = surface
     }
 
+    override fun getAudioRestriction(): AudioRestrictionMode? {
+        throw NotImplementedError("Not used in testing")
+    }
+
+    override fun setAudioRestriction(mode: AudioRestrictionMode) {
+        throw NotImplementedError("Not used in testing")
+    }
+
     override fun start() {
         throw NotImplementedError("Not used in testing")
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 997ad8f..9ab0906 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -35,9 +35,13 @@
     val cameraStatus: Flow<CameraStatus>
 
     abstract class CameraStatus internal constructor() {
-        object CameraPrioritiesChanged : CameraStatus()
+        object CameraPrioritiesChanged : CameraStatus() {
+            override fun toString(): String = "CameraPrioritiesChanged"
+        }
 
-        class CameraAvailable(val cameraId: CameraId) : CameraStatus()
+        class CameraAvailable(val cameraId: CameraId) : CameraStatus() {
+            override fun toString(): String = "CameraAvailable(camera=$cameraId"
+        }
     }
 }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
index 76c367db..27550b3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraDevices.kt
@@ -179,7 +179,7 @@
      * @return The parsed Camera1 id, or null if the value cannot be parsed as a Camera1 id.
      */
     inline fun toCamera1Id(): Int? = value.toIntOrNull()
-    override fun toString(): String = "Camera $value"
+    override fun toString(): String = "CameraId-$value"
 }
 
 /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
index 43f5736..58c19f7 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraError.kt
@@ -186,6 +186,8 @@
             return topMethodName == "_enableShutterSound"
         }
     }
+
+    override fun toString(): String = "CameraError($value)"
 }
 
 // TODO(b/276918807): When we have CameraProperties, handle the exception on a more granular level.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 4f42a45..55aa136 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -35,6 +35,7 @@
 import androidx.camera.camera2.pipe.GraphState.GraphStateStarting
 import androidx.camera.camera2.pipe.GraphState.GraphStateStopped
 import androidx.camera.camera2.pipe.GraphState.GraphStateStopping
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.core.Log
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Deferred
@@ -154,6 +155,21 @@
     fun setSurface(stream: StreamId, surface: Surface?)
 
     /**
+     * CameraPipe allows setting the global audio restriction through [CameraPipe] and audio
+     * restrictions on individual [CameraGraph]s. When multiple settings are present, the highest
+     * level of audio restriction across global and individual [CameraGraph]s is used as the
+     * device's audio restriction
+     *
+     * Returns the mode of audio restriction associated with the [CameraGraph].
+     */
+    fun getAudioRestriction(): AudioRestrictionMode?
+
+    /**
+     * Sets the audio restriction of CameraGraph.
+     */
+    fun setAudioRestriction(mode: AudioRestrictionMode)
+
+    /**
      * This defines the configuration, flags, and pre-defined structure of a [CameraGraph] instance.
      * Note that for parameters, null is considered a valid value, and unset keys are ignored.
      *
@@ -659,6 +675,6 @@
     class GraphStateError(val cameraError: CameraError, val willAttemptRetry: Boolean) :
         GraphState() {
         override fun toString(): String =
-            super.toString() + "(cameraError = $cameraError, willAttemptRetry = $willAttemptRetry)"
+            super.toString() + "(cameraError=$cameraError, willAttemptRetry=$willAttemptRetry)"
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
index 51dc922..cef1f7c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraSurfaceManager.kt
@@ -123,13 +123,16 @@
             surfaceToken = SurfaceToken(surface)
             val newUseCount = (useCountMap[surface] ?: 0) + 1
             useCountMap[surface] = newUseCount
-            Log.debug {
-                "registerSurface: surface=$surface, " +
-                    "surfaceToken=$surfaceToken, newUseCount=$newUseCount" +
-                    (if (DEBUG) " from ${Log.readStackTrace()}" else "")
+            if (DEBUG) {
+                Log.debug {
+                    "registerSurface: surface=$surface, " +
+                        "surfaceToken=$surfaceToken, newUseCount=$newUseCount" +
+                        (if (DEBUG) " from ${Log.readStackTrace()}" else "")
+                }
             }
+
             if (newUseCount == 1) {
-                Log.debug { "Surface $surface has become active" }
+                Log.debug { "$surface for $surfaceToken is active" }
                 listenersToInvoke = listeners.toList()
             }
         }
@@ -148,13 +151,16 @@
             checkNotNull(useCount) { "Surface $surface ($surfaceToken) has no use count" }
             val newUseCount = useCount - 1
             useCountMap[surface] = newUseCount
-            Log.debug {
-                "onTokenClosed: surface=$surface, " +
-                    "surfaceToken=$surfaceToken, newUseCount=$newUseCount" +
-                    (if (DEBUG) " from ${Log.readStackTrace()}" else "")
+
+            if (DEBUG) {
+                Log.debug {
+                    "onTokenClosed: surface=$surface, " +
+                        "surfaceToken=$surfaceToken, newUseCount=$newUseCount" +
+                        (if (DEBUG) " from ${Log.readStackTrace()}" else "")
+                }
             }
             if (newUseCount == 0) {
-                Log.debug { "Surface $surface has become inactive" }
+                Log.debug { "$surface for $surfaceToken is inactive" }
                 listenersToInvoke = listeners.toList()
                 useCountMap.remove(surface)
             }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index dd42328..7e64fd1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -52,7 +52,7 @@
         }
 
         override fun toString(): String {
-            return name
+            return "Metadata.Key($name)"
         }
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
index 76585d3..6e00d10 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Requests.kt
@@ -243,14 +243,17 @@
 
     override fun toString(): String {
         val parametersString =
-            if (parameters.isEmpty()) "" else ", parameters=${Debug.formatParameterMap(parameters)}"
+            if (parameters.isEmpty()) {
+                ""
+            } else {
+                ", parameters=${Debug.formatParameterMap(parameters, limit = 5)}"
+            }
         val extrasString =
-            if (extras.isEmpty()) "" else ", extras=${Debug.formatParameterMap(extras)}"
+            if (extras.isEmpty()) "" else ", extras=${Debug.formatParameterMap(extras, limit = 5)}"
         val templateString = if (template == null) "" else ", template=$template"
         // Ignore listener count, always include stream list (required), and use super.toString to
         // reference the class name.
-        return "Request@${super.hashCode().toString(16)}(streams=$streams" +
-            "$parametersString$extrasString$templateString)"
+        return "Request(streams=$streams$templateString$parametersString$extrasString)"
     }
 }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
index 5da8a72..d009ee5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/StreamFormat.kt
@@ -134,6 +134,6 @@
                 YUY2 -> return "YUY2"
                 YV12 -> return "YV12"
             }
-            return "UNKNOWN-${this.value.toString(16)}"
+            return "UNKNOWN(${this.value.toString(16)})"
         }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt
new file mode 100644
index 0000000..481baa9
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/AudioRestrictionController.kt
@@ -0,0 +1,109 @@
+/*
+* Copyright 2024 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package androidx.camera.camera2.pipe.compat
+
+import android.hardware.camera2.CameraDevice
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_NONE
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION_SOUND
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Class that keeps the global audio restriction mode and audio restriction mode on each
+ * CameraGraph, and computes the final audio restriction mode based on the settings.
+ */
+@Singleton
+@RequiresApi(30)
+class AudioRestrictionController @Inject constructor() {
+    private val lock = Any()
+    var globalAudioRestrictionMode: AudioRestrictionMode = AUDIO_RESTRICTION_NONE
+        get() = synchronized(lock) { field }
+        set(value: AudioRestrictionMode) {
+            synchronized(lock) {
+                field = value
+                updateListenersMode()
+            }
+        }
+
+    private val audioRestrictionModeMap: MutableMap<CameraGraph, AudioRestrictionMode> =
+        mutableMapOf()
+    private val activeListeners: MutableSet<Listener> = mutableSetOf()
+
+    fun getCameraGraphAudioRestriction(cameraGraph: CameraGraph): AudioRestrictionMode {
+        return audioRestrictionModeMap.getOrDefault(cameraGraph, AUDIO_RESTRICTION_NONE)
+    }
+
+    fun setCameraGraphAudioRestriction(cameraGraph: CameraGraph, mode: AudioRestrictionMode) {
+        synchronized(lock) {
+            audioRestrictionModeMap[cameraGraph] = mode
+            updateListenersMode()
+        }
+    }
+
+    fun removeCameraGraph(cameraGraph: CameraGraph) {
+        synchronized(lock) {
+            audioRestrictionModeMap.remove(cameraGraph)
+            updateListenersMode()
+        }
+    }
+
+    @GuardedBy("lock")
+    private fun computeAudioRestrictionMode(): AudioRestrictionMode {
+        if (audioRestrictionModeMap.containsValue(AUDIO_RESTRICTION_VIBRATION_SOUND) ||
+            globalAudioRestrictionMode == AUDIO_RESTRICTION_VIBRATION_SOUND
+        ) {
+            return AUDIO_RESTRICTION_VIBRATION_SOUND
+        }
+        if (audioRestrictionModeMap.containsValue(AUDIO_RESTRICTION_VIBRATION) ||
+            globalAudioRestrictionMode == AUDIO_RESTRICTION_VIBRATION
+        ) {
+            return AUDIO_RESTRICTION_VIBRATION
+        }
+        return AUDIO_RESTRICTION_NONE
+    }
+
+    fun addListener(listener: Listener) {
+        synchronized(lock) {
+            activeListeners.add(listener)
+            val mode = computeAudioRestrictionMode()
+            listener.onCameraAudioRestrictionUpdated(mode)
+        }
+    }
+
+    fun removeListener(listener: Listener?) {
+        synchronized(lock) {
+            activeListeners.remove(listener)
+        }
+    }
+
+    @GuardedBy("lock")
+    private fun updateListenersMode() {
+        val mode = computeAudioRestrictionMode()
+        for (listener in activeListeners) {
+            listener.onCameraAudioRestrictionUpdated(mode)
+        }
+    }
+
+    interface Listener {
+        /** @see CameraDevice.getCameraAudioRestriction */
+        fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode)
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index 74851e8..815114b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -176,12 +176,12 @@
     private val _keys: Lazy<Set<CameraCharacteristics.Key<*>>> =
         lazy(LazyThreadSafetyMode.PUBLICATION) {
             try {
-                Debug.trace("Camera-${camera.value}#keys") {
+                Debug.trace("$camera#keys") {
                     characteristics.keys.orEmpty().toSet()
                 }
             } catch (e: AssertionError) {
                 Log.warn(e) {
-                    "Failed to getKeys from Camera-${camera.value}"
+                    "Failed to getKeys from $camera}"
                 }
                 emptySet()
             }
@@ -190,12 +190,12 @@
     private val _requestKeys: Lazy<Set<CaptureRequest.Key<*>>> =
         lazy(LazyThreadSafetyMode.PUBLICATION) {
             try {
-                Debug.trace("Camera-${camera.value}#availableCaptureRequestKeys") {
+                Debug.trace("$camera#availableCaptureRequestKeys") {
                     characteristics.availableCaptureRequestKeys.orEmpty().toSet()
                 }
             } catch (e: AssertionError) {
                 Log.warn(e) {
-                    "Failed to getAvailableCaptureRequestKeys from Camera-${camera.value}"
+                    "Failed to getAvailableCaptureRequestKeys from $camera"
                 }
                 emptySet()
             }
@@ -204,12 +204,12 @@
     private val _resultKeys: Lazy<Set<CaptureResult.Key<*>>> =
         lazy(LazyThreadSafetyMode.PUBLICATION) {
             try {
-                Debug.trace("Camera-${camera.value}#availableCaptureResultKeys") {
+                Debug.trace("$camera#availableCaptureResultKeys") {
                     characteristics.availableCaptureResultKeys.orEmpty().toSet()
                 }
             } catch (e: AssertionError) {
                 Log.warn(e) {
-                    "Failed to getAvailableCaptureResultKeys from Camera-${camera.value}"
+                    "Failed to getAvailableCaptureResultKeys from $camera"
                 }
                 emptySet()
             }
@@ -221,7 +221,7 @@
                 emptySet()
             } else {
                 try {
-                    Debug.trace("Camera-${camera.value}#physicalCameraIds") {
+                    Debug.trace("$camera#physicalCameraIds") {
                         val ids = Api28Compat.getPhysicalCameraIds(characteristics)
                         Log.info { "Loaded physicalCameraIds from $camera: $ids" }
 
@@ -231,10 +231,10 @@
                             .toSet()
                     }
                 } catch (e: AssertionError) {
-                    Log.warn(e) { "Failed to getPhysicalCameraIds from Camera-${camera.value}" }
+                    Log.warn(e) { "Failed to getPhysicalCameraIds from $camera" }
                     emptySet()
                 } catch (e: NullPointerException) {
-                    Log.warn(e) { "Failed to getPhysicalCameraIds from Camera-${camera.value}" }
+                    Log.warn(e) { "Failed to getPhysicalCameraIds from $camera" }
                     emptySet()
                 }
             }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index d2d8495..cf2716c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -310,6 +310,9 @@
     }
 
     override fun close() = synchronized(lock) {
+        if (closed) {
+            return@synchronized
+        }
         // Close should not shut down
         Debug.trace("$this#close") {
             if (shouldWaitForRepeatingRequest) {
@@ -329,10 +332,11 @@
             }
             closed = true
         }
+        imageWriter?.close()
     }
 
     override fun toString(): String {
-        return "Camera2RequestProcessor-$debugId"
+        return "Camera2CaptureSequenceProcessor-$debugId"
     }
 
     /** The [ImageWriterWrapper] is created once per capture session when the capture
@@ -460,15 +464,13 @@
 
                 val surface = surfaceMap[stream]
                 if (surface != null) {
-                    Log.debug { "  Binding $stream to $surface" }
-
                     // TODO(codelogic) There should be a more efficient way to do these lookups than
                     // having two maps.
                     surfaceToStreamMap[surface] = stream
                     streamToSurfaceMap[stream] = surface
                     hasSurface = true
                 } else if (REQUIRE_SURFACE_FOR_ALL_STREAMS) {
-                    Log.info { "  Failed to bind surface to $stream" }
+                    Log.info { "  Failed to bind surface for $stream" }
 
                     // If requireStreams is set we are required to map every stream to a valid
                     // Surface object for this request. If this condition is violated, then we
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
index abf3a2f..8b5fc4b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2DeviceCloser.kt
@@ -19,6 +19,7 @@
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCaptureSession
 import android.hardware.camera2.CameraDevice
+import android.os.Build
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
@@ -37,6 +38,7 @@
         cameraDevice: CameraDevice? = null,
         closeUnderError: Boolean = false,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     )
 }
 
@@ -51,24 +53,42 @@
         cameraDevice: CameraDevice?,
         closeUnderError: Boolean,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     ) {
-        Log.debug { "Closing $cameraDeviceWrapper and/or $cameraDevice" }
         val unwrappedCameraDevice = cameraDeviceWrapper?.unwrapAs(CameraDevice::class)
         if (unwrappedCameraDevice != null) {
             cameraDevice?.let {
                 check(unwrappedCameraDevice.id == it.id) {
-                    "Unwrapped camera device has camera ID ${unwrappedCameraDevice.id}, " + "" +
-                        "but the accompanied camera device has camera ID ${it.id}"
+                    "Unwrapped camera device has camera ID ${unwrappedCameraDevice.id}, " +
+                        "but the wrapped camera device has camera ID ${it.id}!"
                 }
             }
-            closeCameraDevice(unwrappedCameraDevice, closeUnderError, androidCameraState)
+            closeCameraDevice(
+                unwrappedCameraDevice,
+                closeUnderError,
+                androidCameraState
+            )
             cameraDeviceWrapper.onDeviceClosed()
+            /**
+             * Only remove the audio restriction when CameraDeviceWrapper is present.
+             * When closeCamera is called without a CameraDeviceWrapper, that means a wrapper
+             * hadn't been created for the opened camera.
+             */
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                audioRestriction?.removeListener(cameraDeviceWrapper)
+            }
 
             // We only need to close the device once (don't want to create another capture session).
             // Return here.
             return
         }
-        cameraDevice?.let { closeCameraDevice(it, closeUnderError, androidCameraState) }
+        cameraDevice?.let {
+            closeCameraDevice(
+                it,
+                closeUnderError,
+                androidCameraState
+            )
+        }
     }
 
     private fun closeCameraDevice(
@@ -84,16 +104,15 @@
                 Log.debug { "Empty capture session quirk completed" }
             }
         }
-        Log.debug { "Closing $cameraDevice" }
         Threading.runBlockingWithTimeout(threads.backgroundDispatcher, 5000L) {
             cameraDevice.closeWithTrace()
         }
         if (camera2Quirks.shouldWaitForCameraDeviceOnClosed(cameraId)) {
-            Log.debug { "Waiting for camera device to be completely closed" }
+            Log.debug { "Waiting for OnClosed from $cameraId" }
             if (androidCameraState.awaitCameraDeviceClosed(timeoutMillis = 5000)) {
-                Log.debug { "Camera device is closed" }
+                Log.debug { "Received OnClosed for $cameraId" }
             } else {
-                Log.warn { "Failed to wait for camera device to close after 5000ms" }
+                Log.warn { "Failed to close $cameraId after 500ms" }
             }
         }
     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
index ce3d323..42577fc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2MetadataCache.kt
@@ -100,7 +100,7 @@
     }
 
     override fun awaitCameraMetadata(cameraId: CameraId): CameraMetadata {
-        return Debug.trace("Camera-${cameraId.value}#awaitMetadata") {
+        return Debug.trace("$cameraId#awaitMetadata") {
             synchronized(cache) {
                 val existing = cache[cameraId.value]
                 if (existing != null) {
@@ -120,7 +120,7 @@
         extension: Int
     ): CameraExtensionMetadata {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-            return Debug.trace("Camera-${cameraId.value}#awaitExtensionMetadata") {
+            return Debug.trace("$cameraId#awaitExtensionMetadata") {
                 synchronized(extensionCache) {
                     val existing = extensionCache[cameraId.value]
                     if (existing != null) {
@@ -147,7 +147,7 @@
     ): Camera2CameraMetadata {
         val start = Timestamps.now(timeSource)
 
-        return Debug.trace("Camera-${cameraId.value}#readCameraMetadata") {
+        return Debug.trace("$cameraId#readCameraMetadata") {
             try {
                 Log.debug { "Loading metadata for $cameraId" }
                 val cameraManager =
@@ -217,7 +217,7 @@
     ): Camera2CameraExtensionMetadata {
         val start = Timestamps.now(timeSource)
 
-        return Debug.trace("Camera-${cameraId.value}#readCameraExtensionMetadata") {
+        return Debug.trace("$cameraId#readCameraExtensionMetadata") {
             try {
                 Log.debug { "Loading extension metadata for $cameraId" }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index fd97a9b..c7460ba 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -50,7 +50,7 @@
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-internal interface CameraDeviceWrapper : UnsafeWrapper {
+internal interface CameraDeviceWrapper : UnsafeWrapper, AudioRestrictionController.Listener {
     /** @see [CameraDevice.getId] */
     val cameraId: CameraId
 
@@ -110,11 +110,7 @@
 
     /** @see CameraDevice.getCameraAudioRestriction */
     @RequiresApi(Build.VERSION_CODES.R)
-    fun getCameraAudioRestriction(): AudioRestrictionMode
-
-    /** @see CameraDevice.setCameraAudioRestriction */
-    @RequiresApi(Build.VERSION_CODES.R)
-    fun setCameraAudioRestriction(mode: AudioRestrictionMode)
+    fun getCameraAudioRestriction(): AudioRestrictionMode?
 }
 
 internal fun CameraDevice?.closeWithTrace() {
@@ -466,8 +462,10 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
-        Api30Compat.setCameraAudioRestriction(cameraDevice, mode.value)
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
+        catchAndReportCameraExceptions(cameraId, cameraErrorListener) {
+            Api30Compat.setCameraAudioRestriction(cameraDevice, mode.value)
+        }
     }
 
     override fun onDeviceClosed() {
@@ -481,6 +479,8 @@
             CameraDevice::class -> cameraDevice as T
             else -> null
         }
+
+    override fun toString(): String = "AndroidCameraDevice(camera=$cameraId)"
 }
 
 /**
@@ -647,11 +647,14 @@
     }
 
     @RequiresApi(30)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
-        androidCameraDevice.setCameraAudioRestriction(mode)
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
+        androidCameraDevice.onCameraAudioRestrictionUpdated(mode)
     }
 }
 
+/**
+ * @see [CameraDevice.AUDIO_RESTRICTION_NONE] and other constants.
+ */
 @JvmInline
 value class AudioRestrictionMode(val value: Int) {
     companion object {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
index 4a8dfcd..bc9bbcf 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/RetryingCameraStateOpener.kt
@@ -82,7 +82,7 @@
     )
     override fun openCamera(cameraId: CameraId, stateCallback: StateCallback) {
         val instance = cameraManager.get()
-        Debug.trace("CameraDevice-${cameraId.value}#openCamera") {
+        Debug.trace("$cameraId#openCamera") {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                 Api28Compat.openCamera(
                     instance, cameraId.value, threads.camera2Executor, stateCallback
@@ -173,6 +173,7 @@
         cameraId: CameraId,
         attempts: Int,
         requestTimestamp: TimestampNs,
+        audioRestriction: AudioRestrictionController? = null
     ): OpenCameraResult {
         val metadata = camera2MetadataProvider.getCameraMetadata(cameraId)
         val cameraState =
@@ -186,7 +187,10 @@
                 camera2DeviceCloser,
                 threads,
                 cameraInteropConfig?.cameraDeviceStateCallback,
-                cameraInteropConfig?.cameraSessionStateCallback
+                cameraInteropConfig?.cameraSessionStateCallback,
+                /** interopExtensionSessionStateCallback= */
+                null,
+                audioRestriction
             )
 
         try {
@@ -229,6 +233,7 @@
     private val timeSource: TimeSource,
     private val devicePolicyManager: DevicePolicyManagerWrapper,
     private val cameraInteropConfig: CameraPipe.CameraInteropConfig?,
+    private val audioRestriction: AudioRestrictionController? = null
 ) {
     internal suspend fun openCameraWithRetry(
         cameraId: CameraId,
@@ -245,6 +250,7 @@
                     cameraId,
                     attempts,
                     requestTimestamp,
+                    audioRestriction
                 )
             val elapsed = Timestamps.now(timeSource) - requestTimestamp
             with(result) {
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
index eb3baea..3df1e09 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CameraCaptureSession.StateCallback
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CameraExtensionSession
+import android.os.Build
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraError
@@ -252,7 +253,8 @@
     private val threads: Threads,
     private val interopDeviceStateCallback: CameraDevice.StateCallback? = null,
     private val interopSessionStateCallback: StateCallback? = null,
-    private val interopExtensionSessionStateCallback: CameraExtensionSession.StateCallback? = null
+    private val interopExtensionSessionStateCallback: CameraExtensionSession.StateCallback? = null,
+    private val audioRestriction: AudioRestrictionController? = null
 ) : CameraDevice.StateCallback() {
     private val debugId = androidCameraDebugIds.incrementAndGet()
     private val lock = Any()
@@ -309,7 +311,7 @@
         val openedTimestamp = Timestamps.now(timeSource)
         openTimestampNanos = openedTimestamp
 
-        Debug.traceStart { "Camera-${cameraId.value}#onOpened" }
+        Debug.traceStart { "$cameraId#onOpened" }
         Log.info {
             val attemptDuration = openedTimestamp - requestTimestampNanos
             val totalDuration = openedTimestamp - attemptTimestampNanos
@@ -334,25 +336,28 @@
             camera2DeviceCloser.closeCamera(
                 cameraDevice = cameraDevice,
                 closeUnderError = currentCloseInfo.errorCode != null,
-                androidCameraState = this
+                androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             return
         }
 
         // Update _state.value _without_ holding the lock. This may block the calling thread for a
         // while if it synchronously calls createCaptureSession.
+        val androidCameraDevice = AndroidCameraDevice(
+            metadata,
+            cameraDevice,
+            cameraId,
+            cameraErrorListener,
+            interopSessionStateCallback,
+            interopExtensionSessionStateCallback,
+            threads
+        )
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            audioRestriction?.addListener(androidCameraDevice)
+        }
         _state.value =
-            CameraStateOpen(
-                AndroidCameraDevice(
-                    metadata,
-                    cameraDevice,
-                    cameraId,
-                    cameraErrorListener,
-                    interopSessionStateCallback,
-                    interopExtensionSessionStateCallback,
-                    threads
-                )
-            )
+            CameraStateOpen(androidCameraDevice)
 
         // Check to see if we received close() or other events in the meantime.
         val closeInfo =
@@ -365,7 +370,8 @@
             camera2DeviceCloser.closeCamera(
                 cameraDevice = cameraDevice,
                 closeUnderError = closeInfo.errorCode != null,
-                androidCameraState = this
+                androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             _state.value = computeClosedState(closeInfo)
         }
@@ -374,7 +380,7 @@
 
     override fun onDisconnected(cameraDevice: CameraDevice) {
         check(cameraDevice.id == cameraId.value)
-        Debug.traceStart { "Camera-${cameraId.value}#onDisconnected" }
+        Debug.traceStart { "$cameraId#onDisconnected" }
         Log.debug { "$cameraId: onDisconnected" }
         cameraDeviceClosed.countDown()
 
@@ -391,7 +397,7 @@
 
     override fun onError(cameraDevice: CameraDevice, errorCode: Int) {
         check(cameraDevice.id == cameraId.value)
-        Debug.traceStart { "Camera-${cameraId.value}#onError-$errorCode" }
+        Debug.traceStart { "$cameraId#onError-$errorCode" }
         Log.debug { "$cameraId: onError $errorCode" }
         cameraDeviceClosed.countDown()
 
@@ -405,7 +411,7 @@
 
     override fun onClosed(cameraDevice: CameraDevice) {
         check(cameraDevice.id == cameraId.value)
-        Debug.traceStart { "Camera-${cameraId.value}#onClosed" }
+        Debug.traceStart { "$cameraId#onClosed" }
         Log.debug { "$cameraId: onClosed" }
         cameraDeviceClosed.countDown()
 
@@ -471,6 +477,7 @@
                 cameraDevice,
                 closeUnderError = closeInfo.errorCode != null,
                 androidCameraState = this,
+                audioRestriction = audioRestriction
             )
             _state.value = computeClosedState(closeInfo)
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
index 7b7ffb4..967eb19 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/config/CameraGraphComponent.kt
@@ -30,6 +30,7 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.core.Threads
+import androidx.camera.camera2.pipe.graph.CameraGraphId
 import androidx.camera.camera2.pipe.graph.CameraGraphImpl
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.GraphProcessor
@@ -104,6 +105,12 @@
     companion object {
         @CameraGraphScope
         @Provides
+        fun provideCameraGraphId(): CameraGraphId {
+            return CameraGraphId.nextId()
+        }
+
+        @CameraGraphScope
+        @Provides
         @ForCameraGraph
         fun provideCameraGraphCoroutineScope(threads: Threads): CoroutineScope {
             return CoroutineScope(threads.lightweightDispatcher.plus(CoroutineName("CXCP-Graph")))
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
index bf120f1..2548cff 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/core/Debug.kt
@@ -25,6 +25,7 @@
 import android.hardware.camera2.CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.params.MeteringRectangle
 import android.os.Build
 import android.os.Trace
 import androidx.annotation.RequiresApi
@@ -73,17 +74,7 @@
                 append("$name: (None)\n")
             } else {
                 append("${name}\n")
-                val parametersString: List<Pair<String, Any?>> =
-                    parameters.map {
-                        when (val key = it.key) {
-                            is CameraCharacteristics.Key<*> -> key.name
-                            is CaptureRequest.Key<*> -> key.name
-                            is CaptureResult.Key<*> -> key.name
-                            else -> key.toString()
-                        } to it.value
-                    }
-                parametersString
-                    .sortedBy { it.first }
+                parametersToSortedStringPairs(parameters)
                     .forEach { append("  ${it.first.padEnd(50, ' ')} ${it.second}\n") }
             }
         }
@@ -94,23 +85,36 @@
      *
      * Example: `[abc.xyz=1, abc.zyx=something]`
      */
-    fun formatParameterMap(parameters: Map<*, Any?>): String {
-        return parameters.map {
-            when (val key = it.key) {
-                is CameraCharacteristics.Key<*> -> key.name
-                is CaptureRequest.Key<*> -> key.name
-                is CaptureResult.Key<*> -> key.name
-                else -> key.toString()
-            } to it.value
-        }
-            .sortedBy { it.first }
+    fun formatParameterMap(parameters: Map<*, Any?>, limit: Int = -1): String {
+        return parametersToSortedStringPairs(parameters)
             .joinToString(
-                separator = ", ",
                 prefix = "{",
-                postfix = "}"
+                postfix = "}",
+                limit = limit
             ) { "${it.first}=${it.second}" }
     }
 
+    private fun parametersToSortedStringPairs(
+        parameters: Map<*, Any?>
+    ): List<Pair<String, String>> = parameters.map {
+        keyNameToString(it.key) to valueToString(it.value)
+    }.sortedBy { it.first }
+
+    private fun keyNameToString(key: Any?): String = when (key) {
+        is CameraCharacteristics.Key<*> -> key.name
+        is CaptureRequest.Key<*> -> key.name
+        is CaptureResult.Key<*> -> key.name
+        else -> key.toString()
+    }
+
+    /* Utility for cleaning up some verbose value types for logs */
+    private fun valueToString(value: Any?): String = when (value) {
+        is MeteringRectangle -> "[x=${value.x}, y=${value.y}, " +
+            "w=${value.width}, h=${value.height}, weight=${value.meteringWeight}"
+
+        else -> value.toString()
+    }
+
     fun formatCameraGraphProperties(
         metadata: CameraMetadata,
         graphConfig: CameraGraph.Config,
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphId.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphId.kt
new file mode 100644
index 0000000..80040f7
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphId.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.graph
+
+import androidx.camera.camera2.pipe.CameraGraph
+import kotlinx.atomicfu.atomic
+
+/**
+ * Identifier for a specific [CameraGraph] that can be used to standardize toString methods and as a
+ * key in maps without holding a reference to a [CameraGraph] object, which could lead to accidental
+ * memory leaks and circular dependencies.
+ */
+internal class CameraGraphId private constructor(private val name: String) {
+    override fun toString(): String = name
+
+    companion object {
+        private val cameraGraphIds = atomic(0)
+
+        /**
+         * Create the next CameraGraphId based on a global incrementing counter. This is
+         * intentionally worded as "CameraGraph" instead of "CameraGraphId" since it is used
+         * directly as the toString representation for a [CameraGraph].
+         */
+        fun nextId(): CameraGraphId {
+            return CameraGraphId("CameraGraph-${cameraGraphIds.incrementAndGet()}")
+        }
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
index b9f4539..d6b45bc 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/CameraGraphImpl.kt
@@ -26,6 +26,8 @@
 import androidx.camera.camera2.pipe.GraphState
 import androidx.camera.camera2.pipe.StreamGraph
 import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.config.CameraGraphScope
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.Log
@@ -47,8 +49,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.sync.Mutex
 
-internal val cameraGraphIds = atomic(0)
-
 @RequiresApi(21)
 @CameraGraphScope
 internal class CameraGraphImpl
@@ -56,6 +56,7 @@
 constructor(
     graphConfig: CameraGraph.Config,
     metadata: CameraMetadata,
+    private val cameraGraphId: CameraGraphId,
     private val graphLifecycleManager: GraphLifecycleManager,
     private val graphProcessor: GraphProcessor,
     private val graphListener: GraphListener,
@@ -67,8 +68,8 @@
     private val listener3A: Listener3A,
     private val frameDistributor: FrameDistributor,
     private val frameCaptureQueue: FrameCaptureQueue,
+    private val audioRestriction: AudioRestrictionController? = null
 ) : CameraGraph {
-    private val debugId = cameraGraphIds.incrementAndGet()
     private val sessionMutex = Mutex()
     private val controller3A = Controller3A(graphProcessor, metadata, graphState3A, listener3A)
     private val closed = atomic(false)
@@ -206,6 +207,19 @@
         Debug.traceStop()
     }
 
+    override fun getAudioRestriction(): AudioRestrictionMode? {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            return audioRestriction?.getCameraGraphAudioRestriction(this)
+        }
+        return null
+    }
+
+    override fun setAudioRestriction(mode: AudioRestrictionMode) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            audioRestriction?.setCameraGraphAudioRestriction(this, mode)
+        }
+    }
+
     override fun close() {
         if (closed.compareAndSet(expect = false, update = true)) {
             Debug.traceStart { "$this#close" }
@@ -215,9 +229,12 @@
             frameDistributor.close()
             frameCaptureQueue.close()
             surfaceGraph.close()
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                audioRestriction?.removeCameraGraph(this)
+            }
             Debug.traceStop()
         }
     }
 
-    override fun toString(): String = "CameraGraph-$debugId"
+    override fun toString(): String = cameraGraphId.toString()
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index fc67120..1524e89 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -131,6 +131,7 @@
 @Inject
 constructor(
     private val threads: Threads,
+    private val cameraGraphId: CameraGraphId,
     private val cameraGraphConfig: CameraGraph.Config,
     private val graphState3A: GraphState3A,
     @ForCameraGraph private val graphScope: CoroutineScope,
@@ -648,4 +649,6 @@
             }
         }
     }
+
+    override fun toString(): String = "GraphProcessor(cameraGraph: $cameraGraphId)"
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
index de2945f..ed55f4a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/StreamGraphImpl.kt
@@ -280,7 +280,7 @@
     }
 
     override fun toString(): String {
-        return "StreamGraphImpl $_streamMap"
+        return "StreamGraph($_streamMap)"
     }
 
     /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
index b4eefe3..36e09ab 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
@@ -71,8 +71,7 @@
     }
 
     override fun toString(): String {
-        return "ImageWriter-${StreamFormat(imageWriter.format).name}-" +
-            "inputStreamId$inputStreamId"
+        return "ImageWriter-${StreamFormat(imageWriter.format).name}-$inputStreamId"
     }
 
     companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
index 16faf80..e6b3c6e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
@@ -60,9 +60,7 @@
     @Test
     fun requestHasNiceLoggingString() {
         val request1 = Request(listOf(StreamId(1)))
-        val request2 = Request(listOf(StreamId(1)))
 
-        assertThat("$request1").isNotEqualTo("$request2")
         assertThat("$request1").contains("1")
         assertThat("$request1").contains("Request")
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt
new file mode 100644
index 0000000..5421440
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/AudioRestrictionControllerTest.kt
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.compat
+
+import android.os.Build
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode.Companion.AUDIO_RESTRICTION_VIBRATION_SOUND
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.R)
+class AudioRestrictionControllerTest {
+    private val cameraGraph1: CameraGraph = mock()
+    private val cameraGraph2: CameraGraph = mock()
+    private val listener1: AudioRestrictionController.Listener = mock()
+    private val listener2: AudioRestrictionController.Listener = mock()
+
+    @Test
+    fun setAudioRestrictionMode_ListenerUpdatedToHighestMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph2,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+    }
+
+    @Test
+    fun setGlobalAudioRestrictionMode_ListenerUpdatedToHighestMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(AUDIO_RESTRICTION_VIBRATION)
+
+        audioRestriction.globalAudioRestrictionMode = AUDIO_RESTRICTION_VIBRATION_SOUND
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+    }
+
+    @Test
+    fun setAudioRestrictionMode_lowerModeNotOverrideHigherMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph2, AUDIO_RESTRICTION_VIBRATION)
+
+        // Whenever a setter method is called, an update should be called on the listener
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener1, never()).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun setGlobalAudioRestrictionMode_lowerModeNotOverrideHigherMode() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.globalAudioRestrictionMode = AUDIO_RESTRICTION_VIBRATION
+
+        // Whenever a setter method is called, an update should be called on the listener
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        verify(listener1, never()).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun removeCameraGraphAudioRestriction_associatedModeUpdated() {
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(
+            cameraGraph1,
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph2, AUDIO_RESTRICTION_VIBRATION)
+
+        verify(listener1, times(2)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION_SOUND
+        )
+
+        audioRestriction.removeCameraGraph(cameraGraph1)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(
+            AUDIO_RESTRICTION_VIBRATION
+        )
+    }
+
+    @Test
+    fun addListenerAfterUpdateMode_newListenerUpdated() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, mode)
+        audioRestriction.addListener(listener2)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+
+    @Test
+    fun setRestrictionBeforeAddingListener_listenerSetToUpdatedMode() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+
+        audioRestriction.globalAudioRestrictionMode = mode
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+
+        verify(listener1, times(1)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+
+    @Test
+    fun removedListener_noLongerUpdated() {
+        val mode = AUDIO_RESTRICTION_VIBRATION
+        val audioRestriction = AudioRestrictionController()
+        audioRestriction.addListener(listener1)
+        audioRestriction.addListener(listener2)
+        audioRestriction.removeListener(listener1)
+
+        audioRestriction.setCameraGraphAudioRestriction(cameraGraph1, mode)
+
+        verify(listener1, times(0)).onCameraAudioRestrictionUpdated(mode)
+        verify(listener2, times(1)).onCameraAudioRestrictionUpdated(mode)
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
index 24c59ce..5563e78 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/CameraGraphImplTest.kt
@@ -29,6 +29,8 @@
 import androidx.camera.camera2.pipe.CameraSurfaceManager
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
+import androidx.camera.camera2.pipe.compat.AudioRestrictionMode
 import androidx.camera.camera2.pipe.internal.CameraBackendsImpl
 import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
 import androidx.camera.camera2.pipe.internal.FrameDistributor
@@ -42,6 +44,7 @@
 import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertEquals
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.advanceUntilIdle
@@ -113,10 +116,19 @@
             cameraSurfaceManager,
             emptyMap()
         )
+        val audioRestriction: AudioRestrictionController? =
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+                AudioRestrictionController()
+            } else {
+                null
+            }
+
+        val cameraGraphId = CameraGraphId.nextId()
         val graph =
             CameraGraphImpl(
                 graphConfig,
                 metadata,
+                cameraGraphId,
                 graphLifecycleManager,
                 fakeGraphProcessor,
                 fakeGraphProcessor,
@@ -127,7 +139,8 @@
                 GraphState3A(),
                 Listener3A(),
                 frameDistributor,
-                frameCaptureQueue
+                frameCaptureQueue,
+                audioRestriction
             )
         stream1 =
             checkNotNull(graph.streams[stream1Config]) {
@@ -259,4 +272,13 @@
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
         verify(fakeSurfaceListener, times(1)).onSurfaceInactive(eq(imageReader1.surface))
     }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.R)
+    fun setAudioRestriction_setValueSuccessfully() = runTest {
+        val mode = AudioRestrictionMode(0)
+        val cameraGraph = initializeCameraGraphImpl(this)
+        cameraGraph.setAudioRestriction(mode)
+        assertEquals(mode, cameraGraph.getAudioRestriction())
+    }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 21931e0..74f6f70 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -78,6 +78,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -99,6 +100,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -122,6 +124,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -149,6 +152,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -168,6 +172,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -199,6 +204,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -236,6 +242,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -260,6 +267,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -294,6 +302,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -321,6 +330,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -343,6 +353,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -378,6 +389,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -410,6 +422,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -436,6 +449,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -462,6 +476,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -501,6 +516,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -520,6 +536,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
@@ -539,6 +556,7 @@
         val graphProcessor =
             GraphProcessorImpl(
                 FakeThreads.fromTestScope(this),
+                CameraGraphId.nextId(),
                 FakeGraphConfigs.graphConfig,
                 graphState3A,
                 this,
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
index 7b6cc46..6236a42 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCamera2DeviceCloser.kt
@@ -17,16 +17,21 @@
 package androidx.camera.camera2.pipe.testing
 
 import android.hardware.camera2.CameraDevice
+import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.compat.AndroidCameraState
+import androidx.camera.camera2.pipe.compat.AudioRestrictionController
 import androidx.camera.camera2.pipe.compat.Camera2DeviceCloser
 import androidx.camera.camera2.pipe.compat.CameraDeviceWrapper
 
+@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 internal class FakeCamera2DeviceCloser : Camera2DeviceCloser {
     override fun closeCamera(
         cameraDeviceWrapper: CameraDeviceWrapper?,
         cameraDevice: CameraDevice?,
         closeUnderError: Boolean,
         androidCameraState: AndroidCameraState,
+        audioRestriction: AudioRestrictionController?
     ) {
         cameraDeviceWrapper?.onDeviceClosed()
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
index ff8f9d0..38d1748 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
@@ -120,7 +120,7 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.R)
-    override fun setCameraAudioRestriction(mode: AudioRestrictionMode) {
+    override fun onCameraAudioRestrictionUpdated(mode: AudioRestrictionMode) {
         fakeCamera.cameraDevice.cameraAudioRestriction = mode.value
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 5bc7186..e6361d0 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1067,7 +1067,7 @@
                     removeMeteringRepeating();
                 } else {
                     // Other normal cases, do nothing.
-                    Logger.d(TAG, "mMeteringRepeating is ATTACHED, "
+                    Logger.d(TAG, "No need to remove a previous mMeteringRepeating, "
                             + "SessionConfig Surfaces: " + sizeSessionSurfaces + ", "
                             + "CaptureConfig Surfaces: " + sizeRepeatingSurfaces);
                 }
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
index eabc522b..cdc6214 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
@@ -37,11 +37,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 
 @Composable
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
index 7053aa6..e2b31f8 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -1387,7 +1387,7 @@
 
         zoomRatio =
             (zoomRatio * scaleFactor).coerceIn(
-                1.0f,
+                ZoomUtil.minZoom(cameraManager.getCameraCharacteristics(currentCameraId)),
                 ZoomUtil.maxZoom(cameraManager.getCameraCharacteristics(currentCameraId))
             )
         Log.d(TAG, "onScale: $zoomRatio")
@@ -1428,8 +1428,11 @@
             return availableCaptureRequestKeys.contains(CaptureRequest.CONTROL_ZOOM_RATIO)
         }
 
+        fun minZoom(characteristics: CameraCharacteristics): Float =
+            characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)?.lower ?: 1.0f
+
         fun maxZoom(characteristics: CameraCharacteristics): Float =
-            characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM) ?: 1.0f
+            characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE)?.upper ?: 1.0f
     }
 }
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
index 1f54e41..f6ed832 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
@@ -55,10 +55,10 @@
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Observer
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "ImageCaptureScreen"
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 71f10a5..4bf2a6e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -47,10 +47,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Observer
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "VideoCaptureScreen"
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
index 0c95af8..73e13bb 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/viewfinder/ViewfinderScreen.kt
@@ -41,9 +41,9 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 private const val TAG = "ViewfinderScreen"
 
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index e82b0af..112a597 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -320,11 +320,10 @@
   }
 
   public final class ReceiveContentKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.content.TransferableContent,androidx.compose.foundation.content.TransferableContent?> onReceive);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier contentReceiver(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface ReceiveContentListener {
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface ReceiveContentListener {
     method public default void onDragEnd();
     method public default void onDragEnter();
     method public default void onDragExit();
@@ -357,7 +356,7 @@
   }
 
   public final class TransferableContent_androidKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consumeEach(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consume(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static boolean hasMediaType(androidx.compose.foundation.content.TransferableContent, androidx.compose.foundation.content.MediaType mediaType);
   }
 
@@ -1536,7 +1535,7 @@
 package androidx.compose.foundation.text {
 
   public final class BasicSecureTextFieldKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text.input.ImeActionHandler? onSubmit, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional int textObfuscationMode, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator);
   }
 
   public final class BasicTextFieldKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 5841c91..ea547b8 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -322,11 +322,10 @@
   }
 
   public final class ReceiveContentKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier receiveContent(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.content.TransferableContent,androidx.compose.foundation.content.TransferableContent?> onReceive);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier contentReceiver(androidx.compose.ui.Modifier, java.util.Set<androidx.compose.foundation.content.MediaType> hintMediaTypes, androidx.compose.foundation.content.ReceiveContentListener receiveContentListener);
   }
 
-  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public interface ReceiveContentListener {
+  @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public fun interface ReceiveContentListener {
     method public default void onDragEnd();
     method public default void onDragEnter();
     method public default void onDragExit();
@@ -359,7 +358,7 @@
   }
 
   public final class TransferableContent_androidKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consumeEach(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.content.TransferableContent? consume(androidx.compose.foundation.content.TransferableContent, kotlin.jvm.functions.Function1<? super android.content.ClipData.Item,java.lang.Boolean> predicate);
     method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi public static boolean hasMediaType(androidx.compose.foundation.content.TransferableContent, androidx.compose.foundation.content.MediaType mediaType);
   }
 
@@ -1538,7 +1537,7 @@
 package androidx.compose.foundation.text {
 
   public final class BasicSecureTextFieldKt {
-    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text.input.ImeActionHandler? onSubmit, optional int imeAction, optional int textObfuscationMode, optional int keyboardType, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator, optional androidx.compose.foundation.ScrollState scrollState);
+    method @SuppressCompatibility @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void BasicSecureTextField(androidx.compose.foundation.text.input.TextFieldState state, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.text.input.ImeActionHandler? onSubmit, optional boolean enabled, optional androidx.compose.foundation.text.input.InputTransformation? inputTransformation, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.unit.Density,? super kotlin.jvm.functions.Function0<androidx.compose.ui.text.TextLayoutResult?>,kotlin.Unit>? onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional int textObfuscationMode, optional androidx.compose.foundation.text.input.TextFieldDecorator? decorator);
   }
 
   public final class BasicTextFieldKt {
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 006e233..dc709d1 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
@@ -37,6 +37,7 @@
 import androidx.compose.foundation.samples.BasicTextFieldUndoSample
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
+import androidx.compose.ui.text.samples.AnnotatedStringFromHtml
 
 val TextDemos = DemoCategory(
     "Text",
@@ -213,5 +214,6 @@
             )
         ),
         ComposableDemo("Text Pointer Icon") { TextPointerIconDemo() },
+        ComposableDemo("Html") { AnnotatedStringFromHtml() }
     )
 )
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
index 1e28470..cbfdb0f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/BasicSecureTextFieldDemos.kt
@@ -27,26 +27,23 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicSecureTextField
+import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.text.input.TextFieldState
 import androidx.compose.foundation.text.input.TextObfuscationMode
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.Button
-import androidx.compose.material.Icon
-import androidx.compose.material.IconToggleButton
 import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Info
-import androidx.compose.material.icons.filled.Warning
+import androidx.compose.material.TextButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
 import androidx.core.text.isDigitsOnly
@@ -103,8 +100,10 @@
                 new.revertAllChanges()
             }
         },
-        keyboardType = KeyboardType.NumberPassword,
-        imeAction = ImeAction.Default,
+        keyboardOptions = KeyboardOptions(
+            autoCorrect = false,
+            keyboardType = KeyboardType.NumberPassword
+        ),
         modifier = demoTextFieldModifiers
     )
 }
@@ -114,7 +113,7 @@
 fun PasswordToggleVisibilityDemo() {
     val state = remember { TextFieldState() }
     var visible by remember { mutableStateOf(false) }
-    Row(Modifier.fillMaxWidth()) {
+    Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
         BasicSecureTextField(
             state = state,
             textObfuscationMode = if (visible) {
@@ -128,11 +127,13 @@
                 .border(1.dp, Color.LightGray, RoundedCornerShape(6.dp))
                 .padding(6.dp)
         )
-        IconToggleButton(checked = visible, onCheckedChange = { visible = it }) {
-            if (visible) {
-                Icon(Icons.Default.Warning, "")
-            } else {
-                Icon(Icons.Default.Info, "")
+        if (visible) {
+            TextButton(onClick = { visible = false }) {
+                Text("Hide")
+            }
+        } else {
+            TextButton(onClick = { visible = true }) {
+                Text("Show")
             }
         }
     }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
index d83f3ef..e502d6e 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text2/ReceiveContentDemos.kt
@@ -29,9 +29,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.hasMediaType
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -111,7 +111,7 @@
             ): TransferableContent? {
                 val newImageUris = mutableListOf<Uri>()
                 return transferableContent
-                    .consumeEach { item ->
+                    .consume { item ->
                         // this happens in the ui thread, try not to load images here.
                         val isImageBitmap = item.uri?.isImageBitmap(context) ?: false
                         if (isImageBitmap) {
@@ -131,7 +131,7 @@
     Column(
         modifier = Modifier
             .fillMaxSize()
-            .receiveContent(
+            .contentReceiver(
                 hintMediaTypes = setOf(MediaType.Image),
                 receiveContentListener = receiveContentListener
             )
@@ -221,7 +221,7 @@
                         transferableContent
                     } else {
                         var uri: Uri? = null
-                        transferableContent.consumeEach { item ->
+                        transferableContent.consume { item ->
                             // only consume this item if we can read
                             if (item.uri != null && uri == null) {
                                 uri = item.uri
@@ -246,7 +246,7 @@
                 ReceiveContentShowcase(
                     "Text Consumer",
                     MediaType.Text, {
-                        it.consumeEach { item ->
+                        it.consume { item ->
                             val text = item.coerceToText(context)
                             // only consume if it has text in it.
                             !text.isNullOrBlank() && item.uri == null
@@ -412,7 +412,7 @@
 fun Modifier.dropReceiveContent(
     state: ReceiveContentState
 ) = composed {
-    receiveContent(state.hintMediaTypes, state.listener)
+    contentReceiver(state.hintMediaTypes, state.listener)
         .background(
             color = if (state.hovering) {
                 MaterialTheme.colors.secondary
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
index 8b01a4e..1587db9 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ReceiveContentSamples.kt
@@ -24,9 +24,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.hasMediaType
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.text.BasicTextField
@@ -55,12 +55,12 @@
         }
         BasicTextField(
             state = state,
-            modifier = Modifier.receiveContent(setOf(MediaType.Image)) { transferableContent ->
+            modifier = Modifier.contentReceiver(setOf(MediaType.Image)) { transferableContent ->
                 if (!transferableContent.hasMediaType(MediaType.Image)) {
-                    return@receiveContent transferableContent
+                    return@contentReceiver transferableContent
                 }
                 val newImages = mutableListOf<ImageBitmap>()
-                transferableContent.consumeEach { item ->
+                transferableContent.consume { item ->
                     // only consume this item if we can read an imageBitmap
                     item.readImageBitmap()?.let { newImages += it; true } ?: false
                 }.also {
@@ -95,7 +95,7 @@
                         else -> MaterialTheme.colors.background
                     }
                 )
-                .receiveContent(
+                .contentReceiver(
                     hintMediaTypes = setOf(MediaType.Image),
                     receiveContentListener = object : ReceiveContentListener {
                         override fun onDragStart() {
@@ -123,7 +123,7 @@
                             }
                             val newImages = mutableListOf<ImageBitmap>()
                             return transferableContent
-                                .consumeEach { item ->
+                                .consume { item ->
                                     // only consume this item if we can read an imageBitmap
                                     item
                                         .readImageBitmap()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
index 91d6be5..a0240e1 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/ReceiveContentTest.kt
@@ -62,9 +62,9 @@
         val listenerCalls = mutableListOf<Int>()
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { listenerCalls += 3; it }
-                .receiveContent(setOf(MediaType.Audio)) { listenerCalls += 2; it }
-                .receiveContent(setOf(MediaType.Text)) { listenerCalls += 1; it }
+                .contentReceiver(setOf(MediaType.Video)) { listenerCalls += 3; it }
+                .contentReceiver(setOf(MediaType.Audio)) { listenerCalls += 2; it }
+                .contentReceiver(setOf(MediaType.Text)) { listenerCalls += 1; it }
                 .then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                     calculatedReceiveContent
@@ -90,27 +90,27 @@
         var textReceived: TransferableContent? = null
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) {
+                .contentReceiver(setOf(MediaType.Video)) {
                     videoReceived = it
-                    val t = it.consumeEach {
+                    val t = it.consume {
                         it.uri
                             ?.toString()
                             ?.contains("video") ?: false
                     }
                     t
                 }
-                .receiveContent(setOf(MediaType.Audio)) {
+                .contentReceiver(setOf(MediaType.Audio)) {
                     audioReceived = it
-                    val t = it.consumeEach {
+                    val t = it.consume {
                         it.uri
                             ?.toString()
                             ?.contains("audio") ?: false
                     }
                     t
                 }
-                .receiveContent(setOf(MediaType.Text)) {
+                .contentReceiver(setOf(MediaType.Text)) {
                     textReceived = it
-                    val t = it.consumeEach { it.text != null }
+                    val t = it.consume { it.text != null }
                     t
                 }
                 .then(TestElement {
@@ -171,7 +171,7 @@
                 Box(modifier = Modifier.then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 }))
-                Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { it })
+                Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { it })
             }
         }
 
@@ -192,7 +192,7 @@
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 })
             ) {
-                Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { it })
+                Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { it })
             }
         }
 
@@ -208,7 +208,7 @@
             ReceiveContentListener { null }
         )
         rule.setContent {
-            Box(modifier = Modifier.receiveContent(emptySet()) { it }) {
+            Box(modifier = Modifier.contentReceiver(emptySet()) { it }) {
                 Box(modifier = Modifier.then(TestElement {
                     calculatedReceiveContent = it.getReceiveContentConfiguration()
                 }))
@@ -227,13 +227,13 @@
         var attached by mutableStateOf(true)
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .then(if (attached) {
-                    Modifier.receiveContent(setOf(MediaType.Audio)) { it }
+                    Modifier.contentReceiver(setOf(MediaType.Audio)) { it }
                 } else {
                     Modifier
                 })
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -267,13 +267,13 @@
         var attached by mutableStateOf(false)
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .then(if (attached) {
-                    Modifier.receiveContent(setOf(MediaType.Audio)) { it }
+                    Modifier.contentReceiver(setOf(MediaType.Audio)) { it }
                 } else {
                     Modifier
                 })
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -307,8 +307,8 @@
         var topMediaTypes by mutableStateOf(setOf(MediaType.Video))
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(topMediaTypes) { it }
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(topMediaTypes) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -342,8 +342,8 @@
         var currentMediaTypes by mutableStateOf(setOf(MediaType.Video))
         rule.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Image)) { it }
-                .receiveContent(currentMediaTypes) { it }
+                .contentReceiver(setOf(MediaType.Image)) { it }
+                .contentReceiver(currentMediaTypes) { it }
                 .then(TestElement {
                     getReceiveContentConfiguration = {
                         it.getReceiveContentConfiguration()
@@ -379,11 +379,11 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.Video)) { it }
+                .contentReceiver(setOf(MediaType.Video)) { it }
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Audio)) { it }
+                .contentReceiver(setOf(MediaType.Audio)) { it }
                 .size(50.dp)
-                .receiveContent(setOf(MediaType.Text)) { it }
+                .contentReceiver(setOf(MediaType.Text)) { it }
             )
         }
 
@@ -411,7 +411,7 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Image)) {
+                .contentReceiver(setOf(MediaType.Image)) {
                     transferableContent = it
                     null // consume all
                 })
@@ -443,7 +443,7 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(100.dp)
-                .receiveContent(setOf(MediaType.Audio)) {
+                .contentReceiver(setOf(MediaType.Audio)) {
                     transferableContent = it
                     null // consume all
                 })
@@ -476,14 +476,14 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.All)) {
+                .contentReceiver(setOf(MediaType.All)) {
                     parentTransferableContent = it
                     null
                 }) {
                 Box(modifier = Modifier
                     .align(Alignment.Center)
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.Image)) {
+                    .contentReceiver(setOf(MediaType.Image)) {
                         childTransferableContent = it
                         it // don't consume anything
                     })
@@ -524,21 +524,21 @@
             view = LocalView.current
             Box(modifier = Modifier
                 .size(200.dp)
-                .receiveContent(setOf(MediaType.All)) {
+                .contentReceiver(setOf(MediaType.All)) {
                     grandParentTransferableContent = it
                     null
                 }) {
                 Box(modifier = Modifier
                     .align(Alignment.Center)
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.Image)) {
+                    .contentReceiver(setOf(MediaType.Image)) {
                         parentTransferableContent = it
                         it // don't consume anything
                     }) {
                     Box(modifier = Modifier
                         .align(Alignment.Center)
                         .size(50.dp)
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             childTransferableContent = it
                             it // don't consume anything
                         })
@@ -571,7 +571,7 @@
             Box(
                 modifier = Modifier
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragEnter() {
                             calls += "enter"
                         }
@@ -618,7 +618,7 @@
             Box(
                 modifier = Modifier
                     .size(200.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragEnter() {
                             calls += "enter-1"
                         }
@@ -636,7 +636,7 @@
                     modifier = Modifier
                         .align(Alignment.Center)
                         .size(100.dp)
-                        .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                        .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                             override fun onDragEnter() {
                                 calls += "enter-2"
                             }
@@ -654,7 +654,7 @@
                         modifier = Modifier
                             .align(Alignment.Center)
                             .size(50.dp)
-                            .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                            .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                                 override fun onDragEnter() {
                                     calls += "enter-3"
                                 }
@@ -710,7 +710,7 @@
             Box(
                 modifier = Modifier
                     .size(100.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragStart() {
                             calls += "start"
                         }
@@ -757,7 +757,7 @@
             Box(
                 modifier = Modifier
                     .size(200.dp)
-                    .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                    .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                         override fun onDragStart() {
                             calls += "start-1"
                         }
@@ -775,7 +775,7 @@
                     modifier = Modifier
                         .align(Alignment.Center)
                         .size(100.dp)
-                        .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                        .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start-2"
                             }
@@ -793,7 +793,7 @@
                         modifier = Modifier
                             .align(Alignment.Center)
                             .size(50.dp)
-                            .receiveContent(setOf(MediaType.All), object : ReceiveContentListener {
+                            .contentReceiver(setOf(MediaType.All), object : ReceiveContentListener {
                                 override fun onDragStart() {
                                     calls += "start-3"
                                 }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
index edcb824..d97f989 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/content/TransferableContentTest.kt
@@ -90,14 +90,14 @@
     @Test
     fun consumeEach_returnsNull_ifEverythingIsConsumed() {
         val transferableContent = TransferableContent(createClipData())
-        val remaining = transferableContent.consumeEach { true }
+        val remaining = transferableContent.consume { true }
         assertThat(remaining).isNull()
     }
 
     @Test
     fun consumeEach_returnsSameObject_ifNothingIsConsumed() {
         val transferableContent = TransferableContent(createClipData())
-        val remaining = transferableContent.consumeEach { false }
+        val remaining = transferableContent.consume { false }
         assertThat(remaining).isSameInstanceAs(transferableContent)
     }
 
@@ -109,7 +109,7 @@
             addUri(mimeType = "image/gif")
         })
         // only text would remain
-        val remaining = transferableContent.consumeEach { it.uri != null }
+        val remaining = transferableContent.consume { it.uri != null }
         assertThat(remaining?.clipEntry?.clipData?.itemCount).isEqualTo(1)
         assertThat(remaining?.clipEntry?.clipData?.getItemAt(0)?.uri).isNull()
         assertThat(remaining?.hasMediaType(MediaType("video/mp4"))).isTrue()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt
new file mode 100644
index 0000000..2f008d6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingTest.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.setFocusableContent
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.internal.InputMethodManager
+import androidx.compose.foundation.text.input.internal.inputMethodManagerFactory
+import androidx.compose.foundation.text.matchers.isZero
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CoreTextFieldHandwritingTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val Tag = "CoreTextField"
+
+    private val fakeImm = object : InputMethodManager {
+        private var stylusHandwritingStartCount = 0
+
+        fun expectStylusHandwriting(started: Boolean) {
+            if (started) {
+                assertThat(stylusHandwritingStartCount).isEqualTo(1)
+                stylusHandwritingStartCount = 0
+            } else {
+                assertThat(stylusHandwritingStartCount).isZero()
+            }
+        }
+
+        override fun isActive(): Boolean = true
+
+        override fun restartInput() {}
+
+        override fun showSoftInput() {}
+
+        override fun hideSoftInput() {}
+
+        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+        override fun updateSelection(
+            selectionStart: Int,
+            selectionEnd: Int,
+            compositionStart: Int,
+            compositionEnd: Int
+        ) {}
+
+        override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+
+        override fun startStylusHandwriting() {
+            ++stylusHandwritingStartCount
+        }
+    }
+
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylusHandwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
+    @Test
+    fun coreTextField_startHandwriting_unfocused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_startStylusHandwriting_unfocused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_startStylusHandwriting_focused() {
+        testStylusHandwriting(stylusHandwritingStarted = true) {
+            requestFocus()
+            performStylusHandwriting()
+        }
+    }
+
+    @Test
+    fun coreTextField_click_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusClick()
+        }
+    }
+
+    @Test
+    fun coreTextField_longClick_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusLongClick()
+        }
+    }
+
+    @Test
+    fun coreTextField_longPressAndDrag_notStartStylusHandwriting() {
+        testStylusHandwriting(stylusHandwritingStarted = false) {
+            performStylusLongPressAndDrag()
+        }
+    }
+
+    @Test
+    fun coreTextField_toggleEnabled_startStylusHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        var enabled by mutableStateOf(true)
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag),
+                enabled = enabled
+            )
+        }
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to false, shouldn't start handwriting
+        enabled = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        enabled = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    @Test
+    fun coreTextField_toggleReadOnly_startStylusHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        var readOnly by mutableStateOf(false)
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag),
+                readOnly = readOnly
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to true, shouldn't start handwriting
+        readOnly = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        readOnly = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    private fun testStylusHandwriting(
+        stylusHandwritingStarted: Boolean,
+        interaction: SemanticsNodeInteraction.() -> Unit
+    ) {
+        inputMethodManagerFactory = { fakeImm }
+
+        setContent {
+            val value = remember { TextFieldValue() }
+            CoreTextField(
+                value = value,
+                onValueChange = { },
+                modifier = Modifier
+                    .fillMaxSize()
+                    .testTag(Tag)
+            )
+        }
+
+        interaction.invoke(rule.onNodeWithTag(Tag))
+        rule.waitForIdle()
+        fakeImm.expectStylusHandwriting(stylusHandwritingStarted)
+    }
+
+    private fun setContent(
+        extraItemForInitialFocus: Boolean = true,
+        content: @Composable () -> Unit
+    ) {
+        rule.setFocusableContent(extraItemForInitialFocus) {
+            inputMethodInterceptor.Content {
+                content()
+            }
+        }
+    }
+
+    private fun performHandwritingAndExpect(stylusHandwritingStarted: Boolean) {
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+        rule.waitForIdle()
+        fakeImm.expectStylusHandwriting(stylusHandwritingStarted)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
index cc4fccf..e3f3e01 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldInputServiceIntegrationTest.kt
@@ -471,6 +471,7 @@
                 cursorAnchorInfos += cursorAnchorInfo
             }
 
+            override fun startStylusHandwriting() {}
             override fun isActive(): Boolean = true
             override fun restartInput() {}
             override fun showSoftInput() {}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
new file mode 100644
index 0000000..966c702
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.view.KeyEvent
+import android.view.MotionEvent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.ViewRootForTest
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.TouchInjectionScope
+import androidx.compose.ui.test.invokeGlobalAssertions
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.toOffset
+import androidx.core.view.InputDeviceCompat
+import androidx.test.platform.app.InstrumentationRegistry
+import kotlin.math.roundToInt
+
+// We don't have StylusInjectionScope at the moment. This is a simplified implementation for
+// the basic use cases in this test. It only supports single stylus pointer, and the pointerId
+// is totally ignored.
+internal class HandwritingTestStylusInjectScope(
+    semanticsNode: SemanticsNode
+) : TouchInjectionScope, Density by semanticsNode.layoutInfo.density {
+    private val root = semanticsNode.root as ViewRootForTest
+    private val downTime: Long = System.currentTimeMillis()
+
+    private var lastPosition: Offset = Offset.Unspecified
+    private var currentTime: Long = System.currentTimeMillis()
+    private val boundsInRoot = semanticsNode.boundsInRoot
+
+    override val visibleSize: IntSize =
+        IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
+
+    override val viewConfiguration: ViewConfiguration =
+        semanticsNode.layoutInfo.viewConfiguration
+
+    private fun localToRoot(position: Offset): Offset {
+        return position + boundsInRoot.topLeft
+    }
+
+    override fun advanceEventTime(durationMillis: Long) {
+        require(durationMillis >= 0) {
+            "duration of a delay can only be positive, not $durationMillis"
+        }
+        currentTime += durationMillis
+    }
+
+    override fun currentPosition(pointerId: Int): Offset? {
+        return lastPosition
+    }
+
+    override fun down(pointerId: Int, position: Offset) {
+        val rootPosition = localToRoot(position)
+        lastPosition = rootPosition
+        sendTouchEvent(KeyEvent.ACTION_DOWN)
+    }
+
+    override fun updatePointerTo(pointerId: Int, position: Offset) {
+        lastPosition = localToRoot(position)
+    }
+
+    override fun move(delayMillis: Long) {
+        advanceEventTime(delayMillis)
+        sendTouchEvent(MotionEvent.ACTION_MOVE)
+    }
+
+    @ExperimentalTestApi
+    override fun moveWithHistoryMultiPointer(
+        relativeHistoricalTimes: List<Long>,
+        historicalCoordinates: List<List<Offset>>,
+        delayMillis: Long
+    ) {
+        // Not needed for this test because Android only support one stylus pointer.
+    }
+
+    override fun up(pointerId: Int) {
+        sendTouchEvent(MotionEvent.ACTION_UP)
+    }
+
+    override fun cancel(delayMillis: Long) {
+        sendTouchEvent(MotionEvent.ACTION_CANCEL)
+    }
+
+    private fun sendTouchEvent(action: Int) {
+        val positionInScreen = run {
+            val array = intArrayOf(0, 0)
+            root.view.getLocationOnScreen(array)
+            Offset(array[0].toFloat(), array[1].toFloat())
+        }
+        val motionEvent = MotionEvent.obtain(
+            /* downTime = */ downTime,
+            /* eventTime = */ currentTime,
+            /* action = */ action,
+            /* pointerCount = */ 1,
+            /* pointerProperties = */ arrayOf(
+                MotionEvent.PointerProperties().apply {
+                    id = 0
+                    toolType = MotionEvent.TOOL_TYPE_STYLUS
+                }
+            ),
+            /* pointerCoords = */ arrayOf(
+                MotionEvent.PointerCoords().apply {
+                    val startOffset = lastPosition
+
+                    // Allows for non-valid numbers/Offsets to be passed along to Compose to
+                    // test if it handles them properly (versus breaking here and we not knowing
+                    // if Compose properly handles these values).
+                    x = if (startOffset.isValid()) {
+                        positionInScreen.x + startOffset.x
+                    } else {
+                        Float.NaN
+                    }
+
+                    y = if (startOffset.isValid()) {
+                        positionInScreen.y + startOffset.y
+                    } else {
+                        Float.NaN
+                    }
+                }
+            ),
+            /* metaState = */ 0,
+            /* buttonState = */ 0,
+            /* xPrecision = */ 1f,
+            /* yPrecision = */ 1f,
+            /* deviceId = */ 0,
+            /* edgeFlags = */ 0,
+            /* source = */ InputDeviceCompat.SOURCE_TOUCHSCREEN,
+            /* flags = */ 0
+        )
+
+        InstrumentationRegistry.getInstrumentation().runOnMainSync {
+            root.view.dispatchTouchEvent(motionEvent)
+        }
+    }
+}
+
+/** Start stylus handwriting on the target element. */
+internal fun SemanticsNodeInteraction.performStylusHandwriting() {
+    performStylusInput {
+        val startPosition = visibleSize.center.toOffset()
+        down(startPosition)
+        moveTo(startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f))
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusClick() {
+    performStylusInput {
+        down(visibleSize.center.toOffset())
+        move()
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusLongClick() {
+    performStylusInput {
+        down(visibleSize.center.toOffset())
+        move(viewConfiguration.longPressTimeoutMillis + 1)
+        up()
+    }
+}
+
+internal fun SemanticsNodeInteraction.performStylusLongPressAndDrag() {
+    performStylusInput {
+        val startPosition = visibleSize.center.toOffset()
+        down(visibleSize.center.toOffset())
+        val position = startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f)
+        moveTo(
+            position = position,
+            delayMillis = viewConfiguration.longPressTimeoutMillis + 1
+        )
+        up()
+    }
+}
+
+private fun SemanticsNodeInteraction.performStylusInput(
+    block: TouchInjectionScope.() -> Unit
+): SemanticsNodeInteraction {
+    @OptIn(ExperimentalTestApi::class)
+    invokeGlobalAssertions()
+    val node = fetchSemanticsNode("Failed to inject stylus input.")
+    val stylusInjectionScope = HandwritingTestStylusInjectScope(node)
+    block.invoke(stylusInjectionScope)
+    return this
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
index 0053b63..0e6f520 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/BasicTextFieldHandwritingTest.kt
@@ -16,41 +16,33 @@
 
 package androidx.compose.foundation.text.input
 
-import android.view.KeyEvent.ACTION_DOWN
-import android.view.MotionEvent
-import android.view.MotionEvent.ACTION_CANCEL
-import android.view.MotionEvent.ACTION_MOVE
-import android.view.MotionEvent.ACTION_UP
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.performStylusClick
+import androidx.compose.foundation.text.performStylusHandwriting
+import androidx.compose.foundation.text.performStylusLongClick
+import androidx.compose.foundation.text.performStylusLongPressAndDrag
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.platform.ViewConfiguration
-import androidx.compose.ui.platform.ViewRootForTest
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.SemanticsNode
-import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteraction
-import androidx.compose.ui.test.TouchInjectionScope
-import androidx.compose.ui.test.invokeGlobalAssertions
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.requestFocus
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.center
-import androidx.compose.ui.unit.toOffset
-import androidx.core.view.InputDeviceCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
-import kotlin.math.roundToInt
+import org.junit.Assume.assumeTrue
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 internal class BasicTextFieldHandwritingTest {
@@ -66,6 +58,12 @@
 
     private val imm = FakeInputMethodManager()
 
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylus handwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
     @Test
     fun textField_startStylusHandwriting_unfocused() {
         testStylusHandwriting(stylusHandwritingStarted = true) {
@@ -84,38 +82,21 @@
     @Test
     fun textField_click_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                down(visibleSize.center.toOffset())
-                move()
-                up()
-            }
+            performStylusClick()
         }
     }
 
     @Test
     fun textField_longClick_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                down(visibleSize.center.toOffset())
-                move(viewConfiguration.longPressTimeoutMillis + 1)
-                up()
-            }
+            performStylusLongClick()
         }
     }
 
     @Test
     fun textField_longPressAndDrag_notStartStylusHandwriting() {
         testStylusHandwriting(stylusHandwritingStarted = false) {
-            performStylusInput {
-                val startPosition = visibleSize.center.toOffset()
-                down(visibleSize.center.toOffset())
-                val position = startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f)
-                moveTo(
-                    position = position,
-                    delayMillis = viewConfiguration.longPressTimeoutMillis + 1
-                )
-                up()
-            }
+            performStylusLongPressAndDrag()
         }
     }
 
@@ -157,6 +138,58 @@
         }
     }
 
+    @Test
+    fun textField_toggleEnabled_startStylusHandwriting() {
+        immRule.setFactory { imm }
+        var enabled by mutableStateOf(true)
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                enabled = enabled
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to false, shouldn't start handwriting
+        enabled = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        enabled = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
+    @Test
+    fun textField_toggleReadOnly_startStylusHandwriting() {
+        immRule.setFactory { imm }
+        var readOnly by mutableStateOf(false)
+        inputMethodInterceptor.setTextFieldTestContent {
+            val state = remember { TextFieldState() }
+            BasicTextField(
+                state = state,
+                modifier = Modifier.fillMaxSize().testTag(Tag),
+                readOnly = readOnly
+            )
+        }
+
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+
+        // Toggle enabled to true, shouldn't start handwriting
+        readOnly = true
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = false)
+
+        // Toggle to true again, should be able to start handwriting
+        readOnly = false
+        rule.waitForIdle()
+        performHandwritingAndExpect(stylusHandwritingStarted = true)
+    }
+
     private fun testStylusHandwriting(
         stylusHandwritingStarted: Boolean,
         interaction: SemanticsNodeInteraction.() -> Unit
@@ -180,142 +213,14 @@
         }
     }
 
-    /** Start stylus handwriting on the target element. */
-    private fun SemanticsNodeInteraction.performStylusHandwriting() {
-        performStylusInput {
-            val startPosition = visibleSize.center.toOffset()
-            down(startPosition)
-            moveTo(startPosition + Offset(viewConfiguration.handwritingSlop * 2, 0f))
-            up()
-        }
-    }
-
-    private fun SemanticsNodeInteraction.performStylusInput(
-        block: TouchInjectionScope.() -> Unit
-    ): SemanticsNodeInteraction {
-        @OptIn(ExperimentalTestApi::class)
-        invokeGlobalAssertions()
-        val node = fetchSemanticsNode("Failed to inject stylus input.")
-        val stylusInjectionScope = StylusInjectionScope(node)
-        block.invoke(stylusInjectionScope)
-        return this
-    }
-
-    // We don't have StylusInjectionScope at the moment. This is a simplified implementation for
-    // the basic use cases in this test. It only supports single stylus pointer, and the pointerId
-    // is totally ignored.
-    private inner class StylusInjectionScope(
-        semanticsNode: SemanticsNode
-    ) : TouchInjectionScope, Density by semanticsNode.layoutInfo.density {
-        private val root = semanticsNode.root as ViewRootForTest
-        private val downTime: Long = System.currentTimeMillis()
-
-        private var lastPosition: Offset = Offset.Unspecified
-        private var currentTime: Long = System.currentTimeMillis()
-        private val boundsInRoot = semanticsNode.boundsInRoot
-
-        override val visibleSize: IntSize =
-            IntSize(boundsInRoot.width.roundToInt(), boundsInRoot.height.roundToInt())
-
-        override val viewConfiguration: ViewConfiguration =
-            semanticsNode.layoutInfo.viewConfiguration
-
-        private fun localToRoot(position: Offset): Offset {
-            return position + boundsInRoot.topLeft
-        }
-
-        override fun advanceEventTime(durationMillis: Long) {
-            require(durationMillis >= 0) {
-                "duration of a delay can only be positive, not $durationMillis"
-            }
-            currentTime += durationMillis
-        }
-
-        override fun currentPosition(pointerId: Int): Offset? {
-            return lastPosition
-        }
-
-        override fun down(pointerId: Int, position: Offset) {
-            val rootPosition = localToRoot(position)
-            lastPosition = rootPosition
-            sendTouchEvent(ACTION_DOWN)
-        }
-
-        override fun updatePointerTo(pointerId: Int, position: Offset) {
-            lastPosition = localToRoot(position)
-        }
-
-        override fun move(delayMillis: Long) {
-            advanceEventTime(delayMillis)
-            sendTouchEvent(ACTION_MOVE)
-        }
-
-        @ExperimentalTestApi
-        override fun moveWithHistoryMultiPointer(
-            relativeHistoricalTimes: List<Long>,
-            historicalCoordinates: List<List<Offset>>,
-            delayMillis: Long
-        ) {
-            // Not needed for this test because Android only support one stylus pointer.
-        }
-
-        override fun up(pointerId: Int) {
-            sendTouchEvent(ACTION_UP)
-        }
-
-        override fun cancel(delayMillis: Long) {
-            sendTouchEvent(ACTION_CANCEL)
-        }
-
-        private fun sendTouchEvent(action: Int) {
-            val positionInScreen = run {
-                val array = intArrayOf(0, 0)
-                root.view.getLocationOnScreen(array)
-                Offset(array[0].toFloat(), array[1].toFloat())
-            }
-            val motionEvent = MotionEvent.obtain(
-                /* downTime = */ downTime,
-                /* eventTime = */ currentTime,
-                /* action = */ action,
-                /* pointerCount = */ 1,
-                /* pointerProperties = */ arrayOf(
-                    MotionEvent.PointerProperties().apply {
-                        id = 0
-                        toolType = MotionEvent.TOOL_TYPE_STYLUS
-                    }
-                ),
-                /* pointerCoords = */ arrayOf(
-                    MotionEvent.PointerCoords().apply {
-                        val startOffset = lastPosition
-
-                        // Allows for non-valid numbers/Offsets to be passed along to Compose to
-                        // test if it handles them properly (versus breaking here and we not knowing
-                        // if Compose properly handles these values).
-                        x = if (startOffset.isValid()) {
-                            positionInScreen.x + startOffset.x
-                        } else {
-                            Float.NaN
-                        }
-
-                        y = if (startOffset.isValid()) {
-                            positionInScreen.y + startOffset.y
-                        } else {
-                            Float.NaN
-                        }
-                    }
-                ),
-                /* metaState = */ 0,
-                /* buttonState = */ 0,
-                /* xPrecision = */ 1f,
-                /* yPrecision = */ 1f,
-                /* deviceId = */ 0,
-                /* edgeFlags = */ 0,
-                /* source = */ InputDeviceCompat.SOURCE_TOUCHSCREEN,
-                /* flags = */ 0
-            )
-
-            rule.runOnUiThread {
-                root.view.dispatchTouchEvent(motionEvent)
+    private fun performHandwritingAndExpect(stylusHandwritingStarted: Boolean) {
+        rule.onNodeWithTag(Tag).performStylusHandwriting()
+        rule.waitForIdle()
+        rule.runOnIdle {
+            if (stylusHandwritingStarted) {
+                imm.expectCall("startStylusHandwriting")
+            } else {
+                imm.expectNoMoreCalls()
             }
         }
     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
index 3c3f61d..90f6621 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldDragAndDropTest.kt
@@ -24,9 +24,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.content.testDragAndDrop
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.collectIsHoveredAsState
@@ -87,7 +87,7 @@
     @Test
     fun nonTextContent_isAcceptedIfReceiveContentDefined() {
         rule.setContentAndTestDragAndDrop(
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 null
             }
         ) {
@@ -120,7 +120,7 @@
     @Test
     fun draggingNonText_updatesSelection_withReceiveContent() {
         rule.setContentAndTestDragAndDrop(
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 null
             }
         ) {
@@ -225,7 +225,7 @@
                 Box(
                     modifier = Modifier
                         .size(200.dp)
-                        .receiveContent(emptySet(), object : ReceiveContentListener {
+                        .contentReceiver(emptySet(), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start"
                             }
@@ -298,7 +298,7 @@
                 Box(
                     modifier = Modifier
                         .size(200.dp)
-                        .receiveContent(emptySet(), object : ReceiveContentListener {
+                        .contentReceiver(emptySet(), object : ReceiveContentListener {
                             override fun onDragStart() {
                                 calls += "start"
                             }
@@ -364,9 +364,9 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
-                receivedContent.consumeEach {
+                receivedContent.consume {
                     // do not consume text
                     it.uri != null
                 }
@@ -390,7 +390,7 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
                 // consume everything
                 null
@@ -414,7 +414,7 @@
         lateinit var receivedContent: TransferableContent
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(setOf(MediaType("video/*"))) {
+            modifier = Modifier.contentReceiver(setOf(MediaType("video/*"))) {
                 receivedContent = it
                 val uri = receivedContent.clipEntry.firstUriOrNull()
                 // replace the content
@@ -436,7 +436,7 @@
     fun droppedItem_requestsPermission_ifReceiveContent() {
         rule.setContentAndTestDragAndDrop(
             "Hello World!",
-            modifier = Modifier.receiveContent(emptySet()) { null }
+            modifier = Modifier.contentReceiver(emptySet()) { null }
         ) {
             drag(Offset(fontSize.toPx() * 5, 10f), defaultUri)
             drop()
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
index 695108a..7539f76 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/TextFieldReceiveContentTest.kt
@@ -26,9 +26,9 @@
 import androidx.compose.foundation.content.MediaType
 import androidx.compose.foundation.content.TransferableContent
 import androidx.compose.foundation.content.assertClipData
-import androidx.compose.foundation.content.consumeEach
+import androidx.compose.foundation.content.consume
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.text.BasicTextField
@@ -124,7 +124,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.Image)) { null }
+                    .contentReceiver(setOf(MediaType.Image)) { null }
             )
         }
         rule.onNodeWithTag(tag).requestFocus()
@@ -141,7 +141,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(
+                    .contentReceiver(
                         setOf(
                             MediaType.Image,
                             MediaType.PlainText,
@@ -167,12 +167,12 @@
     @Test
     fun multiReceiveContent_mergesMediaTypes() {
         inputMethodInterceptor.setContent {
-            Box(modifier = Modifier.receiveContent(setOf(MediaType.Text)) { null }) {
+            Box(modifier = Modifier.contentReceiver(setOf(MediaType.Text)) { null }) {
                 BasicTextField(
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -191,14 +191,14 @@
     @Test
     fun multiReceiveContent_mergesMediaTypes_uniquely() {
         inputMethodInterceptor.setContent {
-            Box(modifier = Modifier.receiveContent(
+            Box(modifier = Modifier.contentReceiver(
                 setOf(MediaType.Text, MediaType.Image)
             ) { null }) {
                 BasicTextField(
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -218,7 +218,7 @@
     fun multiReceiveContent_mergesMediaTypes_includingAnotherTraversableNode() {
         inputMethodInterceptor.setContent {
             Box(modifier = Modifier
-                .receiveContent(setOf(MediaType.Text)) { null }
+                .contentReceiver(setOf(MediaType.Text)) { null }
                 .dragAndDropTarget({ true }, object : DragAndDropTarget {
                     override fun onDrop(event: DragAndDropEvent): Boolean {
                         return false
@@ -229,7 +229,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) { null }
+                        .contentReceiver(setOf(MediaType.Image)) { null }
                 )
             }
         }
@@ -253,7 +253,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         transferableContent = it
                         null
                     }
@@ -299,7 +299,7 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         transferableContent = it
                         null
                     }
@@ -338,11 +338,11 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         parentTransferableContent = it
                         null
                     }
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         childTransferableContent = it
                         it
                     }
@@ -383,11 +383,11 @@
                 state = rememberTextFieldState(),
                 modifier = Modifier
                     .testTag(tag)
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         parentTransferableContent = it
                         null
                     }
-                    .receiveContent(setOf(MediaType.All)) {
+                    .contentReceiver(setOf(MediaType.All)) {
                         childTransferableContent = it
                         null
                     }
@@ -422,7 +422,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) {
+                        .contentReceiver(setOf(MediaType.Image)) {
                             transferableContent = it
                             null
                         }
@@ -455,8 +455,8 @@
                     state = state,
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image, MediaType.Text)) {
-                            it.consumeEach { item ->
+                        .contentReceiver(setOf(MediaType.Image, MediaType.Text)) {
+                            it.consume { item ->
                                 // only consume if there's no text
                                 item.text == null
                             }
@@ -494,21 +494,21 @@
                     state = state,
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent1 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("a")
                             }
                         }
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent2 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("b")
                             }
                         }
-                        .receiveContent(setOf(MediaType.Text)) {
+                        .contentReceiver(setOf(MediaType.Text)) {
                             transferableContent3 = it
-                            it.consumeEach {
+                            it.consume {
                                 it.text.contains("c")
                             }
                         }
@@ -548,7 +548,7 @@
                     state = rememberTextFieldState(),
                     modifier = Modifier
                         .testTag(tag)
-                        .receiveContent(setOf(MediaType.Image)) {
+                        .contentReceiver(setOf(MediaType.Image)) {
                             transferableContent = it
                             null
                         }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
index d227538..0106d69 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldTextToolbarTest.kt
@@ -18,8 +18,8 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.content.MediaType
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.foundation.content.createClipData
-import androidx.compose.foundation.content.receiveContent
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -582,7 +582,7 @@
             toolbar = textToolbar,
             singleLine = true,
             clipboardManager = clipboardManager,
-            modifier = Modifier.receiveContent(setOf(MediaType.Image)) { null }
+            modifier = Modifier.contentReceiver(setOf(MediaType.Image)) { null }
         )
 
         rule.onNodeWithTag(TAG).performTouchInput { click() }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
index 7c0f682..8ac71ce 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/content/TransferableContent.android.kt
@@ -28,8 +28,8 @@
 /**
  * Android specific parts of [TransferableContent].
  *
- * @param linkUri Only supplied by InputConnection#commitContent.
- * @param extras Extras bundle that's passed by InputConnection#commitContent.
+ * @property linkUri Only supplied by InputConnection#commitContent.
+ * @property extras Extras bundle that's passed by InputConnection#commitContent.
  */
 @ExperimentalFoundationApi
 actual class PlatformTransferableContent internal constructor(
@@ -61,16 +61,18 @@
 
 /**
  * Helper function to consume parts of [TransferableContent] in Android by splitting it to
- * [ClipData.Item] parts. Use this function in [receiveContent] modifier's `onReceive` callback to
+ * [ClipData.Item] parts. Use this function in [contentReceiver] modifier's `onReceive` callback to
  * easily separate remaining parts from incoming [TransferableContent].
  *
+ * @sample androidx.compose.foundation.samples.ReceiveContentBasicSample
+ *
  * @param predicate Decides whether to consume or leave the given item out. Return true to indicate
  * that this particular item was processed here, it shouldn't be passed further down the content
  * receiver chain. Return false to keep it in the returned [TransferableContent].
  * @return Remaining parts of this [TransferableContent].
  */
 @ExperimentalFoundationApi
-fun TransferableContent.consumeEach(predicate: (ClipData.Item) -> Boolean): TransferableContent? {
+fun TransferableContent.consume(predicate: (ClipData.Item) -> Boolean): TransferableContent? {
     val clipData = clipEntry.clipData
     return if (clipData.itemCount == 1) {
         // return this if the single item inside ClipData is not consumed, or null if it's consumed
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt
new file mode 100644
index 0000000..3ba3d83
--- /dev/null
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.android.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.handwriting
+
+import android.os.Build
+
+internal actual val isStylusHandwritingSupported: Boolean =
+    Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
index e5298d9..9aaa89a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/InputMethodManager.android.kt
@@ -17,10 +17,13 @@
 package androidx.compose.foundation.text.input.internal
 
 import android.content.Context
+import android.os.Build
 import android.util.Log
 import android.view.View
 import android.view.inputmethod.CursorAnchorInfo
 import android.view.inputmethod.ExtractedText
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
 import androidx.core.view.SoftwareKeyboardControllerCompat
 
 internal interface InputMethodManager {
@@ -45,6 +48,8 @@
     )
 
     fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo)
+
+    fun startStylusHandwriting()
 }
 
 /**
@@ -98,4 +103,18 @@
     override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {
         imm.updateCursorAnchorInfo(view, cursorAnchorInfo)
     }
+
+    override fun startStylusHandwriting() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+            Api34StartStylusHandwriting.startStylusHandwriting(imm, view)
+        }
+    }
+}
+
+@RequiresApi(34)
+internal object Api34StartStylusHandwriting {
+    @DoNotInline
+    fun startStylusHandwriting(imm: android.view.inputmethod.InputMethodManager, view: View) {
+        imm.startStylusHandwriting(view)
+    }
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
index 8cd284c..067d170 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.android.kt
@@ -23,6 +23,7 @@
 import android.view.inputmethod.BaseInputConnection
 import android.view.inputmethod.EditorInfo
 import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.platform.PlatformTextInputMethodRequest
@@ -36,7 +37,14 @@
 import androidx.emoji2.text.EmojiCompat
 import java.lang.ref.WeakReference
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
 
 private const val DEBUG_CLASS = "AndroidLegacyPlatformTextInputServiceAdapter"
 
@@ -55,6 +63,21 @@
 
     private var job: Job? = null
     private var currentRequest: LegacyTextInputMethodRequest? = null
+    private var backingStylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+    private var stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+        get() {
+            val finalStylusHandwritingTrigger = backingStylusHandwritingTrigger
+            if (finalStylusHandwritingTrigger != null) {
+                return finalStylusHandwritingTrigger
+            }
+            if (!isStylusHandwritingSupported) {
+                return null
+            }
+            return MutableSharedFlow<Unit>(
+                replay = 1,
+                onBufferOverflow = BufferOverflow.DROP_LATEST
+            ).also { backingStylusHandwritingTrigger = it }
+        }
 
     override fun startInput(
         value: TextFieldValue,
@@ -89,27 +112,40 @@
         // No need to cancel any previous job, the text input system ensures the previous session
         // will be cancelled.
         job = node.launchTextInputSession {
-            val request = LegacyTextInputMethodRequest(
-                view = view,
-                localToScreen = ::localToScreen,
-                inputMethodManager = inputMethodManagerFactory(view)
-            )
-            initializeRequest?.invoke(request)
-            currentRequest = request
-            try {
-                startInputMethod(request)
-            } finally {
-                currentRequest = null
+            coroutineScope {
+                val inputMethodManager = inputMethodManagerFactory(view)
+                val request = LegacyTextInputMethodRequest(
+                    view = view,
+                    localToScreen = ::localToScreen,
+                    inputMethodManager = inputMethodManager
+                )
+
+                if (isStylusHandwritingSupported) {
+                    launch(start = CoroutineStart.UNDISPATCHED) {
+                        stylusHandwritingTrigger?.collect {
+                            inputMethodManager.startStylusHandwriting()
+                        }
+                    }
+                }
+                initializeRequest?.invoke(request)
+                currentRequest = request
+                try {
+                    startInputMethod(request)
+                } finally {
+                    currentRequest = null
+                }
             }
         }
     }
 
+    @OptIn(ExperimentalCoroutinesApi::class)
     override fun stopInput() {
         if (DEBUG) {
             Log.d(TAG, "$DEBUG_CLASS.stopInput")
         }
         job?.cancel()
         job = null
+        stylusHandwritingTrigger?.resetReplayCache()
     }
 
     override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
@@ -136,6 +172,14 @@
             decorationBoxBounds
         )
     }
+
+    /**
+     * Signal the InputMethodManager to startStylusHandwriting. This method can be called
+     * after the editor calls startInput or just before the editor calls startInput.
+     */
+    override fun startStylusHandwriting() {
+        stylusHandwritingTrigger?.tryEmit(Unit)
+    }
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
index 90d2762..55cf28a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContent.kt
@@ -42,47 +42,15 @@
  * supports. It's possible that this modifier receives other type of content that's not specified in
  * this set. Please make sure to check again whether the received [TransferableContent] carries a
  * supported [MediaType]. An empty [MediaType] set implies [MediaType.All].
- * @param onReceive Callback that's triggered when a content is successfully committed. Return
- * an optional [TransferableContent] that contains the unprocessed or unaccepted parts of the
- * received [TransferableContent]. The remaining [TransferableContent] first will be sent to to the
- * closest ancestor [receiveContent] modifier. This chain will continue until there's no ancestor
- * modifier left, or [TransferableContent] is fully consumed. After, the source subsystem that
- * created the original [TransferableContent] and initiated the chain will receive any remaining
- * items to execute its default behavior. For example a text editor that receives content should
- * insert any remaining text to the drop position.
- *
- * @sample androidx.compose.foundation.samples.ReceiveContentBasicSample
- */
-@ExperimentalFoundationApi
-fun Modifier.receiveContent(
-    hintMediaTypes: Set<MediaType>,
-    onReceive: (TransferableContent) -> TransferableContent?
-): Modifier = then(
-    ReceiveContentElement(
-        hintMediaTypes = hintMediaTypes,
-        receiveContentListener = ReceiveContentListener(onReceive)
-    )
-)
-
-/**
- * Configures the current node and any children nodes as a Content Receiver.
- *
- * Content in this context refers to a [TransferableContent] that could be received from another
- * app through Drag-and-Drop, Copy/Paste, or from the Software Keyboard.
- *
- * @param hintMediaTypes A set of media types that are expected by this receiver. This set
- * gets passed to the Software Keyboard to send information about what type of content the editor
- * supports. It's possible that this modifier receives other type of content that's not specified in
- * this set. Please make sure to check again whether the received [TransferableContent] carries a
- * supported [MediaType]. An empty [MediaType] set implies [MediaType.All].
- * @param receiveContentListener A set of callbacks that includes certain Drag-and-Drop state
- * changes. Please checkout [ReceiveContentListener] docs for an explanation of each callback.
+ * @param receiveContentListener Listener to respond to the receive event. This interface also
+ * includes a set of callbacks for certain Drag-and-Drop state changes. Please checkout
+ * [ReceiveContentListener] docs for an explanation of each callback.
  *
  * @sample androidx.compose.foundation.samples.ReceiveContentFullSample
  */
 @Suppress("ExecutorRegistration")
 @ExperimentalFoundationApi
-fun Modifier.receiveContent(
+fun Modifier.contentReceiver(
     hintMediaTypes: Set<MediaType>,
     receiveContentListener: ReceiveContentListener
 ): Modifier = then(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
index 40934de..d5fc700 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/ReceiveContentListener.kt
@@ -20,14 +20,14 @@
 import androidx.compose.foundation.draganddrop.dragAndDropTarget
 
 /**
- * A set of callbacks for [receiveContent] modifier to get information about certain Drag-and-Drop
+ * A set of callbacks for [contentReceiver] modifier to get information about certain Drag-and-Drop
  * state changes, as well as receiving the payload carrying [TransferableContent].
  *
- * [receiveContent]'s drop target supports nesting. When two [receiveContent] modifiers are nested
+ * [contentReceiver]'s drop target supports nesting. When two [contentReceiver] modifiers are nested
  * on the composition tree, parent's drop target actually includes child's bounds, meaning that
  * they are not mutually exclusive like the regular [dragAndDropTarget].
  *
- * Let's assume we have two [receiveContent] boxes named A and B where B is a child of A, aligned
+ * Let's assume we have two [contentReceiver] boxes named A and B where B is a child of A, aligned
  * to bottom end.
  *
  * ---------
@@ -51,22 +51,22 @@
  *
  * The interesting part in this order of calls is that A does not receive an exit event when the
  * item moves over to B. This is different than what would happen if you were to use
- * [dragAndDropTarget] modifier because semantically [receiveContent] works as a chain of nodes.
+ * [dragAndDropTarget] modifier because semantically [contentReceiver] works as a chain of nodes.
  * If the item were to be dropped on B, its [onReceive] chain would also call A's [onReceive] with
  * what's left from B.
  */
 @ExperimentalFoundationApi
-interface ReceiveContentListener {
+fun interface ReceiveContentListener {
 
     /**
-     * Optional callback that's called when a dragging session starts. All [receiveContent] nodes
+     * Optional callback that's called when a dragging session starts. All [contentReceiver] nodes
      * in the current composition tree receives this callback immediately.
      */
     fun onDragStart() = Unit
 
     /**
      * Optional callback that's called when a dragging session ends by either successful drop, or
-     * cancellation. All [receiveContent] nodes in the current composition tree receives this
+     * cancellation. All [contentReceiver] nodes in the current composition tree receives this
      * callback immediately.
      */
     fun onDragEnd() = Unit
@@ -83,9 +83,9 @@
 
     /**
      * Callback that's triggered when a content is successfully committed.
-     * Return an optional [TransferableContent] that contains the ignored parts of the received
+     * @return An optional [TransferableContent] that contains the ignored parts of the received
      * [TransferableContent] by this node. The remaining [TransferableContent] first will be sent to
-     * to the closest ancestor [receiveContent] modifier. This chain will continue until there's no
+     * to the closest ancestor [contentReceiver] modifier. This chain will continue until there's no
      * ancestor modifier left, or [TransferableContent] is fully consumed. After, the source
      * subsystem that created the original [TransferableContent] and initiated the chain will
      * receive any remaining items to apply its default behavior. For example a text editor that
@@ -94,15 +94,3 @@
      */
     fun onReceive(transferableContent: TransferableContent): TransferableContent?
 }
-
-@OptIn(ExperimentalFoundationApi::class)
-internal fun ReceiveContentListener(
-    onReceive: (TransferableContent) -> TransferableContent?
-): ReceiveContentListener {
-    val paramOnReceive = onReceive
-    return object : ReceiveContentListener {
-        override fun onReceive(transferableContent: TransferableContent): TransferableContent? {
-            return paramOnReceive(transferableContent)
-        }
-    }
-}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
index 00fb943..bc812cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/TransferableContent.kt
@@ -25,13 +25,13 @@
  *
  * Note; Consult platform-specific guidelines for best practices in content transfer operations.
  *
- * @param clipEntry The main content data, typically representing a text, image, file, or other
+ * @property clipEntry The main content data, typically representing a text, image, file, or other
  * transferable item.
- * @param source The source from which the content originated like Keyboard, DragAndDrop, or
+ * @property source The source from which the content originated like Keyboard, DragAndDrop, or
  * Clipboard.
- * @param clipMetadata Metadata associated with the content, providing additional information or
+ * @property clipMetadata Metadata associated with the content, providing additional information or
  * context.
- * @param platformTransferableContent Optional platform-specific representation of the content, or
+ * @property platformTransferableContent Optional platform-specific representation of the content, or
  * additional platform-specific information, that can be used to access platform level APIs.
  */
 @ExperimentalFoundationApi
@@ -51,10 +51,21 @@
 
         companion object {
 
+            /**
+             * Indicates that the [TransferableContent] originates from the soft keyboard (also
+             * known as input method editor or IME)
+             */
             val Keyboard = Source(0)
 
+            /**
+             * Indicates that the [TransferableContent] was passed on by the system drag and drop.
+             */
             val DragAndDrop = Source(1)
 
+            /**
+             * Indicates that the [TransferableContent] comes from the clipboard via paste.
+             * (e.g. "Paste" action in the floating action menu or "Ctrl+V" key combination)
+             */
             val Clipboard = Source(2)
         }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
index dbe906e..9cd4d0d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/content/internal/ReceiveContentConfiguration.kt
@@ -23,7 +23,7 @@
 import androidx.compose.foundation.content.ReceiveContentListener
 import androidx.compose.foundation.content.ReceiveContentNode
 import androidx.compose.foundation.content.TransferableContent
-import androidx.compose.foundation.content.receiveContent
+import androidx.compose.foundation.content.contentReceiver
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
 import androidx.compose.ui.modifier.modifierLocalOf
 
@@ -122,8 +122,8 @@
         }
 
     /**
-     * A getter that returns the closest [receiveContent] modifier configuration if this node is
-     * attached. It returns null if the node is detached or there is no parent [receiveContent]
+     * A getter that returns the closest [contentReceiver] modifier configuration if this node is
+     * attached. It returns null if the node is detached or there is no parent [contentReceiver]
      * found.
      */
     private fun getParentReceiveContentListener(): ReceiveContentListener? {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
index 1d4daa6..b640de3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicSecureTextField.kt
@@ -17,11 +17,9 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.input.ImeActionHandler
 import androidx.compose.foundation.text.input.InputTransformation
 import androidx.compose.foundation.text.input.TextFieldBuffer
@@ -35,10 +33,10 @@
 import androidx.compose.foundation.text.input.then
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.onFocusChanged
@@ -58,12 +56,10 @@
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.Density
-import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.launch
 
 /**
  * BasicSecureTextField is specifically designed for password entry fields and is a preconfigured
@@ -74,21 +70,15 @@
  *
  * @param state [TextFieldState] object that holds the internal state of a [BasicTextField].
  * @param modifier optional [Modifier] for this text field.
- * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text
- * field will be neither editable nor focusable, the input of the text field will not be selectable.
  * @param onSubmit Called when the user submits a form either by pressing the action button in the
  * input method editor (IME), or by pressing the enter key on a hardware keyboard. If the user
  * submits the form by pressing the action button in the IME, the provided IME action is passed to
  * the function. If the user submits the form by pressing the enter key on a hardware keyboard,
- * the defined [imeAction] parameter is passed to the function. Return true to indicate that the
- * action has been handled completely, which will skip the default behavior, such as hiding the
- * keyboard for the [ImeAction.Done] action.
- * @param imeAction The IME action. This IME action is honored by keyboard and may show specific
- * icons on the keyboard.
- * @param textObfuscationMode Determines the method used to obscure the input text.
- * @param keyboardType The keyboard type to be used in this text field. It is set to
- * [KeyboardType.Password] by default. Use [KeyboardType.NumberPassword] for numerical password
- * fields.
+ * the defined [KeyboardOptions.imeAction] parameter is passed to the function. Return true to
+ * indicate that the action has been handled completely, which will skip the default behavior,
+ * such as hiding the keyboard for the [ImeAction.Done] action.
+ * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text
+ * field will be neither editable nor focusable, the input of the text field will not be selectable.
  * @param inputTransformation Optional [InputTransformation] that will be used to transform changes
  * to the [TextFieldState] made by the user. The transformation will be applied to changes made by
  * hardware and software keyboard events, pasting or dropping text, accessibility services, and
@@ -97,12 +87,10 @@
  * it will be applied to the next user edit. The transformation will not immediately affect the
  * current [state].
  * @param textStyle Style configuration for text content that's displayed in the editor.
- * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
- * for this TextField. You can create and pass in your own remembered [MutableInteractionSource]
- * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField
- * for different [Interaction]s.
- * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified]
- * provided, there will be no cursor drawn.
+ * @param keyboardOptions Software keyboard options that contain configurations such as
+ * [KeyboardType] and [ImeAction]. This composable by default configures [KeyboardOptions] for a
+ * secure text field by disabling auto correct and setting [KeyboardType] to
+ * [KeyboardType.Password].
  * @param onTextLayout Callback that is executed when the text layout becomes queryable. The
  * callback receives a function that returns a [TextLayoutResult] if the layout can be calculated,
  * or null if it cannot. The function reads the layout result from a snapshot state object, and will
@@ -111,10 +99,15 @@
  * add additional decoration or functionality to the text. For example, to draw a cursor or
  * selection around the text. [Density] scope is the one that was used while creating the given text
  * layout.
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this TextField. You can create and pass in your own remembered [MutableInteractionSource]
+ * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField
+ * for different [Interaction]s.
+ * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified]
+ * provided, there will be no cursor drawn.
+ * @param textObfuscationMode Determines the method used to obscure the input text.
  * @param decorator Allows to add decorations around text field, such as icon, placeholder, helper
  * messages or similar, and automatically increase the hit target area of the text field.
- * @param scrollState Used to manage the horizontal scroll when the input content exceeds the
- * bounds of the text field. It controls the state of the scroll for the text field.
  */
 @ExperimentalFoundationApi
 // This takes a composable lambda, but it is not primarily a container.
@@ -125,23 +118,22 @@
     modifier: Modifier = Modifier,
     // TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
     onSubmit: ImeActionHandler? = null,
-    imeAction: ImeAction = ImeAction.Default,
-    textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
-    keyboardType: KeyboardType = KeyboardType.Password,
     enabled: Boolean = true,
     inputTransformation: InputTransformation? = null,
     textStyle: TextStyle = TextStyle.Default,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.SecureTextField,
+    onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null,
     interactionSource: MutableInteractionSource? = null,
     cursorBrush: Brush = SolidColor(Color.Black),
-    onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null,
+    textObfuscationMode: TextObfuscationMode = TextObfuscationMode.RevealLastTyped,
     decorator: TextFieldDecorator? = null,
-    scrollState: ScrollState = rememberScrollState(),
     // Last parameter must not be a function unless it's intended to be commonly used as a trailing
     // lambda.
 ) {
-    val coroutineScope = rememberCoroutineScope()
-    val secureTextFieldController = remember(coroutineScope) {
-        SecureTextFieldController(coroutineScope)
+    val secureTextFieldController = remember { SecureTextFieldController() }
+    LaunchedEffect(secureTextFieldController) {
+        // start a coroutine that listens for scheduled hide events.
+        secureTextFieldController.observeHideEvents()
     }
 
     // revealing last typed character depends on two conditions;
@@ -151,7 +143,7 @@
 
     // while toggling between obfuscation methods if the revealing gets disabled, reset the reveal.
     if (!revealLastTypedEnabled) {
-        secureTextFieldController.passwordRevealFilter.hide()
+        secureTextFieldController.passwordInputTransformation.hide()
     }
 
     val codepointTransformation = when {
@@ -160,7 +152,7 @@
         }
 
         textObfuscationMode == TextObfuscationMode.Hidden -> {
-            CodepointTransformation.mask('\u2022')
+            PasswordCodepointTransformation
         }
 
         else -> null
@@ -187,72 +179,61 @@
             enabled = enabled,
             readOnly = false,
             inputTransformation = if (revealLastTypedEnabled) {
-                inputTransformation.then(secureTextFieldController.passwordRevealFilter)
+                inputTransformation.then(secureTextFieldController.passwordInputTransformation)
             } else inputTransformation,
             textStyle = textStyle,
-            interactionSource = interactionSource,
-            cursorBrush = cursorBrush,
-            lineLimits = TextFieldLineLimits.SingleLine,
-            scrollState = scrollState,
-            keyboardOptions = KeyboardOptions(
-                autoCorrect = false,
-                keyboardType = keyboardType,
-                imeAction = imeAction
-            ),
+            keyboardOptions = keyboardOptions,
             keyboardActions = onSubmit?.let { KeyboardActions(onSubmit = it::onImeAction) }
                 ?: KeyboardActions.Default,
+            lineLimits = TextFieldLineLimits.SingleLine,
             onTextLayout = onTextLayout,
+            interactionSource = interactionSource,
+            cursorBrush = cursorBrush,
             codepointTransformation = codepointTransformation,
             decorator = decorator,
         )
     }
 }
 
-@OptIn(ExperimentalFoundationApi::class)
-internal class SecureTextFieldController(
-    coroutineScope: CoroutineScope
-) {
+internal class SecureTextFieldController {
     /**
      * A special [InputTransformation] that tracks changes to the content to identify the last typed
      * character to reveal. `scheduleHide` lambda is delegated to a member function to be able to
-     * use [passwordRevealFilter] instance.
+     * use [passwordInputTransformation] instance.
      */
-    val passwordRevealFilter = PasswordRevealFilter(::scheduleHide)
+    val passwordInputTransformation = PasswordInputTransformation(::scheduleHide)
 
     /**
      * Pass to [BasicTextField] for obscuring text input.
      */
     val codepointTransformation = CodepointTransformation { codepointIndex, codepoint ->
-        if (codepointIndex == passwordRevealFilter.revealCodepointIndex) {
+        if (codepointIndex == passwordInputTransformation.revealCodepointIndex) {
             // reveal the last typed character by not obscuring it
             codepoint
         } else {
-            0x2022
+            DEFAULT_OBFUSCATION_MASK.code
         }
     }
 
     val focusChangeModifier = Modifier.onFocusChanged {
-        if (!it.isFocused) passwordRevealFilter.hide()
+        if (!it.isFocused) passwordInputTransformation.hide()
     }
 
     private val resetTimerSignal = Channel<Unit>(Channel.UNLIMITED)
 
-    init {
-        // start a coroutine that listens for scheduled hide events.
-        coroutineScope.launch {
-            resetTimerSignal.consumeAsFlow()
-                .collectLatest {
-                    delay(LAST_TYPED_CHARACTER_REVEAL_DURATION_MILLIS)
-                    passwordRevealFilter.hide()
-                }
-        }
+    suspend fun observeHideEvents() {
+        resetTimerSignal.consumeAsFlow()
+            .collectLatest {
+                delay(LAST_TYPED_CHARACTER_REVEAL_DURATION_MILLIS)
+                passwordInputTransformation.hide()
+            }
     }
 
     private fun scheduleHide() {
         // signal the listener that a new hide call is scheduled.
         val result = resetTimerSignal.trySend(Unit)
-        if (!result.isSuccess) {
-            passwordRevealFilter.hide()
+        if (result.isFailure) {
+            passwordInputTransformation.hide()
         }
     }
 }
@@ -265,7 +246,7 @@
  * typed.
  */
 @OptIn(ExperimentalFoundationApi::class)
-internal class PasswordRevealFilter(
+internal class PasswordInputTransformation(
     val scheduleHide: () -> Unit
 ) : InputTransformation {
     // TODO: Consider setting this as a tracking annotation in AnnotatedString.
@@ -306,6 +287,11 @@
 // adopted from PasswordTransformationMethod from Android platform.
 private const val LAST_TYPED_CHARACTER_REVEAL_DURATION_MILLIS = 1500L
 
+private const val DEFAULT_OBFUSCATION_MASK = '\u2022'
+
+@OptIn(ExperimentalFoundationApi::class)
+private val PasswordCodepointTransformation = CodepointTransformation.mask(DEFAULT_OBFUSCATION_MASK)
+
 // TODO(b/297425359) Investigate cleaning up the IME action handling APIs.
 @OptIn(ExperimentalFoundationApi::class)
 private fun KeyboardActions(onSubmit: ImeActionHandler) = KeyboardActions(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index b7938b5..07cae9a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,6 +27,8 @@
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
+import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -401,6 +403,29 @@
             textDragObserver = manager.touchSelectionObserver,
         )
         .pointerHoverIcon(textPointerIcon)
+        .then(
+            if (isStylusHandwritingSupported) {
+                Modifier.pointerInput(enabled, readOnly) {
+                    if (enabled && !readOnly) {
+                        detectStylusHandwriting {
+                            if (!state.hasFocus) {
+                                focusRequester.requestFocus()
+                            }
+                            // TextInputService is calling LegacyTextInputServiceAdapter under the
+                            // hood.  And because it's a public API, startStylusHandwriting is added
+                            // to legacyTextInputServiceAdapter instead.
+                            // startStylusHandwriting may be called before the actual input
+                            // session starts when the editor is not focused, this is handled
+                            // internally by the LegacyTextInputServiceAdapter.
+                            legacyTextInputServiceAdapter.startStylusHandwriting()
+                            true
+                        }
+                    }
+                }
+            } else {
+                Modifier
+            }
+        )
 
     val drawModifier = Modifier.drawBehind {
         state.layoutResult?.let { layoutResult ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
index 6e68dc8..a5809ff 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/KeyboardOptions.kt
@@ -70,6 +70,15 @@
          */
         @Stable
         val Default = KeyboardOptions()
+
+        /**
+         * Default [KeyboardOptions] for [BasicSecureTextField].
+         */
+        @Stable
+        internal val SecureTextField = KeyboardOptions(
+            autoCorrect = false,
+            keyboardType = KeyboardType.Password
+        )
     }
 
     @Deprecated(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
new file mode 100644
index 0000000..3d4ba3b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.handwriting
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerInputChange
+import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.util.fastFirstOrNull
+
+/**
+ * A utility function that detects stylus movements and calls the [onHandwritingSlopExceeded] when
+ * it detects that stylus movement has exceeds the handwriting slop.
+ * If [onHandwritingSlopExceeded] returns true, this method will consume the events and consider
+ * that the handwriting has successfully started. Otherwise, it'll stop monitoring the current
+ * gesture.
+ */
+internal suspend inline fun PointerInputScope.detectStylusHandwriting(
+    crossinline onHandwritingSlopExceeded: () -> Boolean
+) {
+    awaitEachGesture {
+        val firstDown =
+            awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+        val isStylus =
+            firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+        if (!isStylus) {
+            return@awaitEachGesture
+        }
+        // Await the touch slop before long press timeout.
+        var exceedsTouchSlop: PointerInputChange? = null
+        // The stylus move must exceeds touch slop before long press timeout.
+        while (true) {
+            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
+            // The tracked pointer is consumed or lifted, stop tracking.
+            val change = pointerEvent.changes.fastFirstOrNull {
+                !it.isConsumed && it.id == firstDown.id && it.pressed
+            }
+            if (change == null) {
+                break
+            }
+
+            val time = change.uptimeMillis - firstDown.uptimeMillis
+            if (time >= viewConfiguration.longPressTimeoutMillis) {
+                break
+            }
+
+            val offset = change.position - firstDown.position
+            if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+                exceedsTouchSlop = change
+                break
+            }
+        }
+
+        if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
+            return@awaitEachGesture
+        }
+        exceedsTouchSlop.consume()
+
+        // Consume the remaining changes of this pointer.
+        while (true) {
+            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+            val pointerChange = pointerEvent.changes.fastFirstOrNull {
+                !it.isConsumed && it.id == firstDown.id && it.pressed
+            } ?: return@awaitEachGesture
+            pointerChange.consume()
+        }
+    }
+}
+
+/**
+ *  Whether the platform supports the stylus handwriting or not. This is for platform level support
+ *  and NOT for checking whether the IME supports handwriting.
+ */
+internal expect val isStylusHandwritingSupported: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
index d676e39..c4617590 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.kt
@@ -63,6 +63,8 @@
         textInputModifierNode?.softwareKeyboardController?.hide()
     }
 
+    abstract fun startStylusHandwriting()
+
     interface LegacyPlatformTextInputNode {
         val softwareKeyboardController: SoftwareKeyboardController?
         val layoutCoordinates: LayoutCoordinates?
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index 966fd1e..4a4d8b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -23,8 +23,6 @@
 import androidx.compose.foundation.content.internal.dragAndDropRequestPermission
 import androidx.compose.foundation.content.internal.getReceiveContentConfiguration
 import androidx.compose.foundation.content.readPlainText
-import androidx.compose.foundation.gestures.awaitEachGesture
-import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.interaction.HoverInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.text.BasicTextField
@@ -32,6 +30,8 @@
 import androidx.compose.foundation.text.KeyboardActionScope
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.InputTransformation
 import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
 import androidx.compose.foundation.text.input.internal.selection.TextToolbarState
@@ -45,9 +45,6 @@
 import androidx.compose.ui.input.key.KeyInputModifierNode
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
-import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.modifier.ModifierLocalModifierNode
@@ -65,7 +62,6 @@
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
-import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.PlatformTextInputModifierNode
 import androidx.compose.ui.platform.PlatformTextInputSession
@@ -94,7 +90,6 @@
 import androidx.compose.ui.text.input.KeyboardCapitalization
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastFirstOrNull
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -189,15 +184,17 @@
     LayoutAwareModifierNode {
 
     private val editable get() = enabled && !readOnly
-    private var _stylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
-    private val stylusHandwritingTrigger: MutableSharedFlow<Unit>
+
+    private var backingStylusHandwritingTrigger: MutableSharedFlow<Unit>? = null
+    private val stylusHandwritingTrigger: MutableSharedFlow<Unit>?
         get() {
-            val handwritingTrigger = _stylusHandwritingTrigger
-            if (handwritingTrigger != null) return handwritingTrigger
+            val finalStylusHandwritingTrigger = backingStylusHandwritingTrigger
+            if (finalStylusHandwritingTrigger != null) return finalStylusHandwritingTrigger
+            if (!isStylusHandwritingSupported) return null
             return MutableSharedFlow<Unit>(
                 replay = 1,
                 onBufferOverflow = BufferOverflow.DROP_LATEST
-            ).also { _stylusHandwritingTrigger = it }
+            ).also { backingStylusHandwritingTrigger = it }
         }
 
     private val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
@@ -228,80 +225,35 @@
                     detectTextFieldLongPressAndAfterDrag(requestFocus)
                 }
             }
-            launch(start = CoroutineStart.UNDISPATCHED) {
-                detectStylusHandwriting()
+            // Note: when editable changes (enabled or readOnly changes), this pointerInputModifier
+            // is reset. And we don't need to worry about cancel or launch the stylus handwriting
+            // detecting job.
+            if (isStylusHandwritingSupported && editable) {
+                 launch(start = CoroutineStart.UNDISPATCHED) {
+                    detectStylusHandwriting {
+                        if (!isFocused) {
+                            requestFocus()
+                        }
+
+                        // Send the handwriting start signal to platform.
+                        // The editor should send the signal when it is focused or is about
+                        // to gain focus, Here are more details:
+                        //   1) if the editor already has an active input session, the
+                        //   platform handwriting service should already listen to this flow
+                        //   and it'll start handwriting right away.
+                        //
+                        //   2) if the editor is not focused, but it'll be focused and
+                        //   create a new input session, one handwriting signal will be
+                        //   replayed when the platform collect this flow. And the platform
+                        //   should trigger handwriting accordingly.
+                        stylusHandwritingTrigger?.tryEmit(Unit)
+                        return@detectStylusHandwriting true
+                    }
+                }
             }
         }
     })
 
-    private suspend fun PointerInputScope.detectStylusHandwriting() {
-        awaitEachGesture {
-            val firstDown =
-                awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
-
-            val isStylus =
-                firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
-            if (!editable || !isStylus) {
-                return@awaitEachGesture
-            }
-
-            val viewConfiguration = currentValueOf(LocalViewConfiguration)
-
-            // Await the touch slop before long press timeout.
-            var exceedsTouchSlop: PointerInputChange? = null
-            // The stylus move must exceeds touch slop before long press timeout.
-            while (true) {
-                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
-                // The tracked pointer is consumed or lifted, stop tracking.
-                val change = pointerEvent.changes.fastFirstOrNull {
-                    !it.isConsumed && it.id == firstDown.id && it.pressed
-                }
-                if (change == null) {
-                    break
-                }
-
-                val time = change.uptimeMillis - firstDown.uptimeMillis
-                if (time >= viewConfiguration.longPressTimeoutMillis) {
-                    break
-                }
-
-                val offset = change.position - firstDown.position
-                if (offset.getDistance() > viewConfiguration.handwritingSlop) {
-                    exceedsTouchSlop = change
-                    break
-                }
-            }
-
-            if (exceedsTouchSlop == null) return@awaitEachGesture
-
-            exceedsTouchSlop.consume()
-
-            if (!isFocused) {
-                requestFocus()
-            }
-
-            // Send the handwriting start signal to platform.
-            // The editor should send the signal when it is focused or is about to gain focused,
-            // Here are more details:
-            //   1) if the editor already has an active input session, the platform handwriting
-            //   service should already listen to this flow and it'll start handwriting right away.
-            //
-            //   2) if the editor is not focused, but it'll be focused and create a new input
-            //   session, one handwriting signal will be replayed when the platform collect this
-            //   flow. And the platform should trigger handwriting accordingly.
-            stylusHandwritingTrigger.tryEmit(Unit)
-
-            // Consume the remaining changes of this pointer.
-            while (true) {
-                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
-                val pointerChange = pointerEvent.changes.fastFirstOrNull {
-                    !it.isConsumed && it.id == firstDown.id && it.pressed
-                } ?: return@awaitEachGesture
-                pointerChange.consume()
-            }
-        }
-    }
-
     /**
      * The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
      * need to keep a reference to this event to send a follow-up exit event.
@@ -765,7 +717,7 @@
     private fun disposeInputSession() {
         inputSessionJob?.cancel()
         inputSessionJob = null
-        stylusHandwritingTrigger.resetReplayCache()
+        stylusHandwritingTrigger?.resetReplayCache()
     }
 
     private fun startInputSessionOnWindowFocusChange() {
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
new file mode 100644
index 0000000..5667100
--- /dev/null
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.desktop.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.handwriting
+
+internal actual val isStylusHandwritingSupported: Boolean = false
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
index 5f7b203..079e229 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.desktop.kt
@@ -95,6 +95,10 @@
             input.focusedRect = rect
         }
     }
+
+    override fun startStylusHandwriting() {
+        // Noop for desktop
+    }
 }
 
 internal class LegacyTextInputMethodRequest(
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
index 12b0a9d..417e47f 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/PreferenceAsState.kt
@@ -25,9 +25,9 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.preference.PreferenceManager
 
 /**
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
index da20cd1..ca9dffc 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/settings/SoftInputModeSetting.kt
@@ -27,8 +27,8 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.withFrameMillis
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle.State.RESUMED
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.preference.DropDownPreference
 import androidx.preference.Preference.SummaryProvider
diff --git a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
index a108a27..cb70f48 100644
--- a/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
+++ b/compose/material/material-ripple/src/androidInstrumentedTest/kotlin/androidx/compose/material/ripple/RippleModifierNodeTest.kt
@@ -50,6 +50,8 @@
 import androidx.compose.ui.graphics.asAndroidBitmap
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.node.DelegatableNode
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.captureToImage
@@ -62,6 +64,8 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.launch
 import org.junit.Rule
 import org.junit.Test
@@ -153,6 +157,120 @@
         )
     }
 
+    /**
+     * Regression test for b/329693006
+     */
+    @Test
+    fun pressed_rippleCreatedBeforeDraw() {
+        // Add a static press interaction so that when the ripple is added, it will add a ripple
+        // immediately before the node is drawn
+        val interactionSource = object : MutableInteractionSource {
+            override val interactions: Flow<Interaction> =
+                flowOf(PressInteraction.Press(Offset.Zero))
+            override suspend fun emit(interaction: Interaction) {}
+            override fun tryEmit(interaction: Interaction): Boolean { return true }
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                RippleBoxWithBackground(
+                    interactionSource,
+                    TestRipple,
+                    bounded = true
+                )
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            TestRippleColor,
+            rippleOpacity = TestRippleAlpha.pressedAlpha
+        )
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            // Unused
+            PressInteraction.Press(Offset(10f, 10f)),
+            expectedColor
+        )
+    }
+
+    /**
+     * Regression test for b/329693006, similar to [pressed_rippleCreatedBeforeDraw], but delegating
+     * to the ripple node later in time to simulate clickable behavior.
+     */
+    @Test
+    fun pressed_rippleLazilyDelegatedTo() {
+        // Add a static press interaction so that when the ripple is added, it will add a ripple
+        // immediately before the node is drawn
+        val interactionSource = object : MutableInteractionSource {
+            override val interactions: Flow<Interaction> =
+                flowOf(PressInteraction.Press(Offset.Zero))
+            override suspend fun emit(interaction: Interaction) {}
+            override fun tryEmit(interaction: Interaction): Boolean { return true }
+        }
+
+        class TestRippleNode : DelegatingNode() {
+            fun attachRipple() {
+                delegate(TestRipple.create(interactionSource))
+            }
+        }
+
+        val node = TestRippleNode()
+
+        val element = object : ModifierNodeElement<TestRippleNode>() {
+            override fun create(): TestRippleNode = node
+            override fun update(node: TestRippleNode) {}
+            override fun equals(other: Any?): Boolean = other === this
+            override fun hashCode(): Int = -1
+        }
+
+        var scope: CoroutineScope? = null
+
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+                    Box(
+                        Modifier.padding(25.dp).background(RippleBoxBackgroundColor)
+                    ) {
+                        val shape = RoundedCornerShape(20)
+                        val clip = Modifier.clip(shape)
+                        Box(
+                            Modifier.padding(25.dp).width(40.dp).height(40.dp)
+                                .border(BorderStroke(2.dp, Color.Black), shape)
+                                .background(color = RippleBoxBackgroundColor, shape = shape)
+                                .then(clip)
+                                .then(element)
+                        ) {}
+                    }
+                }
+            }
+        }
+
+        val expectedColor = calculateResultingRippleColor(
+            TestRippleColor,
+            rippleOpacity = TestRippleAlpha.pressedAlpha
+        )
+
+        // Add the ripple node to the hierarchy, which should then create a ripple before the node
+        // has been drawn
+        rule.runOnIdle {
+            node.attachRipple()
+        }
+
+        assertRippleMatches(
+            scope!!,
+            interactionSource,
+            // Unused
+            PressInteraction.Press(Offset(10f, 10f)),
+            expectedColor
+        )
+    }
+
     @Test
     fun hovered() {
         val interactionSource = MutableInteractionSource()
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
index 6eaccdb..766e2de5 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/Ripple.android.kt
@@ -126,18 +126,7 @@
             invalidateDraw()
         }
 
-    /**
-     * Cache the size of the canvas we will draw the ripple into - this is updated each time
-     * [draw] is called. This is needed as before we start animating the ripple, we
-     * need to know its size (changing the bounds mid-animation will cause us to continue the
-     * animation on the UI thread, not the render thread), but the size is only known inside the
-     * draw scope.
-     */
-    private var rippleSize: Size = Size.Zero
-
     override fun DrawScope.drawRipples() {
-        rippleSize = size
-
         drawIntoCanvas { canvas ->
             rippleHostView?.run {
                 // We set these inside addRipple() already, but they may change during the ripple
@@ -146,9 +135,14 @@
                 // currently drawn ripples if the ripples are being drawn on the RenderThread,
                 // since only the software paint is updated, not the hardware paint used in
                 // RippleForeground.
-                updateRippleProperties(
-                    size = size,
-                    radius = targetRadius.roundToInt(),
+                // Radius updates will not take effect until the next ripple, so if the size changes
+                // the only way to update the calculated radius is by using
+                // RippleDrawable.RADIUS_AUTO to calculate the radius from the bounds automatically.
+                // But in this case, if the bounds change, the animation will switch to the UI
+                // thread instead of render thread, so this isn't clearly desired either.
+                // b/183019123
+                setRippleProperties(
+                    size = rippleSize,
                     color = rippleColor,
                     alpha = rippleAlpha().pressedAlpha
                 )
@@ -158,13 +152,13 @@
         }
     }
 
-    override fun addRipple(interaction: PressInteraction.Press) {
+    override fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float) {
         rippleHostView = with(getOrCreateRippleContainer()) {
             getRippleHostView().apply {
                 addRipple(
                     interaction = interaction,
                     bounded = bounded,
-                    size = rippleSize,
+                    size = size,
                     radius = targetRadius.roundToInt(),
                     color = rippleColor,
                     alpha = rippleAlpha().pressedAlpha,
@@ -280,9 +274,8 @@
                 // currently drawn ripples if the ripples are being drawn on the RenderThread,
                 // since only the software paint is updated, not the hardware paint used in
                 // RippleForeground.
-                updateRippleProperties(
+                setRippleProperties(
                     size = size,
-                    radius = rippleRadius,
                     color = color,
                     alpha = alpha
                 )
diff --git a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
index fa4fa68..aa462e2 100644
--- a/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
+++ b/compose/material/material-ripple/src/androidMain/kotlin/androidx/compose/material/ripple/RippleHostView.android.kt
@@ -135,7 +135,8 @@
         }
         val ripple = ripple!!
         this.onInvalidateRipple = onInvalidateRipple
-        updateRippleProperties(size, radius, color, alpha)
+        ripple.trySetRadius(radius)
+        setRippleProperties(size, color, alpha)
         if (bounded) {
             // Bounded ripples should animate from the press position
             ripple.setHotspot(interaction.pressPosition.x, interaction.pressPosition.y)
@@ -161,13 +162,10 @@
     }
 
     /**
-     * Update the underlying [RippleDrawable] with the new properties. Note that changes to
-     * [size] or [radius] while a ripple is animating will cause the animation to move to the UI
-     * thread, so it is important to also provide the correct values in [addRipple].
+     * Update the underlying [RippleDrawable] with the new properties.
      */
-    fun updateRippleProperties(
+    fun setRippleProperties(
         size: Size,
-        radius: Int,
         color: Color,
         alpha: Float
     ) {
@@ -176,7 +174,6 @@
         // (either here or internally in RippleDrawable). Many properties invalidate the ripple when
         // changed, which will lead to a call to updateRippleProperties again, which will cause
         // another invalidation, etc.
-        ripple.trySetRadius(radius)
         ripple.setColor(color, alpha)
         val newBounds = Rect(
             0,
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
index 112c2be..7dfe12a 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/CommonRipple.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.State
 import androidx.compose.runtime.mutableStateMapOf
 import androidx.compose.runtime.remember
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorProducer
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -72,7 +73,7 @@
 ) : RippleNode(interactionSource, bounded, radius, color, rippleAlpha) {
     private val ripples = MutableScatterMap<PressInteraction.Press, RippleAnimation>()
 
-    override fun addRipple(interaction: PressInteraction.Press) {
+    override fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float) {
         // Finish existing ripples
         ripples.forEach { _, ripple -> ripple.finish() }
         val origin = if (bounded) interaction.pressPosition else null
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index 483a851..d747be5f 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.material.ripple
 
+import androidx.collection.mutableObjectListOf
 import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.LinearEasing
@@ -34,6 +35,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ColorProducer
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
@@ -44,9 +46,13 @@
 import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
 import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.requireDensity
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.isUnspecified
+import androidx.compose.ui.unit.toSize
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 
@@ -320,40 +326,80 @@
     private val radius: Dp,
     private val color: ColorProducer,
     protected val rippleAlpha: () -> RippleAlpha
-) : Modifier.Node(), CompositionLocalConsumerModifierNode, DrawModifierNode {
+) : Modifier.Node(),
+    CompositionLocalConsumerModifierNode,
+    DrawModifierNode,
+    LayoutAwareModifierNode {
     final override val shouldAutoInvalidate: Boolean = false
 
     private var stateLayer: StateLayer? = null
 
-    // Calculated inside draw(). This won't happen in Robolectric, so default to 0f to avoid crashes
-    var targetRadius: Float = 0f
+    // The following are calculated inside onRemeasured(). These must be initialized before adding
+    // a ripple.
+
+    // Target radius updating over time for existing ripples isn't supported for Android, and
+    // isn't implemented in common, so for now it can be private.
+    private var targetRadius: Float = 0f
+    // The size is needed for Android to update ripple bounds if the size changes
+    protected var rippleSize: Size = Size.Zero
         private set
 
     val rippleColor: Color
         get() = color()
 
-    final override fun onAttach() {
+    // Track interactions that were emitted before we have been placed - we need to wait until we
+    // have a valid size in order to set the radius and size correctly.
+    private var hasValidSize = false
+    private val pendingInteractions = mutableObjectListOf<PressInteraction>()
+
+    override fun onRemeasured(size: IntSize) {
+        hasValidSize = true
+        val density = requireDensity()
+        rippleSize = size.toSize()
+        targetRadius = with(density) {
+            if (radius.isUnspecified) {
+                // Explicitly calculate the radius instead of using RippleDrawable.RADIUS_AUTO on
+                // Android since the latest spec does not match with the existing radius calculation
+                // in the framework.
+                getRippleEndRadius(bounded, rippleSize)
+            } else {
+                radius.toPx()
+            }
+        }
+        // Flush any pending interactions that were waiting for measurement
+        pendingInteractions.forEach {
+            handlePressInteraction(it)
+        }
+        pendingInteractions.clear()
+    }
+
+    override fun onAttach() {
         coroutineScope.launch {
             interactionSource.interactions.collect { interaction ->
                 when (interaction) {
-                    is PressInteraction.Press -> addRipple(interaction)
-                    is PressInteraction.Release -> removeRipple(interaction.press)
-                    is PressInteraction.Cancel -> removeRipple(interaction.press)
+                    is PressInteraction -> {
+                        if (hasValidSize) {
+                            handlePressInteraction(interaction)
+                        } else {
+                            // Handle these later when we have a valid size
+                            pendingInteractions += interaction
+                        }
+                    }
                     else -> updateStateLayer(interaction, this)
                 }
             }
         }
     }
 
-    override fun ContentDrawScope.draw() {
-        targetRadius = if (radius.isUnspecified) {
-            // Explicitly calculate the radius instead of using RippleDrawable.RADIUS_AUTO on
-            // Android since the latest spec does not match with the existing radius calculation in
-            // the framework.
-            getRippleEndRadius(bounded, size)
-        } else {
-            radius.toPx()
+    private fun handlePressInteraction(pressInteraction: PressInteraction) {
+        when (pressInteraction) {
+            is PressInteraction.Press -> addRipple(pressInteraction, rippleSize, targetRadius)
+            is PressInteraction.Release -> removeRipple(pressInteraction.press)
+            is PressInteraction.Cancel -> removeRipple(pressInteraction.press)
         }
+    }
+
+    override fun ContentDrawScope.draw() {
         drawContent()
         stateLayer?.run {
             drawStateLayer(targetRadius, rippleColor)
@@ -363,7 +409,7 @@
 
     abstract fun DrawScope.drawRipples()
 
-    abstract fun addRipple(interaction: PressInteraction.Press)
+    abstract fun addRipple(interaction: PressInteraction.Press, size: Size, targetRadius: Float)
     abstract fun removeRipple(interaction: PressInteraction.Press)
     private fun updateStateLayer(interaction: Interaction, scope: CoroutineScope) {
         val stateLayer = stateLayer ?: StateLayer(bounded, rippleAlpha).also { instance ->
diff --git a/compose/material3/adaptive/adaptive-layout/api/current.txt b/compose/material3/adaptive/adaptive-layout/api/current.txt
index 6d85541..c60dfc7 100644
--- a/compose/material3/adaptive/adaptive-layout/api/current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/current.txt
@@ -28,13 +28,11 @@
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldDefaults {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy detailPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy listPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
     field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults INSTANCE;
   }
 
   public final class ListDetailPaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldRole {
@@ -59,14 +57,12 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
-    ctor public PaneScaffoldDirective(androidx.compose.foundation.layout.PaddingValues contentPadding, int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
-    method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
     method public java.util.List<androidx.compose.ui.geometry.Rect> getExcludedBounds();
     method public float getHorizontalPartitionSpacerSize();
     method public int getMaxHorizontalPartitions();
     method public int getMaxVerticalPartitions();
     method public float getVerticalPartitionSpacerSize();
-    property public final androidx.compose.foundation.layout.PaddingValues contentPadding;
     property public final java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds;
     property public final float horizontalPartitionSpacerSize;
     property public final int maxHorizontalPartitions;
@@ -85,13 +81,11 @@
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
     field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
   }
 
   public final class SupportingPaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
diff --git a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
index 6d85541..c60dfc7 100644
--- a/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
+++ b/compose/material3/adaptive/adaptive-layout/api/restricted_current.txt
@@ -28,13 +28,11 @@
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldDefaults {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy detailPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy listPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
     field public static final androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldDefaults INSTANCE;
   }
 
   public final class ListDetailPaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void ListDetailPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> listPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> detailPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class ListDetailPaneScaffoldRole {
@@ -59,14 +57,12 @@
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Immutable public final class PaneScaffoldDirective {
-    ctor public PaneScaffoldDirective(androidx.compose.foundation.layout.PaddingValues contentPadding, int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
-    method public androidx.compose.foundation.layout.PaddingValues getContentPadding();
+    ctor public PaneScaffoldDirective(int maxHorizontalPartitions, float horizontalPartitionSpacerSize, int maxVerticalPartitions, float verticalPartitionSpacerSize, java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds);
     method public java.util.List<androidx.compose.ui.geometry.Rect> getExcludedBounds();
     method public float getHorizontalPartitionSpacerSize();
     method public int getMaxHorizontalPartitions();
     method public int getMaxVerticalPartitions();
     method public float getVerticalPartitionSpacerSize();
-    property public final androidx.compose.foundation.layout.PaddingValues contentPadding;
     property public final java.util.List<androidx.compose.ui.geometry.Rect> excludedBounds;
     property public final float horizontalPartitionSpacerSize;
     property public final int maxHorizontalPartitions;
@@ -85,13 +81,11 @@
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldDefaults {
     method public androidx.compose.material3.adaptive.layout.ThreePaneScaffoldAdaptStrategies adaptStrategies(optional androidx.compose.material3.adaptive.layout.AdaptStrategy mainPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy supportingPaneAdaptStrategy, optional androidx.compose.material3.adaptive.layout.AdaptStrategy extraPaneAdaptStrategy);
-    method @androidx.compose.runtime.Composable public androidx.compose.foundation.layout.WindowInsets getWindowInsets();
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.layout.WindowInsets windowInsets;
     field public static final androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldDefaults INSTANCE;
   }
 
   public final class SupportingPaneScaffoldKt {
-    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane, optional androidx.compose.foundation.layout.WindowInsets windowInsets);
+    method @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi @androidx.compose.runtime.Composable public static void SupportingPaneScaffold(androidx.compose.material3.adaptive.layout.PaneScaffoldDirective directive, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> mainPane, kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit> supportingPane, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function1<? super androidx.compose.material3.adaptive.layout.ThreePaneScaffoldScope,kotlin.Unit>? extraPane);
   }
 
   @SuppressCompatibility @androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi public final class SupportingPaneScaffoldRole {
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
index e22fcb5..c7be96d 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldScreenshotTest.kt
@@ -19,7 +19,6 @@
 import android.os.Build
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material3.MaterialTheme
@@ -132,48 +131,6 @@
     }
 
     @Test
-    fun threePaneScaffold_insets_compact_size_window() {
-        val mockInsets = WindowInsets(100.dp, 10.dp, 20.dp, 50.dp)
-        rule.setContent {
-            SampleThreePaneScaffoldWithInsets(mockInsets)
-        }
-
-        rule.onNodeWithTag(ThreePaneScaffoldTestTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "threePaneScaffold_insets_compact")
-    }
-
-    @Test
-    fun threePaneScaffold_insets_medium_size_window() {
-        val mockInsets = WindowInsets(100.dp, 10.dp, 20.dp, 50.dp)
-        rule.setContentWithSimulatedSize(
-            simulatedWidth = 700.dp,
-            simulatedHeight = 500.dp
-        ) {
-            SampleThreePaneScaffoldWithInsets(mockInsets)
-        }
-
-        rule.onNodeWithTag(ThreePaneScaffoldTestTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "threePaneScaffold_insets_medium")
-    }
-
-    @Test
-    fun threePaneScaffold_insets_expanded_size_window() {
-        val mockInsets = WindowInsets(100.dp, 10.dp, 20.dp, 50.dp)
-        rule.setContentWithSimulatedSize(
-            simulatedWidth = 1024.dp,
-            simulatedHeight = 800.dp
-        ) {
-            SampleThreePaneScaffoldWithInsets(mockInsets)
-        }
-
-        rule.onNodeWithTag(ThreePaneScaffoldTestTag)
-            .captureToImage()
-            .assertAgainstGolden(screenshotRule, "threePaneScaffold_insets_expanded")
-    }
-
-    @Test
     fun threePaneScaffold_paneExpansion_fixedFirstPaneWidth() {
         rule.setContentWithSimulatedSize(
             simulatedWidth = 1024.dp,
@@ -454,27 +411,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 @Composable
-private fun SampleThreePaneScaffoldWithInsets(
-    windowInsets: WindowInsets
-) {
-    val scaffoldDirective = calculateStandardPaneScaffoldDirective(
-        currentWindowAdaptiveInfo()
-    )
-    val scaffoldValue = calculateThreePaneScaffoldValue(
-        scaffoldDirective.maxHorizontalPartitions,
-        ThreePaneScaffoldDefaults.adaptStrategies(),
-        null
-    )
-    SampleThreePaneScaffold(
-        scaffoldDirective = scaffoldDirective,
-        scaffoldValue = scaffoldValue,
-        paneOrder = ThreePaneScaffoldDefaults.ListDetailLayoutPaneOrder,
-        windowInsets = windowInsets
-    )
-}
-
-@OptIn(ExperimentalMaterial3AdaptiveApi::class)
-@Composable
 private fun SampleThreePaneScaffoldWithPaneExpansion(
     paneExpansionState: PaneExpansionState,
     paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
index 47a4cd9..015fc59 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffoldTest.kt
@@ -16,12 +16,7 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.union
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
@@ -174,7 +169,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockScaffoldDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(0.dp),
     maxHorizontalPartitions = 1,
     horizontalPartitionSpacerSize = 0.dp,
     maxVerticalPartitions = 1,
@@ -202,7 +196,6 @@
     paneOrder: ThreePaneScaffoldHorizontalOrder,
     paneExpansionDragHandle: (@Composable (PaneExpansionState) -> Unit)? = null,
     paneExpansionState: PaneExpansionState = PaneExpansionState(),
-    windowInsets: WindowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout)
 ) {
     ThreePaneScaffold(
         modifier = Modifier.fillMaxSize().testTag(ThreePaneScaffoldTestTag),
@@ -211,7 +204,6 @@
         paneOrder = paneOrder,
         paneExpansionState = paneExpansionState,
         paneExpansionDragHandle = paneExpansionDragHandle,
-        windowInsets = windowInsets,
         secondaryPane = {
             AnimatedPane(
                 modifier = Modifier.testTag(tag = "SecondaryPane")
diff --git a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
index 2813adb..1d6be9b 100644
--- a/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/androidUnitTest/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirectiveTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.material3.adaptive.Posture
 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.window.core.layout.WindowSizeClass
 import com.google.common.truth.Truth.assertThat
@@ -43,15 +42,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(1)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(16.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(16.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(0.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(16.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(16.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -66,15 +57,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(1)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(0.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -89,15 +72,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(2)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -112,15 +87,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(1)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(2)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(0.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(24.dp)
     }
 
@@ -135,15 +102,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(1)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(16.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(16.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(0.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(16.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(16.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -158,15 +117,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(2)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -181,15 +132,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(2)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(1)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(0.dp)
     }
 
@@ -204,15 +147,7 @@
 
         assertThat(scaffoldDirective.maxHorizontalPartitions).isEqualTo(2)
         assertThat(scaffoldDirective.maxVerticalPartitions).isEqualTo(2)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateLeftPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
-        assertThat(
-            scaffoldDirective.contentPadding.calculateRightPadding(LayoutDirection.Ltr)
-        ).isEqualTo(24.dp)
         assertThat(scaffoldDirective.horizontalPartitionSpacerSize).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateTopPadding()).isEqualTo(24.dp)
-        assertThat(scaffoldDirective.contentPadding.calculateBottomPadding()).isEqualTo(24.dp)
         assertThat(scaffoldDirective.verticalPartitionSpacerSize).isEqualTo(24.dp)
     }
 
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
index aa58ec5..74f39ac 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ListDetailPaneScaffold.kt
@@ -16,11 +16,7 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.union
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -47,7 +43,6 @@
  * @param extraPane the extra pane of the scaffold, which is supposed to hold any supplementary info
  *        besides the list and the detail panes, for example, a task list or a mini-calendar view of
  *        a mail app. See [ListDetailPaneScaffoldRole.Extra].
- * @param windowInsets window insets that the scaffold will respect.
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
@@ -58,14 +53,12 @@
     detailPane: @Composable ThreePaneScaffoldScope.() -> Unit,
     modifier: Modifier = Modifier,
     extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
-    windowInsets: WindowInsets = ListDetailPaneScaffoldDefaults.windowInsets,
 ) {
     ThreePaneScaffold(
         modifier = modifier.fillMaxSize(),
         scaffoldDirective = directive,
         scaffoldValue = value,
         paneOrder = ThreePaneScaffoldDefaults.ListDetailLayoutPaneOrder,
-        windowInsets = windowInsets,
         secondaryPane = listPane,
         tertiaryPane = extraPane,
         primaryPane = detailPane
@@ -78,13 +71,6 @@
 @ExperimentalMaterial3AdaptiveApi
 object ListDetailPaneScaffoldDefaults {
     /**
-     * Default insets that will be used and consumed by [ListDetailPaneScaffold]. By default it will
-     * be the union of [WindowInsets.Companion.systemBars] and
-     * [WindowInsets.Companion.displayCutout].
-     */
-    val windowInsets @Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout)
-
-    /**
      * Creates a default [ThreePaneScaffoldAdaptStrategies] for [ListDetailPaneScaffold].
      *
      * @param detailPaneAdaptStrategy the adapt strategy of the primary pane
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
index 18bc602..c4977e1 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/PaneScaffoldDirective.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.Posture
 import androidx.compose.material3.adaptive.WindowAdaptiveInfo
@@ -51,22 +50,18 @@
     verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
 ): PaneScaffoldDirective {
     val maxHorizontalPartitions: Int
-    val contentPadding: PaddingValues
     val verticalSpacerSize: Dp
     when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
         WindowWidthSizeClass.COMPACT -> {
             maxHorizontalPartitions = 1
-            contentPadding = PaddingValues(16.dp)
             verticalSpacerSize = 0.dp
         }
         WindowWidthSizeClass.MEDIUM -> {
             maxHorizontalPartitions = 1
-            contentPadding = PaddingValues(24.dp)
             verticalSpacerSize = 0.dp
         }
         else -> {
             maxHorizontalPartitions = 2
-            contentPadding = PaddingValues(24.dp)
             verticalSpacerSize = 24.dp
         }
     }
@@ -83,7 +78,6 @@
     }
 
     return PaneScaffoldDirective(
-        contentPadding,
         maxHorizontalPartitions,
         verticalSpacerSize,
         maxVerticalPartitions,
@@ -113,22 +107,18 @@
     verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating
 ): PaneScaffoldDirective {
     val maxHorizontalPartitions: Int
-    val contentPadding: PaddingValues
     val verticalSpacerSize: Dp
     when (windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass) {
         WindowWidthSizeClass.COMPACT -> {
             maxHorizontalPartitions = 1
-            contentPadding = PaddingValues(16.dp)
             verticalSpacerSize = 0.dp
         }
         WindowWidthSizeClass.MEDIUM -> {
             maxHorizontalPartitions = 2
-            contentPadding = PaddingValues(24.dp)
             verticalSpacerSize = 24.dp
         }
         else -> {
             maxHorizontalPartitions = 2
-            contentPadding = PaddingValues(24.dp)
             verticalSpacerSize = 24.dp
         }
     }
@@ -144,7 +134,6 @@
     }
 
     return PaneScaffoldDirective(
-        contentPadding,
         maxHorizontalPartitions,
         verticalSpacerSize,
         maxVerticalPartitions,
@@ -168,7 +157,6 @@
  * partitions the layout can be split into and what should be the gutter size.
  *
  * @constructor create an instance of [PaneScaffoldDirective]
- * @param contentPadding Size of the paddings between the panes and the outer bounds of the layout.
  * @param maxHorizontalPartitions the max number of partitions along the horizontal axis the layout
  *        can be split into.
  * @param horizontalPartitionSpacerSize Size of the spacers between horizontal partitions.
@@ -183,7 +171,6 @@
 @ExperimentalMaterial3AdaptiveApi
 @Immutable
 class PaneScaffoldDirective(
-    val contentPadding: PaddingValues,
     val maxHorizontalPartitions: Int,
     val horizontalPartitionSpacerSize: Dp,
     val maxVerticalPartitions: Int,
@@ -193,7 +180,6 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is PaneScaffoldDirective) return false
-        if (contentPadding != other.contentPadding) return false
         if (maxHorizontalPartitions != other.maxHorizontalPartitions) return false
         if (horizontalPartitionSpacerSize != other.horizontalPartitionSpacerSize) return false
         if (maxVerticalPartitions != other.maxVerticalPartitions) return false
@@ -202,8 +188,7 @@
     }
 
     override fun hashCode(): Int {
-        var result = contentPadding.hashCode()
-        result = 31 * result + maxHorizontalPartitions
+        var result = maxHorizontalPartitions
         result = 31 * result + horizontalPartitionSpacerSize.hashCode()
         result = 31 * result + maxVerticalPartitions
         result = 31 * result + verticalPartitionSpacerSize.hashCode()
@@ -211,8 +196,7 @@
     }
 
     override fun toString(): String {
-        return "PaneScaffoldDirective(contentPadding=$contentPadding, " +
-            "maxHorizontalPartitions=$maxHorizontalPartitions, " +
+        return "PaneScaffoldDirective(maxHorizontalPartitions=$maxHorizontalPartitions, " +
             "horizontalPartitionSpacerSize=$horizontalPartitionSpacerSize, " +
             "maxVerticalPartitions=$maxVerticalPartitions, " +
             "verticalPartitionSpacerSize=$verticalPartitionSpacerSize, " +
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
index a70ea4e..8c62ee1 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/SupportingPaneScaffold.kt
@@ -16,11 +16,7 @@
 
 package androidx.compose.material3.adaptive.layout
 
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.displayCutout
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.systemBars
-import androidx.compose.foundation.layout.union
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -41,7 +37,6 @@
  * @param extraPane the extra pane of the scaffold, which is supposed to hold any additional content
  *        besides the main and the supporting panes, for example, a styling panel in a doc app.
  *        See [SupportingPaneScaffoldRole.Extra].
- * @param windowInsets window insets that the scaffold will respect.
  */
 @ExperimentalMaterial3AdaptiveApi
 @Composable
@@ -52,14 +47,12 @@
     supportingPane: @Composable ThreePaneScaffoldScope.() -> Unit,
     modifier: Modifier = Modifier,
     extraPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
-    windowInsets: WindowInsets = SupportingPaneScaffoldDefaults.windowInsets,
 ) {
     ThreePaneScaffold(
         modifier = modifier.fillMaxSize(),
         scaffoldDirective = directive,
         scaffoldValue = value,
         paneOrder = ThreePaneScaffoldDefaults.SupportingPaneLayoutPaneOrder,
-        windowInsets = windowInsets,
         secondaryPane = supportingPane,
         tertiaryPane = extraPane,
         primaryPane = mainPane
@@ -72,13 +65,6 @@
 @ExperimentalMaterial3AdaptiveApi
 object SupportingPaneScaffoldDefaults {
     /**
-     * Default insets that will be used and consumed by [SupportingPaneScaffold]. By default it will
-     * be the union of [WindowInsets.Companion.systemBars] and
-     * [WindowInsets.Companion.displayCutout].
-     */
-    val windowInsets @Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout)
-
-    /**
      * Creates a default [ThreePaneScaffoldAdaptStrategies] for [SupportingPaneScaffold].
      *
      * @param mainPaneAdaptStrategy the adapt strategy of the main pane
diff --git a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
index b7d2264..0540d83 100644
--- a/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
+++ b/compose/material3/adaptive/adaptive-layout/src/commonMain/kotlin/androidx/compose/material3/adaptive/layout/ThreePaneScaffold.kt
@@ -25,7 +25,6 @@
 import androidx.compose.animation.core.Transition
 import androidx.compose.animation.core.rememberTransition
 import androidx.compose.animation.core.snap
-import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
@@ -90,7 +89,6 @@
     scaffoldDirective: PaneScaffoldDirective,
     scaffoldValue: ThreePaneScaffoldValue,
     paneOrder: ThreePaneScaffoldHorizontalOrder,
-    windowInsets: WindowInsets,
     secondaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
     tertiaryPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
     paneExpansionState: PaneExpansionState = PaneExpansionState(),
@@ -108,7 +106,6 @@
         scaffoldDirective = scaffoldDirective,
         scaffoldState = scaffoldState,
         paneOrder = paneOrder,
-        windowInsets = windowInsets,
         secondaryPane = secondaryPane,
         tertiaryPane = tertiaryPane,
         paneExpansionState = paneExpansionState,
@@ -124,7 +121,6 @@
     scaffoldDirective: PaneScaffoldDirective,
     scaffoldState: SeekableTransitionState<ThreePaneScaffoldValue>,
     paneOrder: ThreePaneScaffoldHorizontalOrder,
-    windowInsets: WindowInsets,
     secondaryPane: @Composable ThreePaneScaffoldScope.() -> Unit,
     tertiaryPane: (@Composable ThreePaneScaffoldScope.() -> Unit)? = null,
     paneExpansionState: PaneExpansionState = PaneExpansionState(),
@@ -239,13 +235,11 @@
                 scaffoldState.targetState,
                 paneExpansionState,
                 ltrPaneOrder,
-                windowInsets
             )
         }.apply {
             this.scaffoldDirective = scaffoldDirective
             this.scaffoldValue = scaffoldState.targetState
             this.paneOrder = ltrPaneOrder
-            this.windowInsets = windowInsets
         }
 
         Layout(
@@ -265,12 +259,10 @@
     scaffoldValue: ThreePaneScaffoldValue,
     val paneExpansionState: PaneExpansionState,
     paneOrder: ThreePaneScaffoldHorizontalOrder,
-    windowInsets: WindowInsets
 ) : MultiContentMeasurePolicy {
     var scaffoldDirective by mutableStateOf(scaffoldDirective)
     var scaffoldValue by mutableStateOf(scaffoldValue)
     var paneOrder by mutableStateOf(paneOrder)
-    var windowInsets by mutableStateOf(windowInsets)
 
     /**
      * Data class that is used to store the position and width of an expanded pane to be reused when
@@ -317,32 +309,16 @@
             }
 
             val verticalSpacerSize = scaffoldDirective.horizontalPartitionSpacerSize.roundToPx()
-            val leftContentPadding = max(
-                scaffoldDirective.contentPadding.calculateLeftPadding(layoutDirection).roundToPx(),
-                windowInsets.getLeft(this@measure, layoutDirection)
-            )
-            val rightContentPadding = max(
-                scaffoldDirective.contentPadding.calculateRightPadding(layoutDirection).roundToPx(),
-                windowInsets.getRight(this@measure, layoutDirection)
-            )
-            val topContentPadding = max(
-                scaffoldDirective.contentPadding.calculateTopPadding().roundToPx(),
-                windowInsets.getTop(this@measure)
-            )
-            val bottomContentPadding = max(
-                scaffoldDirective.contentPadding.calculateBottomPadding().roundToPx(),
-                windowInsets.getBottom(this@measure)
-            )
             val outerBounds = IntRect(
-                leftContentPadding,
-                topContentPadding,
-                constraints.maxWidth - rightContentPadding,
-                constraints.maxHeight - bottomContentPadding
+                0,
+                0,
+                constraints.maxWidth,
+                constraints.maxHeight
             )
 
             if (!paneExpansionState.isUnspecified()) {
                 // Pane expansion should override everything
-                val availableWidth = constraints.maxWidth - leftContentPadding - rightContentPadding
+                val availableWidth = constraints.maxWidth
                 if (paneExpansionState.firstPaneWidth == 0 ||
                     paneExpansionState.firstPanePercentage == 0f) {
                     if (visiblePanes.size > 1) {
@@ -388,10 +364,10 @@
             } else if (scaffoldDirective.excludedBounds.isNotEmpty()) {
                 val layoutBounds = coordinates!!.boundsInWindow()
                 val layoutPhysicalPartitions = mutableListOf<Rect>()
-                var actualLeft = layoutBounds.left + leftContentPadding
-                var actualRight = layoutBounds.right - rightContentPadding
-                val actualTop = layoutBounds.top + topContentPadding
-                val actualBottom = layoutBounds.bottom - bottomContentPadding
+                var actualLeft = layoutBounds.left
+                var actualRight = layoutBounds.right
+                val actualTop = layoutBounds.top
+                val actualBottom = layoutBounds.bottom
                 // Assume hinge bounds are sorted from left to right, non-overlapped.
                 @Suppress("ListIterator")
                 scaffoldDirective.excludedBounds.forEach { hingeBound ->
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
index 358be71..d665d36 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/ListDetailPaneScaffoldNavigatorTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.material3.adaptive.navigation
 
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
 import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
@@ -569,7 +568,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(0.dp),
     maxHorizontalPartitions = 1,
     horizontalPartitionSpacerSize = 0.dp,
     maxVerticalPartitions = 1,
@@ -579,7 +577,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockDualPaneScaffoldDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(16.dp),
     maxHorizontalPartitions = 2,
     horizontalPartitionSpacerSize = 16.dp,
     maxVerticalPartitions = 1,
diff --git a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
index 5092174..e73f542 100644
--- a/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
+++ b/compose/material3/adaptive/adaptive-navigation/src/androidInstrumentedTest/kotlin/androidx/compose/material3/adaptive/navigation/SupportingPaneScaffoldNavigatorTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.material3.adaptive.navigation
 
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
 import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
@@ -583,7 +582,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockSinglePaneScaffoldDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(0.dp),
     maxHorizontalPartitions = 1,
     horizontalPartitionSpacerSize = 0.dp,
     maxVerticalPartitions = 1,
@@ -593,7 +591,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 private val MockDualPaneScaffoldDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(16.dp),
     maxHorizontalPartitions = 2,
     horizontalPartitionSpacerSize = 16.dp,
     maxVerticalPartitions = 1,
diff --git a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
index acfcf48..302c605 100644
--- a/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
+++ b/compose/material3/adaptive/benchmark/src/androidTest/java/androidx/compose/material3/adaptive/benchmark/TestUtils.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
 import androidx.compose.material3.adaptive.layout.AnimatedPane
@@ -37,7 +36,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 val singlePaneDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(16.dp),
     maxHorizontalPartitions = 1,
     horizontalPartitionSpacerSize = 0.dp,
     maxVerticalPartitions = 1,
@@ -47,7 +45,6 @@
 
 @OptIn(ExperimentalMaterial3AdaptiveApi::class)
 val dualPaneDirective = PaneScaffoldDirective(
-    contentPadding = PaddingValues(24.dp),
     maxHorizontalPartitions = 2,
     horizontalPartitionSpacerSize = 24.dp,
     maxVerticalPartitions = 1,
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
index 102eb3c..2e153e1 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SliderTest.kt
@@ -40,7 +40,6 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
-import androidx.compose.testutils.expectAssertionError
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
@@ -739,14 +738,12 @@
     @OptIn(ExperimentalMaterial3Api::class)
     @Test
     fun slider_rowWithInfiniteWidth() {
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    Slider(
-                        state = SliderState(0f),
-                        modifier = Modifier.weight(1f)
-                    )
-                }
+        rule.setContent {
+            Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                Slider(
+                    state = SliderState(0f),
+                    modifier = Modifier.weight(1f)
+                )
             }
         }
     }
@@ -1393,14 +1390,12 @@
     @Test
     fun rangeSlider_rowWithInfiniteWidth() {
         val state = RangeSliderState(0f, 1f)
-        expectAssertionError(false) {
-            rule.setContent {
-                Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
-                    RangeSlider(
-                        state = state,
-                        modifier = Modifier.weight(1f)
-                    )
-                }
+        rule.setContent {
+            Row(modifier = Modifier.requiredWidth(Int.MAX_VALUE.dp)) {
+                RangeSlider(
+                    state = state,
+                    modifier = Modifier.weight(1f)
+                )
             }
         }
     }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
index 767e874..80e6eee 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/carousel/CarouselTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.TargetedFlingBehavior
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.height
@@ -131,6 +132,53 @@
     }
 
     @Test
+    fun carouselSingleAdvanceFling_capsScroll() {
+        // Arrange
+        createCarousel()
+        assertThat(carouselState.pagerState.currentPage).isEqualTo(0)
+
+        // Act
+        rule.onNodeWithTag(CarouselTestTag)
+            .performTouchInput {
+                swipeWithVelocity(
+                    centerRight,
+                    centerLeft,
+                    10000f
+                )
+            }
+
+        // Assert
+        rule.runOnIdle {
+            // A swipe from the very right to very left should be capped at
+            // the item right after the visible pages onscreen regardless of velocity
+            assertThat(carouselState.pagerState.currentPage).isLessThan(
+                carouselState.pagerState.layoutInfo.visiblePagesInfo.size + 1)
+        }
+    }
+
+    @Test
+    fun carouselMultibrowseFling_ScrollsToEnd() {
+        // Arrange
+        createCarousel(
+            flingBehavior =
+            { state: CarouselState -> CarouselDefaults.multiBrowseFlingBehavior(state) },
+        )
+        assertThat(carouselState.pagerState.currentPage).isEqualTo(0)
+
+        // Act
+        rule.onNodeWithTag(CarouselTestTag)
+            .performTouchInput { swipeWithVelocity(centerRight, centerLeft, 10000f) }
+
+        // Assert
+        rule.runOnIdle {
+            // A swipe from the very right to very left at a high velocity should go beyond
+            // first item after the visible pages as it's not capped
+            assertThat(carouselState.pagerState.currentPage).isGreaterThan(
+                carouselState.pagerState.layoutInfo.visiblePagesInfo.size)
+        }
+    }
+
+    @Test
     fun carousel_calculateOutOfBoundsPageCount() {
         val xSmallSize = 5f
         val smallSize = 100f
@@ -203,6 +251,11 @@
             .width(412.dp)
             .height(221.dp),
         orientation: Orientation = Orientation.Horizontal,
+        flingBehavior: @Composable (CarouselState) -> TargetedFlingBehavior = @Composable {
+            CarouselDefaults.singleAdvanceFlingBehavior(
+                state = it,
+            )
+        },
         content: @Composable CarouselScope.(item: Int) -> Unit = { Item(index = it) }
     ) {
         rule.setMaterialContent(lightColorScheme()) {
@@ -222,6 +275,7 @@
                         itemCount = itemCount.invoke(),
                     )
                 },
+                flingBehavior = flingBehavior(state),
                 modifier = modifier.testTag(CarouselTestTag),
                 itemSpacing = 0.dp,
                 content = content,
@@ -243,7 +297,7 @@
             }
             HorizontalUncontainedCarousel(
                 state = state,
-                itemSize = 150.dp,
+                itemWidth = 150.dp,
                 modifier = modifier.testTag(CarouselTestTag),
                 itemSpacing = 0.dp,
                 content = content,
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
index 8a90585..5ef72e1 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/TouchExplorationStateProvider.android.kt
@@ -28,9 +28,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 /**
  * It depends on the state of accessibility services to determine the current state of touch
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
index 1cd65d5..84738c7 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/MultiBrowseTest.kt
@@ -16,12 +16,14 @@
 
 package androidx.compose.material3.carousel
 
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.ui.unit.Density
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalMaterial3Api::class)
 @RunWith(JUnit4::class)
 class MultiBrowseTest {
 
@@ -56,7 +58,7 @@
             availableSpace = 100f,
             itemSpacing = 0f
         )
-        val minSmallItemSize: Float = with(Density) { StrategyDefaults.MinSmallSize.toPx() }
+        val minSmallItemSize: Float = with(Density) { CarouselDefaults.MinSmallItemSize.toPx() }
         val keylines = strategy.defaultKeylines
 
         // If the item size given is larger than the container, the adjusted keyline list from
@@ -71,7 +73,7 @@
 
     @Test
     fun testMultiBrowse_hasNoSmallItemsIfNotEnoughRoom() {
-        val minSmallItemSize: Float = with(Density) { StrategyDefaults.MinSmallSize.toPx() }
+        val minSmallItemSize: Float = with(Density) { CarouselDefaults.MinSmallItemSize.toPx() }
         val keylineList = multiBrowseKeylineList(
             density = Density,
             carouselMainAxisSize = minSmallItemSize,
@@ -104,7 +106,7 @@
 
     @Test
     fun testMultiBrowse_adjustsMediumSizeToBeProportional() {
-        val maxSmallItemSize: Float = with(Density) { StrategyDefaults.MaxSmallSize.toPx() }
+        val maxSmallItemSize: Float = with(Density) { CarouselDefaults.MaxSmallItemSize.toPx() }
         val preferredItemSize = 200f
         val carouselSize = preferredItemSize * 2 + maxSmallItemSize * 2
         val keylineList = multiBrowseKeylineList(
@@ -132,7 +134,7 @@
 
     @Test
     fun testMultiBrowse_withLessItemsThanKeylines() {
-        val maxSmallItemSize: Float = with(Density) { StrategyDefaults.MaxSmallSize.toPx() }
+        val maxSmallItemSize: Float = with(Density) { CarouselDefaults.MaxSmallItemSize.toPx() }
         val preferredItemSize = 200f
         val carouselSize = preferredItemSize * 2 + maxSmallItemSize * 2
         val keylineList = multiBrowseKeylineList(
diff --git a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
index 303982d..9f7ba239 100644
--- a/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
+++ b/compose/material3/material3/src/androidUnitTest/kotlin/androidx/compose/material3/carousel/UncontainedTest.kt
@@ -16,12 +16,14 @@
 
 package androidx.compose.material3.carousel
 
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.ui.unit.Density
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
+@OptIn(ExperimentalMaterial3Api::class)
 @RunWith(JUnit4::class)
 class UncontainedTest {
 
@@ -42,7 +44,7 @@
             itemSpacing = 0f
         )
         val keylines = strategy.defaultKeylines
-        val anchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
+        val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
 
         // A fullscreen layout should be [xSmall-large-xSmall] where the xSmall items are
         // outside the bounds of the carousel container and the large item takes up the
@@ -68,7 +70,7 @@
             itemSpacing = 0f
         )
         val keylines = strategy.defaultKeylines
-        val anchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
+        val anchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
 
         // The layout should be [xSmall-large-xSmall] where the xSmall items are
         // outside the bounds of the carousel container and the large item takes up the
@@ -97,7 +99,7 @@
             itemSpacing = 0f
         )
         val keylines = strategy.defaultKeylines
-        val rightAnchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
+        val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
 
         // The layout should be [xSmall-large-large-large-medium-xSmall] where medium is a size
         // such that a third of it is cut off.
@@ -135,7 +137,7 @@
             itemSpacing = 0f
         )
         val keylines = strategy.defaultKeylines
-        val rightAnchorSize = with(Density) { StrategyDefaults.AnchorSize.toPx() }
+        val rightAnchorSize = with(Density) { CarouselDefaults.AnchorSize.toPx() }
 
         // The layout should be [xSmall-large-large-large-medium-xSmall]
         assertThat(keylines.size).isEqualTo(6)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
index 854a7a9..a4c1394 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt
@@ -448,6 +448,8 @@
 
     internal var defaultNavigationRailItemColorsCached: NavigationRailItemColors? = null
 
+    internal var defaultExpressiveNavigationBarItemColorsCached: NavigationItemColors? = null
+
     internal var defaultRadioButtonColorsCached: RadioButtonColors? = null
 
     @OptIn(ExperimentalMaterial3Api::class)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
index 8887c16..c7b0179 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExpressiveNavigationBar.kt
@@ -119,19 +119,17 @@
  *
  * TODO: Remove internal.
  */
-@ExperimentalMaterial3Api
 internal object ExpressiveNavigationBarItemDefaults {
     /**
      * Creates a [NavigationItemColors] with the provided colors according to the Material
      * specification.
      */
     @Composable
-    fun colors() = MaterialTheme.colorScheme.expressiveNavigationBarItemColors
+    fun colors() = MaterialTheme.colorScheme.defaultExpressiveNavigationBarItemColors
 
-    // TODO: Add a cached expressiveNavigationBarItemColors.
-    internal val ColorScheme.expressiveNavigationBarItemColors: NavigationItemColors
+    internal val ColorScheme.defaultExpressiveNavigationBarItemColors: NavigationItemColors
         get() {
-            return NavigationItemColors(
+            return defaultExpressiveNavigationBarItemColorsCached ?: NavigationItemColors(
                 selectedIconColor = fromToken(ActiveIconColor),
                 selectedTextColor = fromToken(ActiveLabelTextColor),
                 selectedIndicatorColor = fromToken(ActiveIndicatorColor),
@@ -139,7 +137,9 @@
                 unselectedTextColor = fromToken(InactiveLabelTextColor),
                 disabledIconColor = fromToken(InactiveIconColor).copy(alpha = DisabledAlpha),
                 disabledTextColor = fromToken(InactiveLabelTextColor).copy(alpha = DisabledAlpha),
-            )
+            ).also {
+                defaultExpressiveNavigationBarItemColorsCached = it
+            }
         }
 }
 
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
index 15e6155..1c26998 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationItem.kt
@@ -105,7 +105,6 @@
  * TODO: Remove "internal".
  */
 @Immutable
-@ExperimentalMaterial3Api
 internal class NavigationItemColors constructor(
     val selectedIconColor: Color,
     val selectedTextColor: Color,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
index 8eb1984..1a41903 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Carousel.kt
@@ -18,12 +18,21 @@
 
 import androidx.annotation.VisibleForTesting
 import androidx.collection.IntIntMap
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.TargetedFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.PageSize
 import androidx.compose.foundation.pager.PagerDefaults
+import androidx.compose.foundation.pager.PagerSnapDistance
 import androidx.compose.foundation.pager.VerticalPager
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -65,13 +74,18 @@
  * guidelines</a>.
  *
  * @param state The state object to be used to control the carousel's state
- * @param preferredItemSize The size fully visible items would like to be in the main axis. This
- * size is a target and will likely be adjusted by carousel in order to fit a whole number of
+ * @param preferredItemWidth The width the fully visible items would like to be in the main axis.
+ * This width is a target and will likely be adjusted by carousel in order to fit a whole number of
  * items within the container
  * @param modifier A modifier instance to be applied to this carousel container
  * @param itemSpacing The amount of space used to separate items in the carousel
- * @param minSmallSize The minimum allowable size of small masked items
- * @param maxSmallSize The maximum allowable size of small masked items
+ * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
+ * @param minSmallItemWidth The minimum allowable width of small items in dp. Depending on the
+ * [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
+ * range of [minSmallItemWidth] and [maxSmallItemWidth]
+ * @param maxSmallItemWidth The maximum allowable width of small items in dp. Depending on the
+ * [preferredItemWidth] and the width of the carousel, the small item width will be chosen from a
+ * range of [minSmallItemWidth] and [maxSmallItemWidth]
  * @param content The carousel's content Composable
  *
  * TODO: Add sample link
@@ -80,11 +94,13 @@
 @Composable
 internal fun HorizontalMultiBrowseCarousel(
     state: CarouselState,
-    preferredItemSize: Dp,
+    preferredItemWidth: Dp,
     modifier: Modifier = Modifier,
     itemSpacing: Dp = 0.dp,
-    minSmallSize: Dp = StrategyDefaults.MinSmallSize,
-    maxSmallSize: Dp = StrategyDefaults.MaxSmallSize,
+    flingBehavior: TargetedFlingBehavior =
+        CarouselDefaults.singleAdvanceFlingBehavior(state = state),
+    minSmallItemWidth: Dp = CarouselDefaults.MinSmallItemSize,
+    maxSmallItemWidth: Dp = CarouselDefaults.MaxSmallItemSize,
     content: @Composable CarouselScope.(itemIndex: Int) -> Unit
 ) {
     val density = LocalDensity.current
@@ -96,16 +112,17 @@
                 multiBrowseKeylineList(
                     density = this,
                     carouselMainAxisSize = availableSpace,
-                    preferredItemSize = preferredItemSize.toPx(),
+                    preferredItemSize = preferredItemWidth.toPx(),
                     itemCount = state.itemCountState.value.invoke(),
                     itemSpacing = itemSpacingPx,
-                    minSmallSize = minSmallSize.toPx(),
-                    maxSmallSize = maxSmallSize.toPx(),
+                    minSmallItemSize = minSmallItemWidth.toPx(),
+                    maxSmallItemSize = maxSmallItemWidth.toPx(),
                 )
             }
         },
         modifier = modifier,
         itemSpacing = itemSpacing,
+        flingBehavior = flingBehavior,
         content = content
     )
 }
@@ -124,9 +141,10 @@
  * guidelines</a>.
  *
  * @param state The state object to be used to control the carousel's state
- * @param itemSize The size of items in the carousel
+ * @param itemWidth The width of items in the carousel
  * @param modifier A modifier instance to be applied to this carousel container
  * @param itemSpacing The amount of space used to separate items in the carousel
+ * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
  * @param content The carousel's content Composable
  *
  * TODO: Add sample link
@@ -135,9 +153,10 @@
 @Composable
 internal fun HorizontalUncontainedCarousel(
     state: CarouselState,
-    itemSize: Dp,
+    itemWidth: Dp,
     modifier: Modifier = Modifier,
     itemSpacing: Dp = 0.dp,
+    flingBehavior: TargetedFlingBehavior = CarouselDefaults.noSnapFlingBehavior(),
     content: @Composable CarouselScope.(itemIndex: Int) -> Unit
 ) {
     val density = LocalDensity.current
@@ -149,14 +168,14 @@
                 uncontainedKeylineList(
                     density = this,
                     carouselMainAxisSize = availableSpace,
-                    itemSize = itemSize.toPx(),
+                    itemSize = itemWidth.toPx(),
                     itemSpacing = itemSpacingPx,
                 )
             }
         },
         modifier = modifier,
         itemSpacing = itemSpacing,
-        flingBehavior = rememberDecaySnapFlingBehavior(),
+        flingBehavior = flingBehavior,
         content = content
     )
 }
@@ -168,11 +187,12 @@
  * chosen strategy.
  *
  * @param state The state object to be used to control the carousel's state.
- * @param modifier A modifier instance to be applied to this carousel outer layout
+ * @param orientation The layout orientation of the carousel
  * @param keylineList The list of keylines that are fixed positions along the scrolling axis which
  * define the state an item should be in when its center is co-located with the keyline's position.
+ * @param modifier A modifier instance to be applied to this carousel outer layout
  * @param itemSpacing The amount of space used to separate items in the carousel
- * @param orientation The layout orientation of the carousel
+ * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures
  * @param content The carousel's content Composable where each call is passed the index, from the
  * total item count, of the item being composed
  * TODO: Add sample link
@@ -185,7 +205,8 @@
     keylineList: (availableSpace: Float, itemSpacing: Float) -> KeylineList?,
     modifier: Modifier = Modifier,
     itemSpacing: Dp = 0.dp,
-    flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state.pagerState),
+    flingBehavior: TargetedFlingBehavior =
+        CarouselDefaults.singleAdvanceFlingBehavior(state = state),
     content: @Composable CarouselScope.(itemIndex: Int) -> Unit
 ) {
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
@@ -477,3 +498,110 @@
     val total = after.unadjustedOffset - before.unadjustedOffset
     return (unadjustedOffset - before.unadjustedOffset) / total
 }
+
+/**
+ * Contains the default values used by [Carousel].
+ */
+@ExperimentalMaterial3Api
+internal object CarouselDefaults {
+    /** The minimum size that a carousel strategy can choose its small items to be. **/
+    val MinSmallItemSize = 40.dp
+
+    /** The maximum size that a carousel strategy can choose its small items to be. **/
+    val MaxSmallItemSize = 56.dp
+
+    /**
+     * A [TargetedFlingBehavior] that limits a fling to one item at a time. [snapAnimationSpec] can
+     * be used to control the snap animation.
+     *
+     * @param state The [CarouselState] that controls which Carousel this TargetedFlingBehavior will
+     * be applied to.
+     * @param snapAnimationSpec The animation spec used to finally snap to the position.
+     * @return An instance of [TargetedFlingBehavior] that performs snapping to the next item.
+     * The animation will be governed by the post scroll velocity and the Carousel will use
+     * [snapAnimationSpec] to approach the snapped position
+     */
+    @Composable
+    fun singleAdvanceFlingBehavior(
+        state: CarouselState,
+        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
+    ): TargetedFlingBehavior {
+        return PagerDefaults.flingBehavior(
+            state = state.pagerState,
+            pagerSnapDistance = PagerSnapDistance.atMost(1),
+            snapAnimationSpec = snapAnimationSpec,
+        )
+    }
+
+    /**
+     * A [TargetedFlingBehavior] that flings and snaps according to the gesture velocity.
+     * [snapAnimationSpec] and [decayAnimationSpec] can be used to control the animation specs.
+     *
+     * The Carousel may use [decayAnimationSpec] or [snapAnimationSpec] to approach the target item
+     * post-scroll, depending on the gesture velocity.
+     * If the gesture has a high enough velocity to approach the target item, the Carousel will use
+     * [decayAnimationSpec] followed by [snapAnimationSpec] for the final step of the animation.
+     * If the gesture doesn't have enough velocity, it will use [snapAnimationSpec] +
+     * [snapAnimationSpec] in a similar fashion.
+     *
+     * @param state The [CarouselState] that controls which Carousel this TargetedFlingBehavior will
+     * be applied to.
+     * @param decayAnimationSpec The animation spec used to approach the target offset when the
+     * the fling velocity is large enough to naturally decay.
+     * @param snapAnimationSpec The animation spec used to finally snap to the position.
+     * @return An instance of [TargetedFlingBehavior] that performs flinging based on the gesture
+     * velocity and then snapping to the closest item post-fling.
+     * The animation will be governed by the post scroll velocity and the Carousel will use
+     * [snapAnimationSpec] to approach the snapped position
+     */
+    @Composable
+    fun multiBrowseFlingBehavior(
+        state: CarouselState,
+        decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
+    ): TargetedFlingBehavior {
+        val pagerSnapDistance = object : PagerSnapDistance {
+            override fun calculateTargetPage(
+                startPage: Int,
+                suggestedTargetPage: Int,
+                velocity: Float,
+                pageSize: Int,
+                pageSpacing: Int
+            ): Int {
+                return suggestedTargetPage
+            }
+        }
+        return PagerDefaults.flingBehavior(
+            state = state.pagerState,
+            pagerSnapDistance = pagerSnapDistance,
+            decayAnimationSpec = decayAnimationSpec,
+            snapAnimationSpec = snapAnimationSpec,
+        )
+    }
+
+    /**
+     * A [TargetedFlingBehavior] that flings according to the gesture velocity and does not snap
+     * post-fling.
+     *
+     * @return An instance of [TargetedFlingBehavior] that performs flinging based on the gesture
+     * velocity and does not snap to anything post-fling.
+     */
+    @Composable
+    fun noSnapFlingBehavior(): TargetedFlingBehavior {
+        val splineDecay = rememberSplineBasedDecay<Float>()
+        val decayLayoutInfoProvider = remember {
+            object : SnapLayoutInfoProvider {
+                override fun calculateApproachOffset(initialVelocity: Float): Float {
+                    return splineDecay.calculateTargetValue(0f, initialVelocity)
+                }
+
+                override fun calculateSnappingOffset(currentVelocity: Float): Float = 0f
+            }
+        }
+
+        return rememberSnapFlingBehavior(snapLayoutInfoProvider = decayLayoutInfoProvider)
+    }
+
+    internal val AnchorSize = 10.dp
+    internal const val MediumLargeItemDiffThreshold = 0.85f
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
index 7dfc4b9..c8a7af0 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/KeylineSnapPosition.kt
@@ -19,15 +19,7 @@
 import androidx.collection.IntIntMap
 import androidx.collection.emptyIntIntMap
 import androidx.collection.mutableIntIntMapOf
-import androidx.compose.animation.core.calculateTargetValue
-import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.gestures.TargetedFlingBehavior
-import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
 import androidx.compose.foundation.gestures.snapping.SnapPosition
-import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.roundToInt
@@ -85,20 +77,3 @@
             return if (snapPositions.size > 0) snapPositions[itemIndex] else 0
         }
     }
-
-@ExperimentalMaterial3Api
-@Composable
-internal fun rememberDecaySnapFlingBehavior(): TargetedFlingBehavior {
-    val splineDecay = rememberSplineBasedDecay<Float>()
-    val decayLayoutInfoProvider = remember {
-        object : SnapLayoutInfoProvider {
-            override fun calculateApproachOffset(initialVelocity: Float): Float {
-                return splineDecay.calculateTargetValue(0f, initialVelocity)
-            }
-
-            override fun calculateSnappingOffset(currentVelocity: Float): Float = 0f
-        }
-    }
-
-    return rememberSnapFlingBehavior(snapLayoutInfoProvider = decayLayoutInfoProvider)
-}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
index d7f04c8..9c654b2 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Keylines.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.material3.carousel
 
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.ui.unit.Density
 import kotlin.math.ceil
 import kotlin.math.floor
@@ -39,17 +40,18 @@
  * @param preferredItemSize the desired size of large items, in pixels, in the main scrolling axis
  * @param itemSpacing the spacing between items in pixels
  * @param itemCount the number of items in the carousel
- * @param minSmallSize the minimum allowable size of small items in pixels
- * @param maxSmallSize the maximum allowable size of small items in pixels
+ * @param minSmallItemSize the minimum allowable size of small items in pixels
+ * @param maxSmallItemSize the maximum allowable size of small items in pixels
  */
+@OptIn(ExperimentalMaterial3Api::class)
 internal fun multiBrowseKeylineList(
     density: Density,
     carouselMainAxisSize: Float,
     preferredItemSize: Float,
     itemSpacing: Float,
     itemCount: Int,
-    minSmallSize: Float = with(density) { StrategyDefaults.MinSmallSize.toPx() },
-    maxSmallSize: Float = with(density) { StrategyDefaults.MaxSmallSize.toPx() },
+    minSmallItemSize: Float = with(density) { CarouselDefaults.MinSmallItemSize.toPx() },
+    maxSmallItemSize: Float = with(density) { CarouselDefaults.MaxSmallItemSize.toPx() },
 ): KeylineList? {
     if (carouselMainAxisSize == 0f || preferredItemSize == 0f) {
         return null
@@ -63,10 +65,13 @@
     // of the large item and medium items are sized between large and small items. Clamp the
     // small target size within our min-max range and as close to 1/3 of the target large item
     // size as possible.
-    val targetSmallSize: Float = (targetLargeSize / 3f).coerceIn(minSmallSize, maxSmallSize)
+    val targetSmallSize: Float = (targetLargeSize / 3f + itemSpacing).coerceIn(
+        minSmallItemSize + itemSpacing,
+        maxSmallItemSize + itemSpacing
+    )
     val targetMediumSize = (targetLargeSize + targetSmallSize) / 2f
 
-    if (carouselMainAxisSize < minSmallSize * 2) {
+    if (carouselMainAxisSize < minSmallItemSize * 2) {
         // If the available space is too small to fit a large item and small item (where a large
         // item is bigger than a small item), allow arrangements with
         // no small items.
@@ -76,20 +81,20 @@
     // Find the minimum space left for large items after filling the carousel with the most
     // permissible medium and small items to determine a plausible minimum large count.
     val minAvailableLargeSpace = carouselMainAxisSize - targetMediumSize * mediumCounts.max() -
-        maxSmallSize * smallCounts.max()
+        maxSmallItemSize * smallCounts.max()
     val minLargeCount = max(
         1,
         floor(minAvailableLargeSpace / targetLargeSize).toInt())
     val maxLargeCount = ceil(carouselMainAxisSize / targetLargeSize).toInt()
 
     val largeCounts = IntArray(maxLargeCount - minLargeCount + 1) { maxLargeCount - it }
-    val anchorSize = with(density) { StrategyDefaults.AnchorSize.toPx() }
+    val anchorSize = with(density) { CarouselDefaults.AnchorSize.toPx() }
     var arrangement = Arrangement.findLowestCostArrangement(
         availableSpace = carouselMainAxisSize,
         itemSpacing = itemSpacing,
         targetSmallSize = targetSmallSize,
-        minSmallSize = minSmallSize,
-        maxSmallSize = maxSmallSize,
+        minSmallSize = minSmallItemSize,
+        maxSmallSize = maxSmallItemSize,
         smallCounts = smallCounts,
         targetMediumSize = targetMediumSize,
         mediumCounts = mediumCounts,
@@ -117,8 +122,8 @@
             availableSpace = carouselMainAxisSize,
             itemSpacing = itemSpacing,
             targetSmallSize = targetSmallSize,
-            minSmallSize = minSmallSize,
-            maxSmallSize = maxSmallSize,
+            minSmallSize = minSmallItemSize,
+            maxSmallSize = maxSmallItemSize,
             smallCounts = intArrayOf(smallCount),
             targetMediumSize = targetMediumSize,
             mediumCounts = intArrayOf(mediumCount),
@@ -172,6 +177,7 @@
  * @param itemSize the size of large items, in pixels, in the main scrolling axis
  * @param itemSpacing the spacing between items in pixels
  */
+@OptIn(ExperimentalMaterial3Api::class)
 internal fun uncontainedKeylineList(
     density: Density,
     carouselMainAxisSize: Float,
@@ -189,7 +195,7 @@
 
     val mediumCount = if (remainingSpace > 0) 1 else 0
 
-    val defaultAnchorSize = with(density) { StrategyDefaults.AnchorSize.toPx() }
+    val defaultAnchorSize = with(density) { CarouselDefaults.AnchorSize.toPx() }
     val mediumItemSize = calculateMediumChildSize(
         minimumMediumSize = defaultAnchorSize,
         largeItemSize = largeItemSize,
@@ -222,6 +228,7 @@
  * size, and arbitrarily chooses a size small enough such that there is a size disparity between
  * the medium and large sizes, but large enough to have a sufficient percentage cut off.
  */
+@OptIn(ExperimentalMaterial3Api::class)
 private fun calculateMediumChildSize(
     minimumMediumSize: Float,
     largeItemSize: Float,
@@ -238,7 +245,7 @@
     // it's too similar and won't create sufficient motion when scrolling items between the large
     // items and the medium item.
     val largeItemThreshold: Float =
-        largeItemSize * StrategyDefaults.MediumLargeItemDiffThreshold
+        largeItemSize * CarouselDefaults.MediumLargeItemDiffThreshold
     if (mediumItemSize > largeItemThreshold) {
         // Choose whichever is bigger between the maximum threshold of the medium child size, or
         // a size such that only 20% of the space is cut off.
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
index b7b5d53..5641be1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/carousel/Strategy.kt
@@ -22,23 +22,12 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.lerp
 import kotlin.math.max
 import kotlin.math.roundToInt
 
 /**
- * Contains default values used across Strategies
- */
-internal object StrategyDefaults {
-    val MinSmallSize = 40.dp
-    val MaxSmallSize = 56.dp
-    val AnchorSize = 10.dp
-    const val MediumLargeItemDiffThreshold = 0.85f
-}
-
-/**
  * A class responsible for supplying carousel with a [KeylineList] that is corrected for scroll
  * offset, layout direction, and snapping behaviors.
  *
diff --git a/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt b/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
index 9733e52..756ccab 100644
--- a/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
+++ b/compose/runtime/runtime-livedata/src/androidTest/java/androidx/compose/runtime/livedata/LiveDataAdapterTest.kt
@@ -20,10 +20,10 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
index 8c2e559..b2f3744 100644
--- a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/Expect.kt
@@ -26,7 +26,7 @@
  * [expectedMessage] is a regex with just the option [DOT_MATCHES_ALL] enabled.
  */
 fun expectAssertionError(
-    expectError: Boolean,
+    expectError: Boolean = true,
     expectedMessage: String = ".*",
     block: () -> Unit
 ) {
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
index d2658da..bf70701 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/GraphicsLayerV23.android.kt
@@ -193,7 +193,7 @@
             renderNode.setTranslationX(value)
         }
 
-    override var translationY: Float = 1f
+    override var translationY: Float = 0f
         set(value) {
             field = value
             renderNode.setTranslationY(value)
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathSegment.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathSegment.kt
index 31ce2ba..2dc028f 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathSegment.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/PathSegment.kt
@@ -21,7 +21,7 @@
  * a fully formed [Path] object.
  *
  * A segment is identified by a [type][PathSegment.Type] which in turns defines how many
- * [points] are available (from 0 to 3 points, each point is represented by 2 floats) and
+ * [points] are available (from 0 to 4 points, each point is represented by 2 floats) and
  * whether the [weight] is meaningful. Please refer to the documentation of each
  * [type][PathSegment.Type] for more information.
  *
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
index 4b564b4..b18f876 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AssertExistsTest.kt
@@ -66,7 +66,7 @@
         rule.onNodeWithText("Hello")
             .assertExists()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertDoesNotExist()
         }
@@ -83,12 +83,12 @@
         cachedResult
             .assertDoesNotExist()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertExists()
         }
 
-        expectAssertionError(true) {
+        expectAssertionError {
             cachedResult.assertExists()
         }
 
@@ -99,7 +99,7 @@
         rule.onNodeWithText("Hello")
             .assertExists()
 
-        expectAssertionError(true) {
+        expectAssertionError {
             rule.onNodeWithText("Hello")
                 .assertDoesNotExist()
         }
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index 0cad628..af80153 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -117,6 +117,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
   }
 
+  public final class Html_androidKt {
+    method public static androidx.compose.ui.text.AnnotatedString parseAsHtml(String);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index 8fd548b..675ad44 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -117,6 +117,10 @@
   @SuppressCompatibility @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
   }
 
+  public final class Html_androidKt {
+    method public static androidx.compose.ui.text.AnnotatedString parseAsHtml(String);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.kt
new file mode 100644
index 0000000..c068272
--- /dev/null
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringFromHtmlSamples.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 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.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.parseAsHtml
+
+@Composable
+@Sampled
+fun AnnotatedStringFromHtml() {
+    // First, download a string as a plain text using one of the resources' methods. At this stage
+    // you will be handling plurals and formatted strings in needed. Moreover, the string will be
+    // resolved with respect to the current locale and available translations.
+    val string = stringResource(id = R.string.example)
+
+    // Next, convert a string marked with HTML tags into AnnotatedString to be displayed by Text
+    val styledAnnotatedString = string.parseAsHtml()
+
+    BasicText(styledAnnotatedString)
+}
diff --git a/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml b/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml
new file mode 100644
index 0000000..781b3fb
--- /dev/null
+++ b/compose/ui/ui-text/samples/src/main/res/values/styled-string-for-sample.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 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.
+  -->
+
+<resources>
+    <string name="example" translatable="false">
+        &lt;b>bold&lt;/b>
+        &lt;i>italic&lt;/i>
+        &lt;big>big&lt;/big>
+        &lt;small>small&lt;/small>
+        &lt;tt>monospace&lt;/tt>
+        &lt;font face="serif">serif&lt;/font>
+        &lt;font face="serif-monospace">serif-monospace&lt;/font>
+        &lt;font face="sans_serif">sans_serif&lt;/font>
+        &lt;font face="cursive">cursive&lt;/font>
+        &lt;font face="casual">casual&lt;/font>
+        &lt;font face="sans-serif-smallcaps">sans-serif-smallcaps&lt;/font>
+        &lt;font face="sans-serif-condensed-light">sans-serif-condensed-light&lt;/font>
+        &lt;font face="sans-serif-condensed">sans-serif-condensed&lt;/font>
+        &lt;font face="sans-serif-condensed-medium">sans-serif-condensed-medium&lt;/font>
+        &lt;font color="#00ff00">green&lt;/font>
+        &lt;sup>superscript&lt;/sup>
+        &lt;strike>strikethrough&lt;/strike>
+        &lt;sub>subscript&lt;/sub>
+        &lt;u>underline&lt;/u>
+        &lt;span style="background-color:#ff0000">span&lt;/span>
+        &lt;p dir="rtl">right to left&lt;/p>
+        &lt;p dir="ltr">left to right&lt;/p>
+        I am &lt;div>div&lt;/div> element.&lt;br>
+        &lt;a href="https://developer.android.com">Link&lt;/a>
+    </string>
+</resources>
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml b/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml
new file mode 100644
index 0000000..934bf4b
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+        <activity android:name="androidx.activity.ComponentActivity" />
+    </application>
+</manifest>
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt
new file mode 100644
index 0000000..8762ee1
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/AnnotatedStringFromHtmlTest.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2024 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
+
+import android.graphics.Typeface
+import android.text.Layout
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.AlignmentSpan
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.TypefaceSpan
+import android.text.style.URLSpan
+import android.text.style.UnderlineSpan
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.em
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AnnotatedStringFromHtmlTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    // pre-N block-level elements were separated with two new lines
+    @SdkSuppress(minSdkVersion = 24)
+    fun buildAnnotatedString_fromHtml() {
+        rule.setContent {
+            val expected = buildAnnotatedString {
+                fun add(block: () -> Unit) {
+                    block()
+                    append("a")
+                    pop()
+                    append(" ")
+                }
+                fun addStyle(style: SpanStyle) {
+                    add { pushStyle(style) }
+                }
+
+                add { pushLink(LinkAnnotation.Url("https://example.com")) }
+                add { pushStringAnnotation("foo", "Bar") }
+                addStyle(SpanStyle(fontWeight = FontWeight.Bold))
+                addStyle(SpanStyle(fontSize = 1.25.em))
+                append("\na\n") // <div>
+                addStyle(SpanStyle(fontFamily = FontFamily.Serif))
+                addStyle(SpanStyle(color = Color.Green))
+                addStyle(SpanStyle(fontStyle = FontStyle.Italic))
+                append("\na\n") // <p>
+                addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough))
+                addStyle(SpanStyle(fontSize = 0.8.em))
+                addStyle(SpanStyle(background = Color.Red))
+                addStyle(SpanStyle(baselineShift = BaselineShift.Subscript))
+                addStyle(SpanStyle(baselineShift = BaselineShift.Superscript))
+                addStyle(SpanStyle(fontFamily = FontFamily.Monospace))
+                addStyle(SpanStyle(textDecoration = TextDecoration.Underline))
+            }
+
+            val actual = stringResource(androidx.compose.ui.text.test.R.string.html).parseAsHtml()
+
+            assertThat(actual.text).isEqualTo(expected.text)
+            assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles).inOrder()
+            assertThat(actual.paragraphStyles)
+                .containsExactlyElementsIn(expected.paragraphStyles)
+                .inOrder()
+            assertThat(actual.getStringAnnotations(0, actual.length))
+                .containsExactlyElementsIn(expected.getStringAnnotations(0, expected.length))
+                .inOrder()
+            assertThat(actual.getLinkAnnotations(0, actual.length))
+                .containsExactlyElementsIn(expected.getLinkAnnotations(0, expected.length))
+                .inOrder()
+        }
+    }
+
+    @Test
+    fun formattedString_withStyling() {
+        rule.setContent {
+            val actual = stringResource(
+                androidx.compose.ui.text.test.R.string.formatting,
+                "computer"
+            ).parseAsHtml()
+            assertThat(actual.text).isEqualTo("Hello, computer!")
+            assertThat(actual.spanStyles).containsExactly(
+                AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 7, 15)
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_withNoText_noStringAnnotation() {
+        rule.setContent {
+            val actual = "a<annotation key1=value1></annotation>".parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("a")
+            assertThat(actual.getStringAnnotations(0, actual.length)).isEmpty()
+        }
+    }
+
+    @Test
+    fun annotationTag_withNoAttributes_noStringAnnotation() {
+        rule.setContent {
+            val actual = "<annotation>a</annotation>".parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("a")
+            assertThat(actual.getStringAnnotations(0, actual.length)).isEmpty()
+        }
+    }
+
+    @Test
+    fun annotationTag_withOneAttribute_oneStringAnnotation() {
+        rule.setContent {
+            val actual = "<annotation key1=value1>a</annotation>".parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("a")
+            assertThat(actual.getStringAnnotations(0, actual.length)).containsExactly(
+                AnnotatedString.Range("value1", 0, 1, "key1")
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_withMultipleAttributes_multipleStringAnnotations() {
+        rule.setContent {
+            val actual = """
+                <annotation key1="value1" key2=value2 keyThree="valueThree">a</annotation>
+            """.trimIndent().parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("a")
+            assertThat(actual.getStringAnnotations(0, actual.length)).containsExactly(
+                AnnotatedString.Range("value1", 0, 1, "key1"),
+                AnnotatedString.Range("value2", 0, 1, "key2"),
+                AnnotatedString.Range("valueThree", 0, 1, "keythree")
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_withMultipleAnnotations_multipleStringAnnotations() {
+        rule.setContent {
+            val actual = """
+                <annotation key1=val1>a</annotation>a<annotation key2="val2">a</annotation>
+                """.trimIndent().parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("aaa")
+            assertThat(actual.getStringAnnotations(0, actual.length)).containsExactly(
+                AnnotatedString.Range("val1", 0, 1, "key1"),
+                AnnotatedString.Range("val2", 2, 3, "key2")
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_withOtherTag() {
+        rule.setContent {
+            val actual = "<annotation key1=\"value1\">a</annotation><b>a</b>".parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("aa")
+            assertThat(actual.spanStyles).containsExactly(
+                AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 1, 2),
+            )
+            assertThat(actual.getStringAnnotations(0, actual.length)).containsExactly(
+                AnnotatedString.Range("value1", 0, 1, "key1")
+            )
+        }
+    }
+
+    @Test
+    fun annotationTag_wrappedByOtherTag() {
+        rule.setContent {
+            val actual = "<b><annotation key1=\"value1\">a</annotation></b>".parseAsHtml()
+
+            assertThat(actual.text).isEqualTo("a")
+            assertThat(actual.spanStyles).containsExactly(
+                AnnotatedString.Range(SpanStyle(fontWeight = FontWeight.Bold), 0, 1)
+            )
+            assertThat(actual.getStringAnnotations(0, actual.length)).containsExactly(
+                AnnotatedString.Range("value1", 0, 1, "key1")
+            )
+        }
+    }
+
+    fun verify_alignmentSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(ParagraphStyle(textAlign = TextAlign.Center)) { append("a") }
+        }
+        val actual = buildSpannableString(
+            AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER)
+        ).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.paragraphStyles).containsExactlyElementsIn(
+            expected.paragraphStyles
+        )
+    }
+
+    fun verify_backgroundColorSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(background = Color.Red)) { append("a") }
+        }
+        val actual =
+            buildSpannableString(BackgroundColorSpan(Color.Red.toArgb())).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_foregroundColorSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(color = Color.Blue)) { append("a") }
+        }
+        val actual =
+            buildSpannableString(ForegroundColorSpan(Color.Blue.toArgb())).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_relativeSizeSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(fontSize = 0.6f.em)) { append("a") }
+        }
+        val actual = buildSpannableString(RelativeSizeSpan(0.6f)).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_strikeThroughSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) { append("a") }
+        }
+        val actual = buildSpannableString(StrikethroughSpan()).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_styleSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)) {
+                append("a")
+            }
+        }
+        val actual = buildSpannableString(StyleSpan(Typeface.BOLD_ITALIC)).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    fun verify_subscriptSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(baselineShift = BaselineShift.Subscript)) { append("a") }
+        }
+        val actual = buildSpannableString(SubscriptSpan()).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_superScriptSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) { append("a") }
+        }
+        val actual = buildSpannableString(SuperscriptSpan()).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_typefaceSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(fontFamily = FontFamily.Monospace)) { append("a") }
+        }
+        val actual = buildSpannableString(TypefaceSpan("monospace")).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    @Test
+    fun verify_underlineSpan() {
+        val expected = buildAnnotatedString {
+            withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { append("a") }
+        }
+        val actual = buildSpannableString(UnderlineSpan()).toAnnotatedString()
+
+        assertThat(actual.text).isEqualTo(expected.text)
+        assertThat(actual.spanStyles).containsExactlyElementsIn(expected.spanStyles)
+    }
+
+    fun verify_urlSpan() {
+        val spannable = SpannableStringBuilder()
+        spannable.append("a", URLSpan("url"), Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+
+        val expected = buildAnnotatedString {
+            withAnnotation(LinkAnnotation.Url("url")) { append("a") }
+        }
+        assertThat(spannable.toAnnotatedString().text).isEqualTo(expected.text)
+        assertThat(spannable.toAnnotatedString().getLinkAnnotations(0, 1))
+            .containsExactlyElementsIn(expected.getLinkAnnotations(0, 1))
+    }
+
+    private fun buildSpannableString(span: Any) = SpannableStringBuilder().also {
+        it.append("a", span, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
+    }
+}
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml b/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml
new file mode 100644
index 0000000..c20f742
--- /dev/null
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/res/values/styled-string-for-test.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2024 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.
+  -->
+
+<resources>
+    <string name="html" translatable="false">
+        &lt;a href="https://example.com">a&lt;/a>
+        &lt;annotation Foo=Bar>a&lt;/annotation>
+        &lt;b>a&lt;/b>
+        &lt;big>a&lt;/big>
+        &lt;div>a&lt;/div>
+        &lt;font face="serif">a&lt;/font>
+        &lt;font color="#00ff00">a&lt;/font>
+        &lt;i>a&lt;/i>
+        &lt;p>a&lt;/p>
+        &lt;s>a&lt;/s>
+        &lt;small>a&lt;/small>
+        &lt;span style="background-color:red">a&lt;/span>
+        &lt;sub>a&lt;/sub>
+        &lt;sup>a&lt;/sup>
+        &lt;tt>a&lt;/tt>
+        &lt;u>a&lt;/u>
+    </string>
+    <string name="formatting">Hello, &lt;b>%s&lt;/b>!</string>
+</resources>
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 68fbac8..5b73220 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
@@ -402,7 +402,8 @@
         )
     }
 
-    private val wordIterator: WordIterator = layout.wordIterator
+    private val wordIterator: WordIterator
+        get() = layout.wordIterator
 
     override fun getWordBoundary(offset: Int): TextRange {
         val wordIterator = layout.wordIterator
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
new file mode 100644
index 0000000..41b3baa
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/Html.android.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2024 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
+
+import android.graphics.Typeface
+import android.text.Editable
+import android.text.Html.TagHandler
+import android.text.Layout
+import android.text.Spanned
+import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+import android.text.Spanned.SPAN_MARK_MARK
+import android.text.style.AbsoluteSizeSpan
+import android.text.style.AlignmentSpan
+import android.text.style.BackgroundColorSpan
+import android.text.style.ForegroundColorSpan
+import android.text.style.RelativeSizeSpan
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.SubscriptSpan
+import android.text.style.SuperscriptSpan
+import android.text.style.TypefaceSpan
+import android.text.style.URLSpan
+import android.text.style.UnderlineSpan
+import androidx.annotation.VisibleForTesting
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.util.fastForEach
+import androidx.core.text.HtmlCompat
+import org.xml.sax.Attributes
+import org.xml.sax.ContentHandler
+import org.xml.sax.XMLReader
+
+actual fun String.parseAsHtml(): AnnotatedString {
+    // Check ContentHandlerReplacementTag kdoc for more details
+    val stringToParse = "<$ContentHandlerReplacementTag />$this"
+    val spanned = HtmlCompat.fromHtml(
+        stringToParse,
+        HtmlCompat.FROM_HTML_MODE_COMPACT,
+        null,
+        TagHandler
+    )
+    return spanned.toAnnotatedString()
+}
+
+@VisibleForTesting
+internal fun Spanned.toAnnotatedString(): AnnotatedString {
+    return AnnotatedString.Builder(capacity = length)
+        .append(this)
+        .also { it.addSpans(this) }
+        .toAnnotatedString()
+}
+
+private fun AnnotatedString.Builder.addSpans(spanned: Spanned) {
+    spanned.getSpans(0, length, Any::class.java).forEach { span ->
+        val range = TextRange(spanned.getSpanStart(span), spanned.getSpanEnd(span))
+        addSpan(span, range.start, range.end)
+    }
+}
+
+private fun AnnotatedString.Builder.addSpan(span: Any, start: Int, end: Int) {
+    when (span) {
+        is AbsoluteSizeSpan -> {
+            // TODO(soboleva) need density object or make dip/px new units in TextUnit
+        }
+        is AlignmentSpan -> {
+            addStyle(span.toParagraphStyle(), start, end)
+        }
+        is AnnotationSpan -> {
+            addStringAnnotation(span.key, span.value, start, end)
+        }
+        is BackgroundColorSpan -> {
+            addStyle(SpanStyle(background = Color(span.backgroundColor)), start, end)
+        }
+        is ForegroundColorSpan -> {
+            addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end)
+        }
+        is RelativeSizeSpan -> {
+            addStyle(SpanStyle(fontSize = span.sizeChange.em), start, end)
+        }
+        is StrikethroughSpan -> {
+            addStyle(SpanStyle(textDecoration = TextDecoration.LineThrough), start, end)
+        }
+        is StyleSpan -> {
+            span.toSpanStyle()?.let { addStyle(it, start, end) }
+        }
+        is SubscriptSpan -> {
+            addStyle(SpanStyle(baselineShift = BaselineShift.Subscript), start, end)
+        }
+        is SuperscriptSpan -> {
+            addStyle(SpanStyle(baselineShift = BaselineShift.Superscript), start, end)
+        }
+        is TypefaceSpan -> {
+            addStyle(span.toSpanStyle(), start, end)
+        }
+        is UnderlineSpan -> {
+            addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end)
+        }
+        is URLSpan -> {
+            span.url?.let {
+                addLink(LinkAnnotation.Url(it), start, end)
+            }
+        }
+    }
+}
+
+private fun AlignmentSpan.toParagraphStyle(): ParagraphStyle {
+    val alignment = when (this.alignment) {
+        Layout.Alignment.ALIGN_NORMAL -> TextAlign.Start
+        Layout.Alignment.ALIGN_CENTER -> TextAlign.Center
+        Layout.Alignment.ALIGN_OPPOSITE -> TextAlign.End
+        else -> TextAlign.Unspecified
+    }
+    return ParagraphStyle(textAlign = alignment)
+}
+
+private fun StyleSpan.toSpanStyle(): SpanStyle? {
+    /** StyleSpan doc: styles are cumulative -- if both bold and italic are set in
+     * separate spans, or if the base style is bold and a span calls for italic,
+     * you get bold italic.  You can't turn off a style from the base style.
+     */
+    return when (style) {
+        Typeface.BOLD -> {
+            SpanStyle(fontWeight = FontWeight.Bold)
+        }
+        Typeface.ITALIC -> {
+            SpanStyle(fontStyle = FontStyle.Italic)
+        }
+        Typeface.BOLD_ITALIC -> {
+            SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic)
+        }
+        else -> null
+    }
+}
+
+private fun TypefaceSpan.toSpanStyle(): SpanStyle {
+    val fontFamily = when (family) {
+        FontFamily.Cursive.name -> FontFamily.Cursive
+        FontFamily.Monospace.name -> FontFamily.Monospace
+        FontFamily.SansSerif.name -> FontFamily.SansSerif
+        FontFamily.Serif.name -> FontFamily.Serif
+        else -> { optionalFontFamilyFromName(family) }
+    }
+    return SpanStyle(fontFamily = fontFamily)
+}
+
+/**
+ * Mirrors [androidx.compose.ui.text.font.PlatformTypefaces.optionalOnDeviceFontFamilyByName]
+ * behavior with both font weight and font style being Normal in this case */
+private fun optionalFontFamilyFromName(familyName: String?): FontFamily? {
+    if (familyName.isNullOrEmpty()) return null
+    val typeface = Typeface.create(familyName, Typeface.NORMAL)
+    return typeface.takeIf { typeface != Typeface.DEFAULT &&
+        typeface != Typeface.create(Typeface.DEFAULT, Typeface.NORMAL)
+    }?.let { FontFamily(it) }
+}
+
+private val TagHandler = object : TagHandler {
+    override fun handleTag(
+        opening: Boolean,
+        tag: String?,
+        output: Editable?,
+        xmlReader: XMLReader?
+    ) {
+        if (xmlReader == null || output == null) return
+
+        if (opening && tag == ContentHandlerReplacementTag) {
+            val currentContentHandler = xmlReader.contentHandler
+            xmlReader.contentHandler = AnnotationContentHandler(currentContentHandler, output)
+        }
+    }
+}
+
+private class AnnotationContentHandler(
+    private val contentHandler: ContentHandler,
+    private val output: Editable
+) : ContentHandler by contentHandler {
+    override fun startElement(uri: String?, localName: String?, qName: String?, atts: Attributes?) {
+        if (localName == AnnotationTag) {
+            atts?.let { handleAnnotationStart(it) }
+        } else {
+            contentHandler.startElement(uri, localName, qName, atts)
+        }
+    }
+
+    override fun endElement(uri: String?, localName: String?, qName: String?) {
+        if (localName == AnnotationTag) {
+            handleAnnotationEnd()
+        } else {
+            contentHandler.endElement(uri, localName, qName)
+        }
+    }
+
+    private fun handleAnnotationStart(attributes: Attributes) {
+        // Each annotation can have several key/value attributes. So for
+        // <annotation key1=value1 key2=value2>...<annotation>
+        // example we will add two [AnnotationSpan]s which we'll later read
+        for (i in 0 until attributes.length) {
+            val key = attributes.getLocalName(i).orEmpty()
+            val value = attributes.getValue(i).orEmpty()
+            if (key.isNotEmpty() && value.isNotEmpty()) {
+                val start = output.length
+                // add temporary AnnotationSpan to the output to read it when handling
+                // the closing tag
+                output.setSpan(AnnotationSpan(key, value), start, start, SPAN_MARK_MARK)
+            }
+        }
+    }
+
+    private fun handleAnnotationEnd() {
+        // iterate through all of the spans that we added when handling the opening tag. Calculate
+        // the true position of the span and make a replacement
+        output.getSpans(0, output.length, AnnotationSpan::class.java)
+            .filter { output.getSpanFlags(it) == SPAN_MARK_MARK }
+            .fastForEach { annotation ->
+                val start = output.getSpanStart(annotation)
+                val end = output.length
+
+                output.removeSpan(annotation)
+                // only add the annotation if there's a text in between the opening and closing tags
+                if (start != end) {
+                    output.setSpan(annotation, start, end, SPAN_EXCLUSIVE_EXCLUSIVE)
+                }
+            }
+    }
+}
+
+private class AnnotationSpan(val key: String, val value: String)
+
+/**
+ * This tag is added at the beginning of a string fed to the HTML parser in order to trigger
+ * a TagHandler's callback early on so we can replace the ContentHandler with our
+ * own [AnnotationContentHandler]. This is needed to handle the opening <annotation> tags since by
+ * the time TagHandler is triggered, the parser already visited and left the opening <annotation>
+ * tag which contains the attributes. Note that closing tag doesn't have the attributes and
+ * therefore not enough to construct the intermediate [AnnotationSpan] object that is later
+ * transformed into [AnnotatedString]'s string annotation.
+ */
+private const val ContentHandlerReplacementTag = "ContentHandlerReplacementTag"
+private const val AnnotationTag = "annotation"
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt
new file mode 100644
index 0000000..3332e6d
--- /dev/null
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/Html.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024 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
+
+/**
+ * Converts a string with HTML tags into [AnnotatedString].
+ *
+ * If you define your string in the resources, make sure to use HTML-escaped opening brackets
+ * "&lt;" instead of "<".
+ *
+ * For a list of supported tags go check
+ * [Styling with HTML markup](https://developer.android.com/guide/topics/resources/string-resource#StylingWithHTML)
+ * guide. Note that bullet lists and custom annotations are not **yet** available.
+ *
+ * Example of displaying styled string from resources
+ * @sample androidx.compose.ui.text.samples.AnnotatedStringFromHtml
+ */
+expect fun String.parseAsHtml(): AnnotatedString
diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
new file mode 100644
index 0000000..20a8f27
--- /dev/null
+++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/Html.skiko.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2024 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
+
+/**
+ * TBD: not yet implemented.
+ *
+ * Converts a string with HTML tags into [AnnotatedString].
+ */
+actual fun String.parseAsHtml(): AnnotatedString {
+    return AnnotatedString(this)
+}
diff --git a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
index e09ba8d..bb18d5b 100644
--- a/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
+++ b/compose/ui/ui-tooling/src/androidInstrumentedTest/kotlin/androidx/compose/ui/tooling/SimpleComposablePreview.kt
@@ -28,7 +28,6 @@
 import androidx.compose.runtime.saveable.LocalSaveableStateRegistry
 import androidx.compose.ui.geometry.CornerRadius
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.tooling.preview.PreviewDynamicColors
 import androidx.compose.ui.tooling.preview.PreviewFontScale
@@ -37,6 +36,7 @@
 import androidx.compose.ui.tooling.preview.PreviewParameterProvider
 import androidx.compose.ui.tooling.preview.PreviewScreenSizes
 import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 
 @Preview
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index f0f1961..355ba52 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2387,6 +2387,7 @@
     method public int getMeasuredWidth();
     method protected final long getMeasurementConstraints();
     method public final int getWidth();
+    method protected void placeAt(long position, float zIndex, androidx.compose.ui.graphics.layer.GraphicsLayer layer);
     method protected abstract void placeAt(long position, float zIndex, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit>? layerBlock);
     method protected final void setMeasuredSize(long);
     method protected final void setMeasurementConstraints(long);
@@ -2409,9 +2410,13 @@
     method public final void place(androidx.compose.ui.layout.Placeable, long position, optional float zIndex);
     method public final void placeRelative(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex);
     method public final void placeRelative(androidx.compose.ui.layout.Placeable, long position, optional float zIndex);
+    method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     property public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
     property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 3a8128c..ab80884 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2394,6 +2394,7 @@
     method public int getMeasuredWidth();
     method protected final long getMeasurementConstraints();
     method public final int getWidth();
+    method protected void placeAt(long position, float zIndex, androidx.compose.ui.graphics.layer.GraphicsLayer layer);
     method protected abstract void placeAt(long position, float zIndex, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit>? layerBlock);
     method protected final void setMeasuredSize(long);
     method protected final void setMeasurementConstraints(long);
@@ -2416,9 +2417,13 @@
     method public final void place(androidx.compose.ui.layout.Placeable, long position, optional float zIndex);
     method public final void placeRelative(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex);
     method public final void placeRelative(androidx.compose.ui.layout.Placeable, long position, optional float zIndex);
+    method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeRelativeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, int x, int y, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
+    method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, androidx.compose.ui.graphics.layer.GraphicsLayer layer, optional float zIndex);
     method public final void placeWithLayer(androidx.compose.ui.layout.Placeable, long position, optional float zIndex, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> layerBlock);
     property public androidx.compose.ui.layout.LayoutCoordinates? coordinates;
     property protected abstract androidx.compose.ui.unit.LayoutDirection parentLayoutDirection;
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 265869f..8180f6d 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -98,7 +98,7 @@
                 implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
                 implementation("androidx.emoji2:emoji2:1.2.0")
 
-                implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+                implementation("androidx.profileinstaller:profileinstaller:1.3.1")
             }
         }
 
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
index 5c7bddf..3b9a823 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
@@ -44,11 +44,11 @@
 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import kotlin.math.roundToInt
 
 @Suppress("SetTextI18n")
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
index 92d11e5..c9f8888 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/DrawingPrebuiltGraphicsLayerTest.kt
@@ -35,11 +35,13 @@
 import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.layer.drawLayer
 import androidx.compose.ui.graphics.rememberGraphicsLayer
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.platform.LocalGraphicsContext
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -169,6 +171,39 @@
     }
 
     @Test
+    fun keepDrawingTheLayerWePreviouslyPlacedWith() {
+        // even that we don't place it anymore, it still holds the content we can continue drawing
+        rule.setContent {
+            Column {
+                if (!drawPrebuiltLayer) {
+                    val layer = obtainLayer()
+                    Canvas(
+                        Modifier
+                            .layout { measurable, _ ->
+                                val placeable = measurable.measure(Constraints.fixed(size, size))
+                                layout(placeable.width, placeable.height) {
+                                    placeable.placeWithLayer(0, 0, layer)
+                                }
+                            }
+                    ) {
+                        drawRect(Color.Red)
+                    }
+                } else {
+                    LayerDrawingBox()
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            drawPrebuiltLayer = true
+        }
+
+        rule.onNodeWithTag(LayerDrawingBoxTag)
+            .captureToImage()
+            .assertPixels(expectedSize) { Color.Red }
+    }
+
+    @Test
     fun drawNestedLayers_drawLayer() {
         rule.setContent {
             if (!drawPrebuiltLayer) {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index 0d97c19..01b1a83 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -62,6 +62,8 @@
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.drawscope.inset
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.rememberGraphicsLayer
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.Layout
@@ -86,6 +88,7 @@
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -1797,4 +1800,82 @@
             assertEquals(0, modifierRelayoutCount)
         }
     }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun placingWithExplicitLayerDraws() {
+        rule.setContent {
+            val layer = rememberGraphicsLayer()
+            Canvas(
+                modifier = Modifier
+                    .testTag("tag")
+                    .layout { measurable, _ ->
+                        val placeable = measurable.measure(Constraints.fixed(10, 10))
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeWithLayer(0, 0, layer)
+                        }
+                    }
+            ) {
+                drawRect(Color.Blue)
+            }
+        }
+
+        rule.onNodeWithTag("tag")
+            .captureToImage()
+            .assertPixels(IntSize(10, 10)) { Color.Blue }
+    }
+
+    @Test
+    fun placingWithExplicitLayerSetsCorrectSizeAndOffset() {
+        lateinit var layer: GraphicsLayer
+        rule.setContent {
+            layer = rememberGraphicsLayer()
+            Canvas(
+                modifier = Modifier.layout { measurable, _ ->
+                    val placeable = measurable.measure(Constraints.fixed(20, 20))
+                    layout(placeable.width, placeable.height) {
+                        placeable.placeWithLayer(10, 10, layer)
+                    }
+                }
+            ) {
+                drawRect(Color.Blue)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(layer.size.width).isEqualTo(20)
+            assertThat(layer.size.height).isEqualTo(20)
+            assertThat(layer.topLeft.x).isEqualTo(10)
+            assertThat(layer.topLeft.y).isEqualTo(10)
+        }
+    }
+
+    @Test
+    fun layerIsNotReleasedWhenWeStopPlacingIt() {
+        lateinit var layer: GraphicsLayer
+        var needChild by mutableStateOf(true)
+        rule.setContent {
+            layer = rememberGraphicsLayer()
+            if (needChild) {
+                Canvas(
+                    modifier = Modifier.layout { measurable, constraints ->
+                        val placeable = measurable.measure(constraints)
+                        layout(placeable.width, placeable.height) {
+                            placeable.placeWithLayer(1, 0, layer)
+                        }
+                    }
+                ) {
+                    drawRect(Color.Blue)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            needChild = false
+        }
+
+        rule.runOnIdle {
+            assertThat(layer.isReleased).isFalse()
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 8559282..67479b38 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -3765,7 +3766,8 @@
 
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ): OwnedLayer {
         return object : OwnedLayer {
             override fun updateLayerProperties(
@@ -3783,7 +3785,7 @@
             override fun resize(size: IntSize) {
             }
 
-            override fun drawLayer(canvas: Canvas) {
+            override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
                 drawBlock(canvas)
             }
 
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 71afb4c..b08bb68 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -32,6 +32,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -3420,7 +3421,8 @@
 
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ): OwnedLayer {
         TODO("Not yet implemented")
     }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index 5e0d001..316e860 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -209,8 +210,11 @@
     override val autofill: Autofill
         get() = TODO("Not yet implemented")
 
-    override fun createLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit) =
-        createLayer()
+    override fun createLayer(
+        drawBlock: (Canvas) -> Unit,
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
+    ) = createLayer()
 
     override fun onRequestRelayout(
         layoutNode: LayoutNode,
@@ -581,7 +585,7 @@
     override fun resize(size: IntSize) {
     }
 
-    override fun drawLayer(canvas: Canvas) {
+    override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
     }
 
     override fun updateDisplayList() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
index cadfde0..af67ee0 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/OnSizeChangedTest.kt
@@ -216,8 +216,8 @@
     @Test
     @SmallTest
     fun addedModifier() {
-        var latch1 = CountDownLatch(1)
-        var latch2 = CountDownLatch(1)
+        val latch1 = CountDownLatch(1)
+        val latch2 = CountDownLatch(1)
         var changedSize1 = IntSize.Zero
         var changedSize2 = IntSize.Zero
         var addModifier by mutableStateOf(false)
@@ -246,7 +246,6 @@
         assertEquals(10, changedSize1.height)
         assertEquals(10, changedSize1.width)
 
-        latch1 = CountDownLatch(1)
         addModifier = true
 
         // We've added an onSizeChanged modifier, so it must trigger another size change
@@ -257,6 +256,133 @@
 
     @Test
     @SmallTest
+    fun addedModifierNode() {
+        val sizeLatch1 = CountDownLatch(1)
+        val sizeLatch2 = CountDownLatch(1)
+        val placedLatch1 = CountDownLatch(1)
+        val placedLatch2 = CountDownLatch(1)
+        var changedSize1 = IntSize.Zero
+        var changedSize2 = IntSize.Zero
+        var addModifier by mutableStateOf(false)
+
+        val node = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize1 = size
+                sizeLatch1.countDown()
+            }
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch1.countDown()
+            }
+        }
+
+        val node2 = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize2 = size
+                sizeLatch2.countDown()
+            }
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch2.countDown()
+            }
+        }
+
+        rule.runOnUiThread {
+            activity.setContent {
+                with(LocalDensity.current) {
+                    val mod = if (addModifier) Modifier.elementFor(node2) else Modifier
+                    Box(
+                        Modifier.padding(10.toDp()).elementFor(node).then(mod)
+                    ) {
+                        Box(Modifier.requiredSize(10.toDp()))
+                    }
+                }
+            }
+        }
+
+        // Initial setting will call onRemeasured and onPlaced
+        assertTrue(sizeLatch1.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch1.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize1.height)
+        assertEquals(10, changedSize1.width)
+
+        addModifier = true
+
+        // We've added a node, so it must trigger onRemeasured and onPlaced on the new node
+        assertTrue(sizeLatch2.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch2.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize2.height)
+        assertEquals(10, changedSize2.width)
+    }
+
+    @Test
+    @SmallTest
+    fun lazilyDelegatedModifierNode() {
+        val sizeLatch1 = CountDownLatch(1)
+        val sizeLatch2 = CountDownLatch(1)
+        val placedLatch1 = CountDownLatch(1)
+        val placedLatch2 = CountDownLatch(1)
+        var changedSize1 = IntSize.Zero
+        var changedSize2 = IntSize.Zero
+
+        val node = object : LayoutAwareModifierNode, Modifier.Node() {
+            override fun onRemeasured(size: IntSize) {
+                changedSize1 = size
+                sizeLatch1.countDown()
+            }
+
+            override fun onPlaced(coordinates: LayoutCoordinates) {
+                placedLatch1.countDown()
+            }
+        }
+
+        val node2 = object : DelegatingNode() {
+            fun addDelegate() {
+                delegate(
+                    object : LayoutAwareModifierNode, Modifier.Node() {
+                        override fun onRemeasured(size: IntSize) {
+                            changedSize2 = size
+                            sizeLatch2.countDown()
+                        }
+
+                        override fun onPlaced(coordinates: LayoutCoordinates) {
+                            placedLatch2.countDown()
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnUiThread {
+            activity.setContent {
+                with(LocalDensity.current) {
+                    val mod = Modifier.elementFor(node2)
+                    Box(
+                        Modifier.padding(10.toDp()).elementFor(node).then(mod)
+                    ) {
+                        Box(Modifier.requiredSize(10.toDp()))
+                    }
+                }
+            }
+        }
+
+        // Initial setting will call onRemeasured and onPlaced
+        assertTrue(sizeLatch1.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch1.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize1.height)
+        assertEquals(10, changedSize1.width)
+
+        rule.runOnUiThread {
+            node2.addDelegate()
+        }
+
+        // We've delegated to a node, so it must trigger onRemeasured and onPlaced on the new node
+        assertTrue(sizeLatch2.await(1, TimeUnit.SECONDS))
+        assertTrue(placedLatch2.await(1, TimeUnit.SECONDS))
+        assertEquals(10, changedSize2.height)
+        assertEquals(10, changedSize2.width)
+    }
+
+    @Test
+    @SmallTest
     fun modifierIsReturningEqualObjectForTheSameLambda() {
         val lambda: (IntSize) -> Unit = { }
         assertEquals(Modifier.onSizeChanged(lambda), Modifier.onSizeChanged(lambda))
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index a85b878..95e0379 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -2859,7 +2859,7 @@
     private fun SemanticsNodeInteraction.assertIsDetached() {
         assertDoesNotExist()
         // we want to verify the node is not deactivated, but such API does not exist yet
-        expectAssertionError(true) {
+        expectAssertionError {
             assertIsDeactivated()
         }
     }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index 51f9876..0b4ff31 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -484,7 +485,8 @@
     }
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ): OwnedLayer {
         val transform = Matrix()
         val inverseTransform = Matrix()
@@ -492,7 +494,9 @@
             override fun isInLayer(position: Offset) = true
             override fun move(position: IntOffset) {}
             override fun resize(size: IntSize) {}
-            override fun drawLayer(canvas: Canvas) { drawBlock(canvas) }
+            override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
+                drawBlock(canvas)
+            }
             override fun updateDisplayList() {}
             override fun invalidate() { invalidatedLayers.add(this) }
             override fun destroy() {}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
index 6c8d88b..8ea1a4c 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInAppCompatActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
index 9d79d60..73ed014 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInComponentActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
index 497f8be..cae14f2 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/owners/LifecycleOwnerInFragmentTest.kt
@@ -22,11 +22,11 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
 import androidx.fragment.app.FragmentContainerView
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import java.util.concurrent.CountDownLatch
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
index b1c2668..4f703cb 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/platform/ComposeViewOverlayTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.InternalComposeUiApi
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.rules.activityScenarioRule
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9c7155d1..3e56cd1 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -74,7 +74,6 @@
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.platform.ViewCompositionStrategy
 import androidx.compose.ui.platform.findViewTreeCompositionContext
@@ -108,6 +107,7 @@
 import androidx.lifecycle.Lifecycle.Event.ON_STOP
 import androidx.lifecycle.LifecycleEventObserver
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.savedstate.SavedStateRegistry
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 923af73..17c75d2 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
@@ -111,6 +111,7 @@
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.setFrom
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.toComposeRect
@@ -1376,8 +1377,12 @@
 
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ): OwnedLayer {
+        if (explicitLayer != null) {
+            return GraphicsLayerOwnerLayer(explicitLayer, this, drawBlock, invalidateParentLayer)
+        }
         // First try the layer cache
         val layer = layerCache.pop()
         if (layer !== null) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
new file mode 100644
index 0000000..d99860d
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/GraphicsLayerOwnerLayer.android.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2024 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.platform
+
+import androidx.compose.ui.geometry.MutableRect
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
+import androidx.compose.ui.graphics.drawscope.CanvasDrawScope
+import androidx.compose.ui.graphics.drawscope.draw
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.internal.throwIllegalStateException
+import androidx.compose.ui.layout.GraphicLayerInfo
+import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.center
+import androidx.compose.ui.unit.toOffset
+import androidx.compose.ui.unit.toSize
+
+internal class GraphicsLayerOwnerLayer(
+    private val graphicsLayer: GraphicsLayer,
+    private val ownerView: AndroidComposeView,
+    drawBlock: (Canvas) -> Unit,
+    invalidateParentLayer: () -> Unit
+) : OwnedLayer, GraphicLayerInfo {
+    private var drawBlock: ((Canvas) -> Unit)? = drawBlock
+    private var invalidateParentLayer: (() -> Unit)? = invalidateParentLayer
+
+    private var size: IntSize = IntSize.Zero
+    private var isDestroyed = false
+    private val matrixCache = Matrix()
+
+    private var isDirty = true
+        set(value) {
+            if (value != field) {
+                field = value
+                ownerView.notifyLayerIsDirty(this, value)
+            }
+        }
+
+    private var density = Density(1f)
+    private var layoutDirection = LayoutDirection.Ltr
+    private val scope = CanvasDrawScope()
+
+    private var tmpTouchPointPath: Path? = null
+    private var tmpOpPath: Path? = null
+
+    override fun updateLayerProperties(
+        scope: ReusableGraphicsLayerScope,
+        layoutDirection: LayoutDirection,
+        density: Density,
+    ) {
+        throwIllegalStateException(
+            "Current apis doesn't allow for both GraphicsLayer and GraphicsLayerScope to be " +
+                "used together"
+        )
+    }
+
+    override fun isInLayer(position: Offset): Boolean {
+        val x = position.x
+        val y = position.y
+
+        if (graphicsLayer.clip) {
+            val outline = graphicsLayer.outline
+            return isInOutline(outline, x, y, tmpTouchPointPath, tmpOpPath)
+        }
+
+        return true
+    }
+
+    override fun move(position: IntOffset) {
+        graphicsLayer.topLeft = position
+    }
+
+    override fun resize(size: IntSize) {
+        if (size != this.size) {
+            this.size = size
+            invalidate()
+        }
+    }
+
+    override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
+        updateDisplayList()
+        scope.draw(density, layoutDirection, canvas, size.toSize(), parentLayer) {
+            drawLayer(graphicsLayer)
+        }
+    }
+
+    override fun updateDisplayList() {
+        if (isDirty) {
+            graphicsLayer.buildLayer(density, layoutDirection, size) {
+                drawIntoCanvas { canvas ->
+                    drawBlock?.let { it(canvas) }
+                }
+            }
+            isDirty = false
+        }
+    }
+
+    override fun invalidate() {
+        if (!isDirty && !isDestroyed) {
+            ownerView.invalidate()
+            isDirty = true
+        }
+    }
+
+    override fun destroy() {
+        drawBlock = null
+        invalidateParentLayer = null
+        isDestroyed = true
+        isDirty = false
+    }
+
+    override fun mapOffset(point: Offset, inverse: Boolean): Offset {
+        return getMatrix(inverse).map(point)
+    }
+
+    override fun mapBounds(rect: MutableRect, inverse: Boolean) {
+        getMatrix(inverse).map(rect)
+    }
+
+    override fun reuseLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit) {
+        throwIllegalStateException("reuseLayer is not supported yet")
+    }
+
+    override fun transform(matrix: Matrix) {
+        matrix.timesAssign(getMatrix(inverse = false))
+    }
+
+    override fun inverseTransform(matrix: Matrix) {
+        matrix.timesAssign(getMatrix(inverse = true))
+    }
+
+    override val layerId: Long
+        get() = graphicsLayer.layerId
+
+    override val ownerViewId: Long
+        get() = graphicsLayer.ownerViewId
+
+    private fun getMatrix(inverse: Boolean = false): Matrix {
+        updateMatrix()
+        if (inverse) {
+            matrixCache.invert()
+        }
+        return matrixCache
+    }
+
+    private fun updateMatrix() = with(graphicsLayer) {
+        val pivot = if (pivotOffset.isUnspecified) size.center.toOffset() else pivotOffset
+
+        matrixCache.reset()
+        matrixCache *= Matrix().apply {
+            translate(x = -pivot.x, y = -pivot.y)
+        }
+        matrixCache *= Matrix().apply {
+            translate(translationX, translationY)
+            rotateX(rotationX)
+            rotateY(rotationY)
+            rotateZ(rotationZ)
+            scale(scaleX, scaleY)
+        }
+        matrixCache *= Matrix().apply {
+            translate(x = pivot.x, y = pivot.y)
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index 2d0e9030..7e258ec 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
 import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.GraphicLayerInfo
@@ -276,7 +277,7 @@
         }
     }
 
-    override fun drawLayer(canvas: Canvas) {
+    override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
         val androidCanvas = canvas.nativeCanvas
         if (androidCanvas.isHardwareAccelerated) {
             updateDisplayList()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index ca943b3..4ef277e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
 import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.GraphicLayerInfo
 import androidx.compose.ui.node.OwnedLayer
@@ -308,7 +309,7 @@
         }
     }
 
-    override fun drawLayer(canvas: Canvas) {
+    override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
         drawnWithZ = elevation > 0f
         if (drawnWithZ) {
             canvas.enableZ()
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 47250c8..744a9a9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -47,13 +47,13 @@
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.ViewRootForInspector
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.savedstate.SavedStateRegistryOwner
 
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index abde106..14e60cb 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -36,6 +36,7 @@
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -2644,7 +2645,8 @@
 
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ): OwnedLayer {
         val transform = Matrix()
         val inverseTransform = Matrix()
@@ -2670,7 +2672,7 @@
             override fun resize(size: IntSize) {
             }
 
-            override fun drawLayer(canvas: Canvas) {
+            override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
                 drawBlock(canvas)
             }
 
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
index 1d27c7b..1c7c4c2 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/ModifierLocalConsumerEntityTest.kt
@@ -33,6 +33,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsContext
 import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -386,8 +387,11 @@
         override val autofill: Autofill
             get() = TODO("Not yet implemented")
 
-        override fun createLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit) =
-            TODO("Not yet implemented")
+        override fun createLayer(
+            drawBlock: (Canvas) -> Unit,
+            invalidateParentLayer: () -> Unit,
+            explicitLayer: GraphicsLayer?
+        ) = TODO("Not yet implemented")
 
         override fun onRequestRelayout(
             layoutNode: LayoutNode,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
index 6ebe14f..91fe684 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/OnRemeasuredModifier.kt
@@ -20,9 +20,9 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.IntSize
 
 /**
@@ -44,27 +44,15 @@
 @Stable
 fun Modifier.onSizeChanged(
     onSizeChanged: (IntSize) -> Unit
-) = this.then(
-    OnSizeChangedModifier(
-        onSizeChanged = onSizeChanged,
-        inspectorInfo = debugInspectorInfo {
-            name = "onSizeChanged"
-            properties["onSizeChanged"] = onSizeChanged
-        }
-    )
-)
+) = this.then(OnSizeChangedModifier(onSizeChanged = onSizeChanged))
 
 private class OnSizeChangedModifier(
-    val onSizeChanged: (IntSize) -> Unit,
-    inspectorInfo: InspectorInfo.() -> Unit
-) : OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
-    private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+    private val onSizeChanged: (IntSize) -> Unit
+) : ModifierNodeElement<OnSizeChangedNode>() {
+    override fun create(): OnSizeChangedNode = OnSizeChangedNode(onSizeChanged)
 
-    override fun onRemeasured(size: IntSize) {
-        if (previousSize != size) {
-            onSizeChanged(size)
-            previousSize = size
-        }
+    override fun update(node: OnSizeChangedNode) {
+        node.update(onSizeChanged)
     }
 
     override fun equals(other: Any?): Boolean {
@@ -77,6 +65,33 @@
     override fun hashCode(): Int {
         return onSizeChanged.hashCode()
     }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "onSizeChanged"
+        properties["onSizeChanged"] = onSizeChanged
+    }
+}
+
+private class OnSizeChangedNode(
+    private var onSizeChanged: (IntSize) -> Unit
+) : Modifier.Node(), LayoutAwareModifierNode {
+    // When onSizeChanged changes, we want to invalidate so onRemeasured is called again
+    override val shouldAutoInvalidate: Boolean = true
+    private var previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+
+    fun update(onSizeChanged: (IntSize) -> Unit) {
+        this.onSizeChanged = onSizeChanged
+        // Reset the previous size, so when onSizeChanged changes the new lambda gets invoked,
+        // matching previous behavior
+        previousSize = IntSize(Int.MIN_VALUE, Int.MIN_VALUE)
+    }
+
+    override fun onRemeasured(size: IntSize) {
+        if (previousSize != size) {
+            onSizeChanged(size)
+            previousSize = size
+        }
+    }
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index c432407..d96c185 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.layout
 
 import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.node.LookaheadCapablePlaceable
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.unit.Constraints
@@ -105,6 +106,24 @@
     )
 
     /**
+     * Positions the [Placeable] at [position] in its parent's coordinate system.
+     *
+     * @param zIndex controls the drawing order for the [Placeable]. A [Placeable] with larger
+     * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
+     * have the same [zIndex] the order in which the items were placed is used.
+     * @param layer [GraphicsLayer] to place this placeable with. If the [Placeable] will be
+     * placed with a new [position] next time only the graphic layer will be moved without
+     * requiring to redrawn the [Placeable] content.
+     */
+    protected open fun placeAt(
+        position: IntOffset,
+        zIndex: Float,
+        layer: GraphicsLayer
+    ) {
+        placeAt(position, zIndex, null)
+    }
+
+    /**
      * The constraints used for the measurement made to obtain this [Placeable].
      */
     protected var measurementConstraints: Constraints = DefaultConstraints
@@ -331,6 +350,98 @@
             layerBlock: GraphicsLayerScope.() -> Unit = DefaultLayerBlock
         ) = placeApparentToRealOffset(position, zIndex, layerBlock)
 
+        /**
+         * Place a [Placeable] at [x], [y] in its parent's coordinate system with an introduced
+         * graphic layer.
+         * Unlike [placeRelative], the given position will not implicitly react in RTL layout direction
+         * contexts.
+         *
+         * @param x x coordinate in the parent's coordinate system.
+         * @param y y coordinate in the parent's coordinate system.
+         * @param zIndex controls the drawing order for the [Placeable]. A [Placeable] with larger
+         * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
+         * have the same [zIndex] the order in which the items were placed is used.
+         * @param layer [GraphicsLayer] to place this placeable with. If the [Placeable] will be
+         * placed with a new [x] or [y] next time only the graphic layer will be moved without
+         * requiring to redrawn the [Placeable] content.
+         */
+        fun Placeable.placeWithLayer(
+            x: Int,
+            y: Int,
+            layer: GraphicsLayer,
+            zIndex: Float = 0f,
+        ) = placeApparentToRealOffset(IntOffset(x, y), zIndex, layer)
+
+        /**
+         * Place a [Placeable] at [position] in its parent's coordinate system with an introduced
+         * graphic layer.
+         * Unlike [placeRelative], the given [position] will not implicitly react in RTL layout direction
+         * contexts.
+         *
+         * @param position position it parent's coordinate system.
+         * @param zIndex controls the drawing order for the [Placeable]. A [Placeable] with larger
+         * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
+         * have the same [zIndex] the order in which the items were placed is used.
+         * @param layer [GraphicsLayer] to place this placeable with. If the [Placeable] will be
+         * placed with a new [position] next time only the graphic layer will be moved without
+         * requiring to redrawn the [Placeable] content.
+         */
+        fun Placeable.placeWithLayer(
+            position: IntOffset,
+            layer: GraphicsLayer,
+            zIndex: Float = 0f,
+        ) = placeApparentToRealOffset(position, zIndex, layer)
+
+        /**
+         * Place a [Placeable] at [x], [y] in its parent's coordinate system with an introduced
+         * graphic layer.
+         * If the layout direction is right-to-left, the given position will be horizontally
+         * mirrored so that the position of the [Placeable] implicitly reacts to RTL layout
+         * direction contexts.
+         * If this method is used outside the [MeasureScope.layout] positioning block, the
+         * automatic position mirroring will not happen and the [Placeable] will be placed at the
+         * given position, similar to the [place] method.
+         *
+         * @param x x coordinate in the parent's coordinate system.
+         * @param y y coordinate in the parent's coordinate system.
+         * @param zIndex controls the drawing order for the [Placeable]. A [Placeable] with larger
+         * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
+         * have the same [zIndex] the order in which the items were placed is used.
+         * @param layer [GraphicsLayer] to place this placeable with. If the [Placeable] will be
+         * placed with a new [x] or [y] next time only the graphic layer will be moved without
+         * requiring to redrawn the [Placeable] content.
+         */
+        fun Placeable.placeRelativeWithLayer(
+            x: Int,
+            y: Int,
+            layer: GraphicsLayer,
+            zIndex: Float = 0f
+        ) = placeAutoMirrored(IntOffset(x, y), zIndex, layer)
+
+        /**
+         * Place a [Placeable] at [position] in its parent's coordinate system with an introduced
+         * graphic layer.
+         * If the layout direction is right-to-left, the given [position] will be horizontally
+         * mirrored so that the position of the [Placeable] implicitly reacts to RTL layout
+         * direction contexts.
+         * If this method is used outside the [MeasureScope.layout] positioning block, the
+         * automatic position mirroring will not happen and the [Placeable] will be placed at the
+         * given [position], similar to the [place] method.
+         *
+         * @param position position it parent's coordinate system.
+         * @param zIndex controls the drawing order for the [Placeable]. A [Placeable] with larger
+         * [zIndex] will be drawn on top of all the children with smaller [zIndex]. When children
+         * have the same [zIndex] the order in which the items were placed is used.
+         * @param layer [GraphicsLayer] to place this placeable with. If the [Placeable] will be
+         * placed with a new [position] next time only the graphic layer will be moved without
+         * requiring to redrawn the [Placeable] content.
+         */
+        fun Placeable.placeRelativeWithLayer(
+            position: IntOffset,
+            layer: GraphicsLayer,
+            zIndex: Float = 0f
+        ) = placeAutoMirrored(position, zIndex, layer)
+
         @Suppress("NOTHING_TO_INLINE")
         internal inline fun Placeable.placeAutoMirrored(
             position: IntOffset,
@@ -349,13 +460,39 @@
         }
 
         @Suppress("NOTHING_TO_INLINE")
+        internal inline fun Placeable.placeAutoMirrored(
+            position: IntOffset,
+            zIndex: Float,
+            layer: GraphicsLayer
+        ) {
+            if (parentLayoutDirection == LayoutDirection.Ltr || parentWidth == 0) {
+                placeApparentToRealOffset(position, zIndex, layer)
+            } else {
+                placeApparentToRealOffset(
+                    IntOffset((parentWidth - width - position.x), position.y),
+                    zIndex,
+                    layer
+                )
+            }
+        }
+
+        @Suppress("NOTHING_TO_INLINE")
         internal inline fun Placeable.placeApparentToRealOffset(
             position: IntOffset,
             zIndex: Float,
-            noinline layerBlock: (GraphicsLayerScope.() -> Unit)?
+            noinline layerBlock: (GraphicsLayerScope.() -> Unit)?,
         ) {
             placeAt(position + apparentToRealOffset, zIndex, layerBlock)
         }
+
+        @Suppress("NOTHING_TO_INLINE")
+        internal inline fun Placeable.placeApparentToRealOffset(
+            position: IntOffset,
+            zIndex: Float,
+            layer: GraphicsLayer
+        ) {
+            placeAt(position + apparentToRealOffset, zIndex, layer)
+        }
     }
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 25ba62db..26d852c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -154,10 +154,22 @@
     override fun placeAt(
         position: IntOffset,
         zIndex: Float,
-        layerBlock: (GraphicsLayerScope.() -> Unit)?
+        layer: GraphicsLayer
+    ) {
+        super.placeAt(position, zIndex, layer)
+        onAfterPlaceAt()
+    }
+
+    override fun placeAt(
+        position: IntOffset,
+        zIndex: Float,
+        layerBlock: (GraphicsLayerScope.() -> Unit)?,
     ) {
         super.placeAt(position, zIndex, layerBlock)
+        onAfterPlaceAt()
+    }
 
+    private fun onAfterPlaceAt() {
         // The coordinator only runs their placement block to obtain our position, which allows them
         // to calculate the offset of an alignment line we have already provided a position for.
         // No need to place our wrapped as well (we might have actually done this already in
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
index b1ce10a..e491fd1e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNodeCoordinator.kt
@@ -231,9 +231,22 @@
     override fun placeAt(
         position: IntOffset,
         zIndex: Float,
+        layer: GraphicsLayer
+    ) {
+        super.placeAt(position, zIndex, layer)
+        onAfterPlaceAt()
+    }
+
+    override fun placeAt(
+        position: IntOffset,
+        zIndex: Float,
         layerBlock: (GraphicsLayerScope.() -> Unit)?
     ) {
         super.placeAt(position, zIndex, layerBlock)
+        onAfterPlaceAt()
+    }
+
+    private fun onAfterPlaceAt() {
         // The coordinator only runs their placement block to obtain our position, which allows them
         // to calculate the offset of an alignment line we have already provided a position for.
         // No need to place our wrapped as well (we might have actually done this already in
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 353991f..25cc805 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -1387,6 +1387,7 @@
             // we don't need to reset state as it was done when deactivated
         } else {
             resetModifierState()
+            resetExplicitLayers()
         }
         // resetModifierState detaches all nodes, so we need to re-attach them upon reuse.
         semanticsId = generateSemanticsId()
@@ -1404,6 +1405,13 @@
         if (isAttached) {
             invalidateSemantics()
         }
+        resetExplicitLayers()
+    }
+
+    private fun resetExplicitLayers() {
+        forEachCoordinatorIncludingInner {
+            it.releaseExplicitLayer()
+        }
     }
 
     override fun onRelease() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
index ba34e9c5..32679aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeDrawScope.kt
@@ -34,7 +34,7 @@
  */
 @OptIn(ExperimentalComposeUiApi::class)
 internal class LayoutNodeDrawScope(
-    private val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
+    val canvasDrawScope: CanvasDrawScope = CanvasDrawScope()
 ) : DrawScope by canvasDrawScope, ContentDrawScope {
 
     // NOTE, currently a single ComponentDrawScope is shared across composables
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 282375b..c0b271a 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.internal.checkPrecondition
 import androidx.compose.ui.internal.checkPreconditionNotNull
 import androidx.compose.ui.internal.requirePrecondition
@@ -319,6 +320,7 @@
 
         private var lastPosition: IntOffset = IntOffset.Zero
         private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
+        private var lastExplicitLayer: GraphicsLayer? = null
         private var lastZIndex: Float = 0f
 
         private var parentDataDirty: Boolean = true
@@ -480,6 +482,7 @@
 
         // Used by placeOuterBlock to avoid allocating the lambda on every call
         private var placeOuterCoordinatorLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
+        private var placeOuterCoordinatorLayer: GraphicsLayer? = null
         private var placeOuterCoordinatorPosition = IntOffset.Zero
         private var placeOuterCoordinatorZIndex = 0f
 
@@ -488,7 +491,14 @@
                 ?: layoutNode.requireOwner().placementScope
             with(scope) {
                 val layerBlock = placeOuterCoordinatorLayerBlock
-                if (layerBlock == null) {
+                val layer = placeOuterCoordinatorLayer
+                if (layer != null) {
+                    outerCoordinator.placeWithLayer(
+                        placeOuterCoordinatorPosition,
+                        layer,
+                        placeOuterCoordinatorZIndex
+                    )
+                } else if (layerBlock == null) {
                     outerCoordinator.place(
                         placeOuterCoordinatorPosition,
                         placeOuterCoordinatorZIndex
@@ -689,6 +699,23 @@
             zIndex: Float,
             layerBlock: (GraphicsLayerScope.() -> Unit)?
         ) {
+            placeSelf(position, zIndex, layerBlock, null)
+        }
+
+        override fun placeAt(
+            position: IntOffset,
+            zIndex: Float,
+            layer: GraphicsLayer
+        ) {
+            placeSelf(position, zIndex, null, layer)
+        }
+
+        private fun placeSelf(
+            position: IntOffset,
+            zIndex: Float,
+            layerBlock: (GraphicsLayerScope.() -> Unit)?,
+            layer: GraphicsLayer?
+        ) {
             isPlacedByParent = true
             if (position != lastPosition) {
                 if (coordinatesAccessedDuringModifierPlacement ||
@@ -724,13 +751,14 @@
             }
 
             // Post-lookahead (if any) placement
-            placeOuterCoordinator(position, zIndex, layerBlock)
+            placeOuterCoordinator(position, zIndex, layerBlock, layer)
         }
 
         private fun placeOuterCoordinator(
             position: IntOffset,
             zIndex: Float,
-            layerBlock: (GraphicsLayerScope.() -> Unit)?
+            layerBlock: (GraphicsLayerScope.() -> Unit)?,
+            layer: GraphicsLayer?
         ) {
             requirePrecondition(!layoutNode.isDeactivated) {
                 "place is called on a deactivated node"
@@ -740,12 +768,13 @@
             lastPosition = position
             lastZIndex = zIndex
             lastLayerBlock = layerBlock
+            lastExplicitLayer = layer
             placedOnce = true
             onNodePlacedCalled = false
 
             val owner = layoutNode.requireOwner()
             if (!layoutPending && isPlaced) {
-                outerCoordinator.placeSelfApparentToRealOffset(position, zIndex, layerBlock)
+                outerCoordinator.placeSelfApparentToRealOffset(position, zIndex, layerBlock, layer)
                 onNodePlaced()
             } else {
                 alignmentLines.usedByModifierLayout = false
@@ -753,10 +782,10 @@
                 placeOuterCoordinatorLayerBlock = layerBlock
                 placeOuterCoordinatorPosition = position
                 placeOuterCoordinatorZIndex = zIndex
+                placeOuterCoordinatorLayer = layer
                 owner.snapshotObserver.observeLayoutModifierSnapshotReads(
                     layoutNode, affectsLookahead = false, block = placeOuterCoordinatorBlock
                 )
-                placeOuterCoordinatorLayerBlock = null
             }
 
             layoutState = LayoutState.Idle
@@ -772,7 +801,7 @@
                 relayoutWithoutParentInProgress = true
                 checkPrecondition(placedOnce) { "replace called on unplaced item" }
                 val wasPlacedBefore = isPlaced
-                placeOuterCoordinator(lastPosition, lastZIndex, lastLayerBlock)
+                placeOuterCoordinator(lastPosition, lastZIndex, lastLayerBlock, lastExplicitLayer)
                 if (wasPlacedBefore && !onNodePlacedCalled) {
                     // parent should be notified that this node is not placed anymore so the
                     // children `placeOrder`s are updated.
@@ -1000,10 +1029,11 @@
             val lookaheadDelegate = checkPreconditionNotNull(lookaheadPassDelegate) {
                 "invalid lookaheadDelegate"
             }
-            placeAt(
+            placeSelf(
                 lookaheadDelegate.lastPosition,
                 lookaheadDelegate.lastZIndex,
-                lookaheadDelegate.lastLayerBlock
+                lookaheadDelegate.lastLayerBlock,
+                lookaheadDelegate.lastExplicitLayer
             )
         }
     }
@@ -1055,6 +1085,9 @@
         internal var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
             private set
 
+        internal var lastExplicitLayer: GraphicsLayer? = null
+            private set
+
         override var isPlaced: Boolean = false
         override val innerCoordinator: NodeCoordinator
             get() = layoutNode.innerCoordinator
@@ -1326,6 +1359,23 @@
             zIndex: Float,
             layerBlock: (GraphicsLayerScope.() -> Unit)?
         ) {
+            placeSelf(position, zIndex, layerBlock, null)
+        }
+
+        override fun placeAt(
+            position: IntOffset,
+            zIndex: Float,
+            layer: GraphicsLayer
+        ) {
+            placeSelf(position, zIndex, null, layer)
+        }
+
+        private fun placeSelf(
+            position: IntOffset,
+            zIndex: Float,
+            layerBlock: (GraphicsLayerScope.() -> Unit)?,
+            layer: GraphicsLayer?
+        ) {
             requirePrecondition(!layoutNode.isDeactivated) {
                 "place is called on a deactivated node"
             }
@@ -1362,6 +1412,7 @@
             lastPosition = position
             lastZIndex = zIndex
             lastLayerBlock = layerBlock
+            lastExplicitLayer = layer
             layoutState = LayoutState.Idle
         }
 
@@ -1590,7 +1641,7 @@
 
                 onNodePlacedCalled = false
                 val wasPlacedBefore = isPlaced
-                placeAt(lastPosition, 0f, null)
+                placeSelf(lastPosition, 0f, lastLayerBlock, lastExplicitLayer)
                 if (wasPlacedBefore && !onNodePlacedCalled) {
                     // parent should be notified that this node is not placed anymore so the
                     // children `placeOrder`s are updated.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index dac2206..380b4b4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -34,6 +34,7 @@
 import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.internal.checkPrecondition
 import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.internal.requirePrecondition
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.LookaheadLayoutCoordinates
@@ -145,7 +146,12 @@
         get() = wrapped
 
     override fun replace() {
-        placeAt(position, zIndex, layerBlock)
+        val explicitLayer = explicitLayer
+        if (explicitLayer != null) {
+            placeAt(position, zIndex, explicitLayer)
+        } else {
+            placeAt(position, zIndex, layerBlock)
+        }
     }
 
     override val hasMeasureResult: Boolean
@@ -216,7 +222,9 @@
             wrappedBy?.invalidateLayer()
         }
         measuredSize = IntSize(width, height)
-        updateLayerParameters(invokeOnLayoutChange = false)
+        if (layerBlock != null) {
+            updateLayerParameters(invokeOnLayoutChange = false)
+        }
         visitNodes(Nodes.Draw) {
             it.onMeasureResultChanged()
         }
@@ -313,18 +321,56 @@
         layerBlock: (GraphicsLayerScope.() -> Unit)?
     ) {
         if (forcePlaceWithLookaheadOffset) {
-            placeSelf(lookaheadDelegate!!.position, zIndex, layerBlock)
+            placeSelf(lookaheadDelegate!!.position, zIndex, layerBlock, null)
         } else {
-            placeSelf(position, zIndex, layerBlock)
+            placeSelf(position, zIndex, layerBlock, null)
+        }
+    }
+
+    override fun placeAt(
+        position: IntOffset,
+        zIndex: Float,
+        layer: GraphicsLayer
+    ) {
+        if (forcePlaceWithLookaheadOffset) {
+            placeSelf(lookaheadDelegate!!.position, zIndex, null, layer)
+        } else {
+            placeSelf(position, zIndex, null, layer)
         }
     }
 
     private fun placeSelf(
         position: IntOffset,
         zIndex: Float,
-        layerBlock: (GraphicsLayerScope.() -> Unit)?
+        layerBlock: (GraphicsLayerScope.() -> Unit)?,
+        explicitLayer: GraphicsLayer?
     ) {
-        updateLayerBlock(layerBlock)
+        if (explicitLayer != null) {
+            requirePrecondition(layerBlock == null) {
+                "both ways to create layers shouldn't be used together"
+            }
+            if (this.explicitLayer !== explicitLayer) {
+                // reset previous layer object first if the explicitLayer changed
+                this.explicitLayer = null
+                updateLayerBlock(null)
+                this.explicitLayer = explicitLayer
+            }
+            if (layer == null) {
+                layer = layoutNode.requireOwner().createLayer(
+                    drawBlock,
+                    invalidateParentLayer,
+                    explicitLayer
+                ).apply {
+                    resize(measuredSize)
+                    move(position)
+                }
+                layoutNode.innerLayerCoordinatorIsDirty = true
+                invalidateParentLayer()
+            }
+        } else {
+            releaseExplicitLayer()
+            updateLayerBlock(layerBlock)
+        }
         if (this.position != position) {
             this.position = position
             layoutNode.layoutDelegate.measurePassDelegate
@@ -344,12 +390,20 @@
         }
     }
 
+    fun releaseExplicitLayer() {
+        if (explicitLayer != null) {
+            explicitLayer = null
+            updateLayerBlock(null)
+        }
+    }
+
     fun placeSelfApparentToRealOffset(
         position: IntOffset,
         zIndex: Float,
-        layerBlock: (GraphicsLayerScope.() -> Unit)?
+        layerBlock: (GraphicsLayerScope.() -> Unit)?,
+        layer: GraphicsLayer?
     ) {
-        placeSelf(position + apparentToRealOffset, zIndex, layerBlock)
+        placeSelf(position + apparentToRealOffset, zIndex, layerBlock, layer)
     }
 
     /**
@@ -358,9 +412,7 @@
     fun draw(canvas: Canvas, graphicsLayer: GraphicsLayer?) {
         val layer = layer
         if (layer != null) {
-            // todo graphicsLayer should be used as a parent layer here when we migrate
-            //  the local implementation to the new implementation.
-            layer.drawLayer(canvas)
+            layer.drawLayer(canvas, graphicsLayer)
         } else {
             val x = position.x.toFloat()
             val y = position.y.toFloat()
@@ -395,8 +447,7 @@
     private val drawBlock: (Canvas) -> Unit = { canvas ->
         if (layoutNode.isPlaced) {
             snapshotObserver.observeReads(this, onCommitAffectingLayer) {
-                // todo local layers will be passing the reference here when we migrate them.
-                drawContainedDrawModifiers(canvas, null)
+                drawContainedDrawModifiers(canvas, explicitLayer)
             }
             lastLayerDrawingWasSkipped = false
         } else {
@@ -411,14 +462,17 @@
         layerBlock: (GraphicsLayerScope.() -> Unit)?,
         forceUpdateLayerParameters: Boolean = false
     ) {
+        requirePrecondition(layerBlock == null || explicitLayer == null) {
+            "layerBlock can't be provided when explicitLayer is provided"
+        }
         val layoutNode = layoutNode
         val updateParameters = forceUpdateLayerParameters || this.layerBlock !== layerBlock ||
             layerDensity != layoutNode.density || layerLayoutDirection != layoutNode.layoutDirection
-        this.layerBlock = layerBlock
         this.layerDensity = layoutNode.density
         this.layerLayoutDirection = layoutNode.layoutDirection
 
         if (layoutNode.isAttached && layerBlock != null) {
+            this.layerBlock = layerBlock
             if (layer == null) {
                 layer = layoutNode.requireOwner().createLayer(
                     drawBlock,
@@ -434,6 +488,7 @@
                 updateLayerParameters()
             }
         } else {
+            this.layerBlock = null
             layer?.let {
                 it.destroy()
                 layoutNode.innerLayerCoordinatorIsDirty = true
@@ -448,6 +503,10 @@
     }
 
     private fun updateLayerParameters(invokeOnLayoutChange: Boolean = true) {
+        if (explicitLayer != null) {
+            // the parameters of the explicit layers are configured differently.
+            return
+        }
         val layer = layer
         if (layer != null) {
             val layerBlock = checkPreconditionNotNull(layerBlock) {
@@ -491,6 +550,8 @@
     var layer: OwnedLayer? = null
         private set
 
+    private var explicitLayer: GraphicsLayer? = null
+
     override val isValidOwnerScope: Boolean
         get() = layer != null && !released && layoutNode.isAttached
 
@@ -955,6 +1016,7 @@
      * released or when the [NodeCoordinator] is released (will not be used anymore).
      */
     fun onRelease() {
+        releaseExplicitLayer()
         released = true
         // It is important to call invalidateParentLayer() here, even though updateLayerBlock() may
         // call it. The reason is because we end up calling this from the bottom up, which means
@@ -1210,6 +1272,7 @@
             if (coordinator.isValidOwnerScope) {
                 // coordinator.layerPositionalProperties should always be non-null here, but
                 // we'll just be careful with a null check.
+                // todo how this will be communicated to us in the new impl?
                 val layerPositionalProperties = coordinator.layerPositionalProperties
                 if (layerPositionalProperties == null) {
                     coordinator.updateLayerParameters()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index f55d67f..e669d1e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -268,6 +268,9 @@
             coordinator.onRelease()
         }
     }
+    if (Nodes.LayoutAware in selfKindSet && node is LayoutAwareModifierNode) {
+        node.requireLayoutNode().invalidateMeasurements()
+    }
     if (Nodes.GlobalPositionAware in selfKindSet && node is GlobalPositionAwareModifierNode) {
         node.requireLayoutNode().invalidateOnPositioned()
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index a82996a..23fdbd4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -59,7 +60,7 @@
     /**
      * Causes the layer to be drawn into [canvas]
      */
-    fun drawLayer(canvas: Canvas)
+    fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?)
 
     /**
      * Updates the drawing on the current canvas.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index de231dd..dbc9f47 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.GraphicsContext
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.key.KeyEvent
@@ -254,7 +255,11 @@
     /**
      * Creates an [OwnedLayer] which will be drawing the passed [drawBlock].
      */
-    fun createLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit): OwnedLayer
+    fun createLayer(
+        drawBlock: (Canvas) -> Unit,
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer? = null
+    ): OwnedLayer
 
     /**
      * The semantics have changed. This function will be called when a SemanticsNode is added to
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index 07efa2e..bc03008 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -43,6 +43,7 @@
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.asComposeCanvas
 import androidx.compose.ui.graphics.layer.GraphicsContext
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.input.InputMode.Companion.Keyboard
 import androidx.compose.ui.input.InputModeManager
 import androidx.compose.ui.input.InputModeManagerImpl
@@ -376,7 +377,8 @@
 
     override fun createLayer(
         drawBlock: (Canvas) -> Unit,
-        invalidateParentLayer: () -> Unit
+        invalidateParentLayer: () -> Unit,
+        explicitLayer: GraphicsLayer?
     ) = SkiaLayer(
         density,
         invalidateParentLayer = {
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
index 18568a9..5f8b1e8 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.asComposeCanvas
 import androidx.compose.ui.graphics.asSkiaPath
+import androidx.compose.ui.graphics.layer.GraphicsLayer
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.graphics.toSkiaRRect
@@ -212,7 +213,7 @@
         }
     }
 
-    override fun drawLayer(canvas: Canvas) {
+    override fun drawLayer(canvas: Canvas, parentLayer: GraphicsLayer?) {
         if (picture == null) {
             val bounds = size.toSize().toRect()
             val pictureCanvas = pictureRecorder.beginRecording(bounds.toSkiaRect())
diff --git a/constraintlayout/constraintlayout/build.gradle b/constraintlayout/constraintlayout/build.gradle
index 7bafbac..93842ed 100644
--- a/constraintlayout/constraintlayout/build.gradle
+++ b/constraintlayout/constraintlayout/build.gradle
@@ -33,7 +33,7 @@
     implementation("androidx.appcompat:appcompat:1.2.0")
     implementation("androidx.core:core:1.3.2")
     implementation(project(":constraintlayout:constraintlayout-core"))
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     testImplementation(libs.junit)
 
diff --git a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
index e8e1bbf3..244b776 100644
--- a/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
+++ b/core/core/src/main/java/androidx/core/hardware/fingerprint/FingerprintManagerCompat.java
@@ -18,16 +18,11 @@
 
 import android.Manifest;
 import android.content.Context;
-import android.content.pm.PackageManager;
-import android.hardware.fingerprint.FingerprintManager;
-import android.os.Build;
 import android.os.CancellationSignal;
 import android.os.Handler;
 
-import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
 import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 
@@ -39,62 +34,53 @@
 /**
  * A class that coordinates access to the fingerprint hardware.
  * <p>
- * On platforms before {@link android.os.Build.VERSION_CODES#M}, this class behaves as there would
- * be no fingerprint hardware available.
+ * This class has been deprecated and should no longer be used. On all platform versions, it behaves
+ * as though no fingerprint hardware is available.
  *
- * @deprecated Use {@code androidx.biometrics.BiometricPrompt} instead.
+ * @deprecated {@code FingerprintManager} was removed from the platform SDK in Android V, use
+ * {@code androidx.biometrics.BiometricPrompt} instead.
  */
 @SuppressWarnings({"deprecation", "unused"})
 @Deprecated
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 public class FingerprintManagerCompat {
 
-    private final Context mContext;
-
     /** Get a {@link FingerprintManagerCompat} instance for a provided context. */
     @NonNull
     public static FingerprintManagerCompat from(@NonNull Context context) {
-        return new FingerprintManagerCompat(context);
+        return new FingerprintManagerCompat();
     }
 
-    private FingerprintManagerCompat(Context context) {
-        mContext = context;
+    private FingerprintManagerCompat() {
     }
 
     /**
-     * Determine if there is at least one fingerprint enrolled.
+     * Prior to deprecation, this method would determine if there is at least one fingerprint
+     * enrolled.
      *
-     * @return true if at least one fingerprint is enrolled, false otherwise
+     * @return false
      */
     @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
     public boolean hasEnrolledFingerprints() {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            return (fp != null) && Api23Impl.hasEnrolledFingerprints(fp);
-        } else {
-            return false;
-        }
+        return false;
     }
 
     /**
-     * Determine if fingerprint hardware is present and functional.
+     * Prior to deprecation, this method would determine if fingerprint hardware is present and
+     * functional.
      *
-     * @return true if hardware is present and functional, false otherwise.
+     * @return false
      */
     @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
     public boolean isHardwareDetected() {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            return (fp != null) && Api23Impl.isHardwareDetected(fp);
-        } else {
-            return false;
-        }
+        return false;
     }
 
     /**
-     * Request authentication of a crypto object. This call warms up the fingerprint hardware
-     * and starts scanning for a fingerprint. It terminates when
-     * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+     * Prior to deprecation, this method would request authentication of a crypto object.
+     * <p>
+     * This call warms up the fingerprint hardware and starts scanning for a fingerprint. It
+     * terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
      * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
      * provided cancel object.
@@ -114,15 +100,14 @@
             @Nullable androidx.core.os.CancellationSignal cancel,
             @NonNull AuthenticationCallback callback,
             @Nullable Handler handler) {
-        authenticate(crypto, flags,
-                cancel != null ? (CancellationSignal) cancel.getCancellationSignalObject() : null,
-                callback, handler);
+        // No-op.
     }
 
     /**
-     * Request authentication of a crypto object. This call warms up the fingerprint hardware
-     * and starts scanning for a fingerprint. It terminates when
-     * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
+     * Prior to deprecation, this method would request authentication of a crypto object.
+     * <p>
+     * This call warms up the fingerprint hardware and starts scanning for a fingerprint.
+     * It terminates when {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} or
      * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called, at
      * which point the object is no longer valid. The operation can be canceled by using the
      * provided cancel object.
@@ -137,56 +122,7 @@
     public void authenticate(@Nullable CryptoObject crypto, int flags,
             @Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
             @Nullable Handler handler) {
-        if (Build.VERSION.SDK_INT >= 23) {
-            final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
-            if (fp != null) {
-                Api23Impl.authenticate(fp, wrapCryptoObject(crypto), cancel, flags,
-                        wrapCallback(callback), handler);
-            }
-        }
-    }
-
-    @Nullable
-    @RequiresApi(23)
-    private static FingerprintManager getFingerprintManagerOrNull(@NonNull Context context) {
-        return Api23Impl.getFingerprintManagerOrNull(context);
-    }
-
-    @RequiresApi(23)
-    private static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
-        return Api23Impl.wrapCryptoObject(cryptoObject);
-    }
-
-    @RequiresApi(23)
-    static CryptoObject unwrapCryptoObject(FingerprintManager.CryptoObject cryptoObject) {
-        return Api23Impl.unwrapCryptoObject(cryptoObject);
-    }
-
-    @RequiresApi(23)
-    private static FingerprintManager.AuthenticationCallback wrapCallback(
-            final AuthenticationCallback callback) {
-        return new FingerprintManager.AuthenticationCallback() {
-            @Override
-            public void onAuthenticationError(int errMsgId, CharSequence errString) {
-                callback.onAuthenticationError(errMsgId, errString);
-            }
-
-            @Override
-            public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
-                callback.onAuthenticationHelp(helpMsgId, helpString);
-            }
-
-            @Override
-            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
-                callback.onAuthenticationSucceeded(new AuthenticationResult(
-                        unwrapCryptoObject(Api23Impl.getCryptoObject(result))));
-            }
-
-            @Override
-            public void onAuthenticationFailed() {
-                callback.onAuthenticationFailed();
-            }
-        };
+        // No-op.
     }
 
     /**
@@ -296,82 +232,4 @@
          */
         public void onAuthenticationFailed() { }
     }
-
-    @RequiresApi(23)
-    static class Api23Impl {
-        private Api23Impl() {
-            // This class is not instantiable.
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static boolean hasEnrolledFingerprints(Object fingerprintManager) {
-            return ((FingerprintManager) fingerprintManager).hasEnrolledFingerprints();
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static boolean isHardwareDetected(Object fingerprintManager) {
-            return ((FingerprintManager) fingerprintManager).isHardwareDetected();
-        }
-
-        @RequiresPermission(Manifest.permission.USE_FINGERPRINT)
-        @DoNotInline
-        static void authenticate(Object fingerprintManager, Object crypto,
-                CancellationSignal cancel, int flags, Object callback, Handler handler) {
-            ((FingerprintManager) fingerprintManager).authenticate(
-                    (FingerprintManager.CryptoObject) crypto, cancel, flags,
-                    (FingerprintManager.AuthenticationCallback) callback, handler);
-        }
-
-        @DoNotInline
-        static FingerprintManager.CryptoObject getCryptoObject(Object authenticationResult) {
-            return ((FingerprintManager.AuthenticationResult) authenticationResult)
-                    .getCryptoObject();
-        }
-
-        @DoNotInline
-        public static FingerprintManager getFingerprintManagerOrNull(Context context) {
-            if (Build.VERSION.SDK_INT == 23) {
-                return context.getSystemService(FingerprintManager.class);
-            } else if (Build.VERSION.SDK_INT > 23 && context.getPackageManager()
-                    .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
-                return context.getSystemService(FingerprintManager.class);
-            } else {
-                return null;
-            }
-        }
-
-        @DoNotInline
-        public static FingerprintManager.CryptoObject wrapCryptoObject(CryptoObject cryptoObject) {
-            if (cryptoObject == null) {
-                return null;
-            } else if (cryptoObject.getCipher() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getCipher());
-            } else if (cryptoObject.getSignature() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getSignature());
-            } else if (cryptoObject.getMac() != null) {
-                return new FingerprintManager.CryptoObject(cryptoObject.getMac());
-            } else {
-                return null;
-            }
-        }
-
-        @DoNotInline
-        public static CryptoObject unwrapCryptoObject(Object cryptoObjectObj) {
-            FingerprintManager.CryptoObject cryptoObject =
-                    (FingerprintManager.CryptoObject) cryptoObjectObj;
-            if (cryptoObject == null) {
-                return null;
-            } else if (cryptoObject.getCipher() != null) {
-                return new CryptoObject(cryptoObject.getCipher());
-            } else if (cryptoObject.getSignature() != null) {
-                return new CryptoObject(cryptoObject.getSignature());
-            } else if (cryptoObject.getMac() != null) {
-                return new CryptoObject(cryptoObject.getMac());
-            } else {
-                return null;
-            }
-        }
-    }
 }
diff --git a/development/bench-flame-diff/README.md b/development/bench-flame-diff/README.md
index 87977c0..cfc9889 100644
--- a/development/bench-flame-diff/README.md
+++ b/development/bench-flame-diff/README.md
@@ -56,7 +56,9 @@
 
 ## CLI completion
 
-Generate completion files with `./generate-completion-files.sh` and source in your shell config, e.g.:
+Generate shell-specific completion files with `./generate-completion.sh`.
+
+Then, source in your shell config, e.g.:
 - For `bash`: `dst="$(pwd)/completion_bash.sh"; echo "source '$dst'" >> ~/.bashrc`
 - For `zsh`: `dst="$(pwd)/completion_zsh.sh"; echo "source '$dst'" >> ~/.zshrc`
 
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 25050759..42c5a91 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -41,7 +41,7 @@
     api("androidx.lifecycle:lifecycle-livedata-core:2.6.1")
     api("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
     api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     api("androidx.savedstate:savedstate:1.2.1")
     api("androidx.annotation:annotation-experimental:1.4.0")
     api(libs.kotlinStdlib)
diff --git a/lifecycle/lifecycle-runtime/build.gradle b/lifecycle/lifecycle-runtime/build.gradle
index f16a328..bcb9ce0 100644
--- a/lifecycle/lifecycle-runtime/build.gradle
+++ b/lifecycle/lifecycle-runtime/build.gradle
@@ -61,7 +61,7 @@
             dependencies {
                 api(libs.kotlinCoroutinesAndroid)
                 implementation("androidx.arch.core:core-runtime:2.2.0")
-                implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+                implementation("androidx.profileinstaller:profileinstaller:1.3.1")
             }
         }
 
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
index e5ee149..1ead559 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInAppCompatActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.compose.setContent
 import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import java.util.concurrent.CountDownLatch
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
index 50f06f9..fb0e225 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelInComponentActivityTest.kt
@@ -19,8 +19,8 @@
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
index 3b02e84..4f8159f 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidTest/java/androidx/lifecycle/viewmodel/compose/ViewModelTest.kt
@@ -21,7 +21,6 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -30,6 +29,7 @@
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStore
 import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
 import androidx.lifecycle.viewmodel.CreationExtras
 import androidx.lifecycle.viewmodel.MutableCreationExtras
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index b5e5e8d..c513438 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -49,7 +49,7 @@
     api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.2")
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     implementation(libs.kotlinSerializationCore)
 
     api(libs.kotlinStdlib)
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
index 47f7464..396288f 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphBuilderTest.kt
@@ -20,7 +20,10 @@
 import androidx.navigation.serialization.generateRoutePattern
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.reflect.KClass
+import kotlin.test.assertFailsWith
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.serializer
 import org.junit.Assert.fail
@@ -156,6 +159,82 @@
             .isTrue()
     }
 
+    @Test fun navigationStartDestinationKClass() {
+        @Serializable
+        class Graph(val arg: Int)
+
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = provider.navigation(
+            route = Graph::class,
+            startDestination = TestClass::class
+        ) {
+            navDestination(TestClass::class) { }
+        }
+
+        // assert graph info
+        val expectedGraphRoute = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationStartDestinationKClass.Graph/{arg}"
+        assertWithMessage("graph route should be set")
+            .that(graph.route)
+            .isEqualTo(expectedGraphRoute)
+        assertWithMessage("graph id should be set")
+            .that(graph.id)
+            .isEqualTo(serializer<Graph>().hashCode())
+
+        // assert start destination info
+        val expectedStartRoute = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationStartDestinationKClass.TestClass/{arg}"
+        assertWithMessage("Destination route should be added to the graph")
+            .that(expectedStartRoute in graph)
+            .isTrue()
+        assertWithMessage("startDestinationRoute should be set")
+            .that(graph.startDestinationRoute)
+            .isEqualTo(expectedStartRoute)
+        assertWithMessage("startDestinationId should be set")
+            .that(graph.startDestinationId)
+            .isEqualTo(serializer<TestClass>().hashCode())
+    }
+
+    @Test fun navigationStartDestinationObject() {
+        @Serializable
+        class Graph(val arg: Int)
+
+        @Serializable
+        class TestClass(val arg2: Int)
+
+        val graph = provider.navigation(
+            route = Graph::class,
+            startDestination = TestClass(1)
+        ) {
+            navDestination(TestClass::class) { }
+        }
+
+        // assert graph info
+        val expectedGraphRoute = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationStartDestinationObject.Graph/{arg}"
+        assertWithMessage("graph route should be set")
+            .that(graph.route)
+            .isEqualTo(expectedGraphRoute)
+        assertWithMessage("graph id should be set")
+            .that(graph.id)
+            .isEqualTo(serializer<Graph>().hashCode())
+
+        // assert start destination info
+        val expectedStartRoute = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationStartDestinationObject.TestClass/1"
+        assertWithMessage("Destination route should be added to the graph")
+            .that(expectedStartRoute in graph)
+            .isTrue()
+        assertWithMessage("startDestinationRoute should be set")
+            .that(graph.startDestinationRoute)
+            .isEqualTo(expectedStartRoute)
+        assertWithMessage("startDestinationId should be set")
+            .that(graph.startDestinationId)
+            .isEqualTo(serializer<TestClass>().hashCode())
+    }
+
     @Suppress("DEPRECATION")
     @Test(expected = IllegalStateException::class)
     fun navigationMissingStartDestination() {
@@ -173,6 +252,32 @@
         fail("NavGraph should throw IllegalStateException if no startDestinationRoute is set")
     }
 
+    @Test
+    fun navigationMissingStartDestinationKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        assertFailsWith<IllegalStateException> {
+            provider.navigation(startDestination = TestClass::class) {
+                // nav destination must have been added via route from KClass
+                navDestination("route") { }
+            }
+        }
+    }
+
+    @Test
+    fun navigationMissingStartDestinationObject() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        assertFailsWith<IllegalStateException> {
+            provider.navigation(startDestination = TestClass(0)) {
+                // nav destination must have been added via route from KClass
+                navDestination("route") { }
+            }
+        }
+    }
+
     @Suppress("DEPRECATION")
     @Test
     fun navigationNested() {
@@ -197,6 +302,109 @@
             .that(DESTINATION_ROUTE in graph)
             .isTrue()
     }
+
+    @Test
+    fun navigationNestedKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        @Serializable
+        class NestedGraph(val arg: Int)
+
+        val graph = provider.navigation(startDestination = NestedGraph::class) {
+            navigation(startDestination = TestClass::class, route = NestedGraph::class) {
+                navDestination(TestClass::class) {}
+            }
+        }
+        val nestedGraph = graph.findNode(
+            serializer<NestedGraph>().generateRoutePattern()
+        ) as NavGraph
+        // assert graph
+        val expectedNestedGraph = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedKClass.NestedGraph/{arg}"
+        assertThat(nestedGraph.route).isEqualTo(expectedNestedGraph)
+        assertThat(nestedGraph.id).isEqualTo(serializer<NestedGraph>().hashCode())
+        // assert nested startDestination
+        val expectedNestedStart = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedKClass.TestClass/{arg}"
+        assertThat(nestedGraph.startDestinationRoute).isEqualTo(expectedNestedStart)
+        assertThat(nestedGraph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
+        assertWithMessage("Destination should be added to the nested graph")
+            .that(expectedNestedStart in nestedGraph)
+            .isTrue()
+    }
+
+    @Test
+    fun navigationNestedObject() {
+        @Serializable
+        class TestClass(val arg2: Int)
+
+        @Serializable
+        class NestedGraph(val arg: Int)
+
+        val graph = provider.navigation(startDestination = NestedGraph::class) {
+            navigation(startDestination = TestClass(15), route = NestedGraph::class) {
+                navDestination(TestClass::class) {}
+            }
+        }
+        val nestedGraph = graph.findNode(
+            serializer<NestedGraph>().generateRoutePattern()
+        ) as NavGraph
+
+        // assert graph
+        val expectedNestedGraph = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedObject.NestedGraph/{arg}"
+        assertThat(nestedGraph.route).isEqualTo(expectedNestedGraph)
+        assertThat(nestedGraph.id).isEqualTo(serializer<NestedGraph>().hashCode())
+
+        // assert nested StartDestination
+        val expectedNestedStart = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedObject.TestClass/15"
+        assertThat(nestedGraph.startDestinationRoute).isEqualTo(expectedNestedStart)
+        assertThat(nestedGraph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
+        assertWithMessage("Destination should be added to the nested graph")
+            .that(expectedNestedStart in nestedGraph)
+            .isTrue()
+    }
+
+    @Test
+    fun navigationNestedObjectAndKClass() {
+        @Serializable
+        class TestClass(val arg2: Int)
+
+        @Serializable
+        class NestedGraph(val arg: Int)
+
+        val graph = provider.navigation(startDestination = NestedGraph(0)) {
+            navigation(startDestination = TestClass(15), route = NestedGraph::class) {
+                navDestination(TestClass::class) {}
+            }
+        }
+        val nestedGraph = graph.findNode(
+            serializer<NestedGraph>().generateRoutePattern()
+        ) as NavGraph
+
+        // assert graph
+        val expectedStart = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedObjectAndKClass.NestedGraph/0"
+        assertThat(graph.startDestinationRoute).isEqualTo(expectedStart)
+        assertThat(graph.startDestinationId).isEqualTo(serializer<NestedGraph>().hashCode())
+
+        // assert nested graph
+        val expectedNestedGraph = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedObjectAndKClass.NestedGraph/{arg}"
+        assertThat(nestedGraph.route).isEqualTo(expectedNestedGraph)
+        assertThat(nestedGraph.id).isEqualTo(serializer<NestedGraph>().hashCode())
+
+        // assert nested StartDestination
+        val expectedNestedStart = "androidx.navigation.NavGraphBuilderTest." +
+            "navigationNestedObjectAndKClass.TestClass/15"
+        assertThat(nestedGraph.startDestinationRoute).isEqualTo(expectedNestedStart)
+        assertThat(nestedGraph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
+        assertWithMessage("Destination should be added to the nested graph")
+            .that(expectedNestedStart in nestedGraph)
+            .isTrue()
+    }
 }
 
 private const val DESTINATION_ID = 1
@@ -222,3 +430,12 @@
     route: String,
     builder: NavDestinationBuilder<NavDestination>.() -> Unit
 ) = destination(NavDestinationBuilder(provider[NoOpNavigator::class], route).apply(builder))
+
+/**
+ * Create a base NavDestination. Generally, only subtypes of NavDestination should be
+ * added to a NavGraph (hence why this is not in the common-ktx library)
+ */
+fun NavGraphBuilder.navDestination(
+    route: KClass<*>,
+    builder: NavDestinationBuilder<NavDestination>.() -> Unit
+) = destination(NavDestinationBuilder(provider[NoOpNavigator::class], route).apply(builder))
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
index fafd8c0..b33cd9b 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavGraphTest.kt
@@ -137,7 +137,7 @@
     }
 
     @Test
-    fun graphSetStartDestinationRoute() {
+    fun graphSetStartDestinationKClass() {
         @Serializable
         @SerialName("route")
         class TestClass(val arg: Int)
@@ -154,7 +154,7 @@
     }
 
     @Test
-    fun graphSetStartDestinationRouteMissingStartDestination() {
+    fun graphSetStartDestinationKClassMissingStartDestination() {
         @Serializable
         class TestClass
 
@@ -165,6 +165,110 @@
             graph.setStartDestination(TestClass::class)
         }
     }
+
+    @Test
+    fun graphSetStartDestinationObject() {
+        @Serializable
+        @SerialName("route")
+        class TestClass(val arg: Int, val arg2: String? = "test")
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            setStartDestination(15)
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+        assertThat(graph.startDestinationId).isEqualTo(15)
+
+        graph.setStartDestination(TestClass(20))
+        assertThat(graph.startDestinationRoute).isEqualTo("route/20?arg2=test")
+        assertThat(graph.startDestinationId).isEqualTo(serializer<TestClass>().hashCode())
+    }
+
+    @Test
+    fun graphSetStartDestinationObjectMissingStartDestination() {
+        @Serializable
+        class TestClass
+
+        val graph = NavGraph(navGraphNavigator)
+
+        // start destination not added via KClass, cannot match
+        assertFailsWith<IllegalStateException> {
+            graph.setStartDestination(TestClass())
+        }
+    }
+
+    @Test
+    fun findNodeKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        val dest = graph.findNode(TestClass::class)
+        assertThat(dest).isNotNull()
+    }
+
+    @Test
+    fun getNodeKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        assertThat(graph[TestClass::class]).isNotNull()
+    }
+
+    @Test
+    fun containNodeKClass() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        assertThat(graph.contains(TestClass::class)).isTrue()
+    }
+
+    @Test
+    fun findNodeObject() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        val dest = graph.findNode(TestClass(15))
+        assertThat(dest).isNotNull()
+    }
+
+    @Test
+    fun getNodeObject() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        assertThat(graph[TestClass(15)]).isNotNull()
+    }
+
+    @Test
+    fun containNodeObject() {
+        @Serializable
+        class TestClass(val arg: Int)
+
+        val graph = NavGraph(navGraphNavigator).apply {
+            addDestination(NavDestinationBuilder(navGraphNavigator, TestClass::class).build())
+        }
+
+        assertThat(graph.contains(TestClass(15))).isTrue()
+    }
 }
 
 private const val DESTINATION_ID = 1
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
index f008ff3..553e1a0 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/serialization/RouteFilledTest.kt
@@ -31,7 +31,6 @@
 import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.serializer
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -47,10 +46,8 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass
 
-        val serializer = serializer<TestClass>()
-
         val clazz = TestClass()
-        assertThatRouteFilledFrom(clazz, serializer).isEqualTo(PATH_SERIAL_NAME)
+        assertThatRouteFilledFrom(clazz).isEqualTo(PATH_SERIAL_NAME)
     }
 
     @Test
@@ -59,12 +56,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String)
 
-        val serializer = serializer<TestClass>()
-
         val clazz = TestClass("test")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg"))
         ).isEqualTo("$PATH_SERIAL_NAME/test")
     }
@@ -75,12 +69,10 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String, val arg2: Int)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test", 0)
 
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg"), intArgument("arg2"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/test/0"
@@ -93,11 +85,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String?)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/test"
@@ -110,11 +100,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String?)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(null)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/null"
@@ -130,7 +118,6 @@
         val clazz = TestClass("null")
         assertThatRouteFilledFrom(
             clazz,
-            serializer<TestClass>(),
             listOf(nullableStringArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/null"
@@ -143,11 +130,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String?, val arg2: Int?)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test", 0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg"), nullableIntArgument("arg2"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/test/0"
@@ -160,11 +145,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String?, val arg2: Int?)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(null, null)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg"), nullableIntArgument("arg2"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/null/null"
@@ -177,11 +160,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String = "test")
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg", true))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=test"
@@ -194,11 +175,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String = "test")
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("newTest")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg", true))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=newTest"
@@ -211,11 +190,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String? = "test")
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg", true))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=test"
@@ -228,11 +205,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String? = null)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(nullableStringArgument("arg", true))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=null"
@@ -248,7 +223,6 @@
         val clazz = TestClass("null")
         assertThatRouteFilledFrom(
             clazz,
-            serializer<TestClass>(),
             listOf(nullableStringArgument("arg", true))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=null"
@@ -261,11 +235,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String? = "test", val arg2: Int? = 0)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 nullableStringArgument("arg", true),
                 nullableIntArgument("arg2", true)
@@ -281,11 +253,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: String? = null, val arg2: Int? = null)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 nullableStringArgument("arg", true),
                 nullableIntArgument("arg2", true)
@@ -301,11 +271,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val pathArg: String, val queryArg: Int = 0)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 stringArgument("pathArg"),
                 intArgument("queryArg", true)
@@ -321,11 +289,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val queryArg: Int = 0, val pathArg: String)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(1, "test")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 intArgument("queryArg", true),
                 stringArgument("pathArg")
@@ -341,11 +307,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val pathArg: String?, val queryArg: Int? = 0)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test", 1)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 nullableStringArgument("pathArg"),
                 nullableIntArgument("queryArg", true)
@@ -361,11 +325,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val array: IntArray)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(intArrayOf(0, 1, 2))
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArrayArgument("array"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?array=0&array=1&array=2"
@@ -378,11 +340,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val array: IntArray?)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(intArrayOf(0, 1, 2))
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArrayArgument("array"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?array=0&array=1&array=2"
@@ -395,12 +355,10 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val array: IntArray? = null)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
-            listOf(intArrayArgument("array"),)
+            listOf(intArrayArgument("array"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?array=null"
         )
@@ -412,11 +370,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val string: String, val array: IntArray)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test", intArrayOf(0, 1, 2))
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 stringArgument("string"),
                 intArrayArgument("array")
@@ -432,11 +388,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val array: IntArray, val arg: Int = 0)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(intArrayOf(0, 1, 2), 15)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(
                 intArrayArgument("array"),
                 intArgument("arg")
@@ -454,11 +408,9 @@
             constructor(arg2: Int) : this(arg2.toString())
         }
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0"
@@ -467,11 +419,9 @@
 
     @Test
     fun withCompanionObject() {
-        val serializer = serializer<ClassWithCompanionObject>()
         val clazz = ClassWithCompanionObject(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0"
@@ -480,11 +430,9 @@
 
     @Test
     fun withCompanionParameter() {
-        val serializer = serializer<ClassWithCompanionParam>()
         val clazz = ClassWithCompanionParam(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0"
@@ -499,11 +447,9 @@
             fun testFun() { }
         }
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass("test")
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(stringArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/test"
@@ -530,11 +476,9 @@
             unknownDefaultValuePresent = false
         }
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(CustomType())
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(customArg)
         ).isEqualTo(
             "$PATH_SERIAL_NAME/customValue"
@@ -564,11 +508,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val custom: CustomType)
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(CustomType(NestedCustomType()))
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(customArg)
         ).isEqualTo(
             "$PATH_SERIAL_NAME/customValue[nestedCustomValue]"
@@ -596,11 +538,9 @@
             nullable = false
             unknownDefaultValuePresent = false
         }
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(0, CustomSerializerClass(1L))
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"), customArg)
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0/customSerializerClass[1]"
@@ -615,14 +555,12 @@
             val noBackingField: Int
                 get() = 0
         }
-        val serializer = serializer<TestClass>()
         // only members with backing field should appear on route
         val clazz = TestClass()
         assertThatRouteFilledFrom(
-            clazz,
-            serializer
+            clazz
         ).isEqualTo(
-            "$PATH_SERIAL_NAME"
+            PATH_SERIAL_NAME
         )
     }
 
@@ -633,11 +571,9 @@
         class TestClass {
             val arg: Int = 0
         }
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=0"
@@ -651,11 +587,9 @@
         class TestClass {
             lateinit var arg: IntArray
         }
-        val serializer = serializer<TestClass>()
         val clazz = TestClass().also { it.arg = intArrayOf(0) }
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArrayArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME?arg=0"
@@ -669,7 +603,7 @@
 
         assertFailsWith<SerializationException> {
             // the class must be serializable
-            serializer<TestClass>().generateRouteWithArgs(TestClass(), emptyMap())
+            TestClass().generateRouteWithArgs(emptyMap<String, NavType<Any?>>())
         }
     }
 
@@ -682,11 +616,11 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass : TestAbstractClass()
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass()
-        assertThatRouteFilledFrom(clazz, serializer,
+        assertThatRouteFilledFrom(
+            clazz,
         ).isEqualTo(
-            "$PATH_SERIAL_NAME"
+            PATH_SERIAL_NAME
         )
     }
 
@@ -699,12 +633,10 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg2: Int) : TestAbstractClass(arg2)
 
-        val serializer = serializer<TestClass>()
         // args will be duplicated
         val clazz = TestClass(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"), intArgument("arg2"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0/0"
@@ -713,12 +645,10 @@
 
     @Test
     fun childClassOfSealed_withArgs() {
-        val serializer = serializer<SealedClass.TestClass>()
         // child class overrides parent variable so only child variable shows up in route pattern
         val clazz = SealedClass.TestClass(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg2"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0"
@@ -731,11 +661,9 @@
         @SerialName(PATH_SERIAL_NAME)
         class TestClass(val arg: Int) : TestInterface
 
-        val serializer = serializer<TestClass>()
         val clazz = TestClass(0)
         assertThatRouteFilledFrom(
             clazz,
-            serializer,
             listOf(intArgument("arg"))
         ).isEqualTo(
             "$PATH_SERIAL_NAME/0"
@@ -744,30 +672,27 @@
 
     @Test
     fun routeFromObject() {
-        val serializer = serializer<TestObject>()
-        assertThatRouteFilledFrom(TestObject, serializer).isEqualTo(
-            "$PATH_SERIAL_NAME"
+        assertThatRouteFilledFrom(TestObject).isEqualTo(
+            PATH_SERIAL_NAME
         )
     }
 
     @Test
     fun routeFromObject_argsNotSerialized() {
-        val serializer = serializer<TestObjectWithArg>()
         // object variables are not serialized and does not show up on route
-        assertThatRouteFilledFrom(TestObjectWithArg, serializer).isEqualTo(
-            "$PATH_SERIAL_NAME"
+        assertThatRouteFilledFrom(TestObjectWithArg).isEqualTo(
+            PATH_SERIAL_NAME
         )
     }
 }
 
 private fun <T : Any> assertThatRouteFilledFrom(
     obj: T,
-    serializer: KSerializer<T>,
     customArgs: List<NamedNavArgument>? = null
 ): String {
     val typeMap = mutableMapOf<String, NavType<Any?>>()
     customArgs?.forEach { typeMap[it.name] = it.argument.type }
-    return serializer.generateRouteWithArgs(obj, typeMap)
+    return obj.generateRouteWithArgs(typeMap)
 }
 
 internal fun String.isEqualTo(other: String) {
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
index 2c4329e..61da16f 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestinationBuilder.kt
@@ -94,18 +94,18 @@
     @OptIn(InternalSerializationApi::class)
     public constructor(
         navigator: Navigator<out D>,
-        route: KClass<*>,
-        typeMap: Map<KType, NavType<*>> = mapOf(),
+        route: KClass<*>? = null,
+        typeMap: Map<KType, NavType<*>>? = null,
     ) : this(
         navigator,
-        route.serializer().hashCode(),
-        route.serializer().generateRoutePattern(typeMap.ifEmpty { null })
+        route?.serializer()?.hashCode() ?: -1,
+        route?.serializer()?.generateRoutePattern(typeMap)
     ) {
-        route.serializer()
-            .generateNavArguments(typeMap)
-            .forEach {
+        route?.apply {
+            serializer().generateNavArguments(typeMap).forEach {
                 arguments[it.name] = it.argument
             }
+        }
     }
 
     /**
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
index b241168..7f6ddf4 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraph.kt
@@ -25,9 +25,12 @@
 import androidx.collection.valueIterator
 import androidx.core.content.res.use
 import androidx.navigation.common.R
+import androidx.navigation.serialization.generateRouteWithArgs
 import java.lang.StringBuilder
 import kotlin.reflect.KClass
+import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
 import kotlinx.serialization.serializer
 
 /**
@@ -181,6 +184,31 @@
         return if (!route.isNullOrBlank()) findNode(route, true) else null
     }
 
+    /**
+     * Finds a destination in the collection by route from [KClass]. This will recursively check the
+     * [parent][parent] of this navigation graph if node is not found in this navigation graph.
+     *
+     * @param route Route to locate
+     * @return the node with route - the node must have been created with a route from [KClass]
+     */
+    @OptIn(InternalSerializationApi::class)
+    internal fun <T : Any> findNode(route: KClass<T>?): NavDestination? {
+        return if (route != null) findNode(route.serializer().hashCode()) else null
+    }
+
+    /**
+     * Finds a destination in the collection by route from Object. This will recursively check the
+     * [parent][parent] of this navigation graph if node is not found in this navigation graph.
+     *
+     * @param route Route to locate
+     * @return the node with route - the node must have been created with a route from [KClass]
+     */
+    @OptIn(InternalSerializationApi::class)
+    @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
+    internal fun <T> findNode(route: T?): NavDestination? {
+        return if (route != null) findNode(route!!::class.serializer().hashCode()) else null
+    }
+
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public fun findNode(@IdRes resId: Int, searchParents: Boolean): NavDestination? {
         val destination = nodes[resId]
@@ -342,16 +370,44 @@
     @OptIn(InternalSerializationApi::class)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public fun setStartDestination(startDestRoute: KClass<*>) {
-        val serializer = startDestRoute.serializer()
+        setStartDestination(startDestRoute.serializer()) { startDestination ->
+            startDestination.route!!
+        }
+    }
+
+    /**
+     * Sets the starting destination for this NavGraph.
+     *
+     * This will override any previously set [startDestinationId]
+     *
+     * @param startDestRoute The route of the destination as an object to be shown when navigating
+     * to this NavGraph.
+     */
+    @OptIn(InternalSerializationApi::class)
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public fun <T : Any> setStartDestination(startObject: T) {
+        setStartDestination(startObject::class.serializer()) { startDestination ->
+            val args = startDestination.arguments.mapValues {
+                it.value.type
+            }
+            startObject.generateRouteWithArgs(args)
+        }
+    }
+
+    @OptIn(ExperimentalSerializationApi::class)
+    private fun <T> setStartDestination(
+        serializer: KSerializer<T>,
+        parseRoute: (NavDestination) -> String,
+    ) {
         val id = serializer.hashCode()
         val startDest = findNode(id)
         checkNotNull(startDest) {
-            "Cannot find startDestination $startDestRoute from NavGraph. Ensure the starting " +
-                "NavDestination was added via KClass."
+            "Cannot find startDestination ${serializer.descriptor.serialName} from NavGraph. " +
+                "Ensure the starting NavDestination was added via KClass."
         }
         // when dest id is based on serializer, we expect the dest route to have been generated
         // and set
-        startDestinationRoute = startDest.route!!
+        startDestinationRoute = parseRoute(startDest)
         // bypass startDestinationId setter so we don't set route back to null
         this.startDestId = id
     }
@@ -461,12 +517,39 @@
     findNode(route)
         ?: throw IllegalArgumentException("No destination for $route was found in $this")
 
+/**
+ * Returns the destination with `route` from [KClass].
+ *
+ * @throws IllegalArgumentException if no destination is found with that route.
+ */
+@Suppress("NOTHING_TO_INLINE")
+internal inline operator fun <T : Any> NavGraph.get(route: KClass<T>): NavDestination =
+    findNode(route)
+        ?: throw IllegalArgumentException("No destination for $route was found in $this")
+
+/**
+ * Returns the destination with `route` from an Object.
+ *
+ * @throws IllegalArgumentException if no destination is found with that route.
+ */
+@Suppress("NOTHING_TO_INLINE")
+internal inline operator fun <T> NavGraph.get(route: T): NavDestination =
+    findNode(route)
+        ?: throw IllegalArgumentException("No destination for $route was found in $this")
+
 /** Returns `true` if a destination with `id` is found in this navigation graph. */
 public operator fun NavGraph.contains(@IdRes id: Int): Boolean = findNode(id) != null
 
 /** Returns `true` if a destination with `route` is found in this navigation graph. */
 public operator fun NavGraph.contains(route: String): Boolean = findNode(route) != null
 
+/** Returns `true` if a destination with `route` is found in this navigation graph. */
+internal operator fun <T : Any> NavGraph.contains(route: KClass<T>): Boolean =
+    findNode(route) != null
+
+/** Returns `true` if a destination with `route` is found in this navigation graph. */
+internal operator fun <T> NavGraph.contains(route: T): Boolean = findNode(route) != null
+
 /**
  * Adds a destination to this NavGraph. The destination must have an
  * [id][NavDestination.id] set.
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
index d69f5d2..6e42aff 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavGraphBuilder.kt
@@ -17,6 +17,10 @@
 package androidx.navigation
 
 import androidx.annotation.IdRes
+import androidx.annotation.RestrictTo
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlinx.serialization.InternalSerializationApi
 
 /**
  * Construct a new [NavGraph]
@@ -58,6 +62,46 @@
     .build()
 
 /**
+ * Construct a new [NavGraph]
+ *
+ * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavigatorProvider.navigation(
+    startDestination: KClass<*>,
+    route: KClass<*>? = null,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): NavGraph = NavGraphBuilder(this, startDestination, route, typeMap).apply(builder)
+    .build()
+
+/**
+ * Construct a new [NavGraph]
+ *
+ * @param startDestination the starting destination's route as an Object for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavigatorProvider.navigation(
+    startDestination: Any,
+    route: KClass<*>? = null,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): NavGraph = NavGraphBuilder(this, startDestination, route, typeMap).apply(builder)
+    .build()
+
+/**
  * Construct a nested [NavGraph]
  *
  * @param id the destination's unique id
@@ -96,6 +140,44 @@
 ): Unit = destination(NavGraphBuilder(provider, startDestination, route).apply(builder))
 
 /**
+ * Construct a nested [NavGraph]
+ *
+ * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed nested NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavGraphBuilder.navigation(
+    startDestination: KClass<*>,
+    route: KClass<*>,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): Unit = destination(NavGraphBuilder(provider, startDestination, route, typeMap).apply(builder))
+
+/**
+ * Construct a nested [NavGraph]
+ *
+ * @param startDestination the starting destination's route as an Object for this NavGraph. The
+ * respective NavDestination must be added as a [KClass] in order to match.
+ * @param route the graph's unique route as a [KClass]
+ * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+ * if [route] uses custom NavTypes.
+ *
+ * @return the newly constructed nested NavGraph
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public inline fun NavGraphBuilder.navigation(
+    startDestination: Any,
+    route: KClass<*>,
+    typeMap: Map<KType, NavType<*>>? = null,
+    builder: NavGraphBuilder.() -> Unit
+): Unit = destination(NavGraphBuilder(provider, startDestination, route, typeMap).apply(builder))
+
+/**
  * DSL for constructing a new [NavGraph]
  */
 @NavDestinationDsl
@@ -106,6 +188,8 @@
     public val provider: NavigatorProvider
     @IdRes private var startDestinationId: Int = 0
     private var startDestinationRoute: String? = null
+    private var startDestinationClass: KClass<*>? = null
+    private var startDestinationObject: Any? = null
 
     /**
      * DSL for constructing a new [NavGraph]
@@ -151,6 +235,52 @@
         this.startDestinationRoute = startDestination
     }
 
+    /**
+     * DSL for constructing a new [NavGraph]
+     *
+     * @param provider navigator used to create the destination
+     * @param startDestination the starting destination's route as a [KClass] for this NavGraph. The
+     * respective NavDestination must be added as a [KClass] in order to match.
+     * @param route the graph's unique route as a [KClass]
+     * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+     * if [route] uses custom NavTypes.
+     *
+     * @return the newly created NavGraph
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public constructor(
+        provider: NavigatorProvider,
+        startDestination: KClass<*>,
+        route: KClass<*>?,
+        typeMap: Map<KType, NavType<*>>?
+    ) : super(provider[NavGraphNavigator::class], route, typeMap) {
+        this.provider = provider
+        this.startDestinationClass = startDestination
+    }
+
+    /**
+     * DSL for constructing a new [NavGraph]
+     *
+     * @param provider navigator used to create the destination
+     * @param startDestination the starting destination's route as an Object for this NavGraph. The
+     * respective NavDestination must be added as a [KClass] in order to match.
+     * @param route the graph's unique route as a [KClass]
+     * @param typeMap A mapping of KType to custom NavType<*> in the [route]. Only necessary
+     * if [route] uses custom NavTypes.
+     *
+     * @return the newly created NavGraph
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public constructor(
+        provider: NavigatorProvider,
+        startDestination: Any,
+        route: KClass<*>?,
+        typeMap: Map<KType, NavType<*>>?
+    ) : super(provider[NavGraphNavigator::class], route, typeMap) {
+            this.provider = provider
+            this.startDestinationObject = startDestination
+        }
+
     private val destinations = mutableListOf<NavDestination>()
 
     /**
@@ -174,9 +304,11 @@
         destinations += destination
     }
 
+    @OptIn(InternalSerializationApi::class)
     override fun build(): NavGraph = super.build().also { navGraph ->
         navGraph.addDestinations(destinations)
-        if (startDestinationId == 0 && startDestinationRoute == null) {
+        if (startDestinationId == 0 && startDestinationRoute == null &&
+            startDestinationClass == null && startDestinationObject == null) {
             if (route != null) {
                 throw IllegalStateException("You must set a start destination route")
             } else {
@@ -185,6 +317,10 @@
         }
         if (startDestinationRoute != null) {
             navGraph.setStartDestination(startDestinationRoute!!)
+        } else if (startDestinationClass != null) {
+            navGraph.setStartDestination(startDestinationClass!!)
+        } else if (startDestinationObject != null) {
+            navGraph.setStartDestination(startDestinationObject!!)
         } else {
             navGraph.setStartDestination(startDestinationId)
         }
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteEncoder.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteEncoder.kt
index 2a9256d..719dab4 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteEncoder.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteEncoder.kt
@@ -48,8 +48,9 @@
      * default implementation which further serializes nested non-primitive values). So we
      * delegate to the default entry by directly calling [super.encodeSerializableValue].
      */
-    fun encodeRouteWithArgs(value: T): String {
-        super.encodeSerializableValue(serializer, value)
+    @Suppress("UNCHECKED_CAST")
+    fun encodeRouteWithArgs(value: Any): String {
+        super.encodeSerializableValue(serializer, value as T)
         return builder.build()
     }
 
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteSerializer.kt b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteSerializer.kt
index c99b628..b186ad0 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteSerializer.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/serialization/RouteSerializer.kt
@@ -18,14 +18,17 @@
 
 package androidx.navigation.serialization
 
+import androidx.annotation.RestrictTo
 import androidx.navigation.NamedNavArgument
 import androidx.navigation.NavType
 import androidx.navigation.navArgument
 import kotlin.reflect.KType
+import kotlinx.serialization.InternalSerializationApi
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.PolymorphicSerializer
 import kotlinx.serialization.descriptors.SerialDescriptor
 import kotlinx.serialization.descriptors.capturedKClass
+import kotlinx.serialization.serializer
 
 /**
  * Generates a route pattern for use in Navigation functions such as [::navigate] from
@@ -126,11 +129,15 @@
  * The generated route pattern contains the path, path args, and query args.
  * See [RouteBuilder.Builder.computeParamType] for logic on how parameter type (path or query)
  * is computed.
+ *
+ * [T] as receiver to allow secondary constructors for nav builders (i.e. NavGraphBuilder)
+ * to take object <T : Any> as parameter
  */
-internal fun <T : Any> KSerializer<T>.generateRouteWithArgs(
-    destination: T,
+@OptIn(InternalSerializationApi::class)
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public fun <T : Any> T.generateRouteWithArgs(
     typeMap: Map<String, NavType<Any?>>
-): String = RouteEncoder(this, typeMap).encodeRouteWithArgs(destination)
+): String = RouteEncoder(this::class.serializer(), typeMap).encodeRouteWithArgs(this)
 
 private fun <T> KSerializer<T>.assertNotAbstractClass(handler: () -> Unit) {
     // abstract class
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
index ae36260..d2b50f3 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/NavigationSamples.kt
@@ -42,12 +42,12 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.platform.LocalInspectionMode
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.navigation.NavController
 import androidx.navigation.NavHostController
 import androidx.navigation.NavType
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
index 20edc30..1e36d9d 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavBackStackEntryProviderTest.kt
@@ -19,12 +19,12 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModelStoreOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.testing.TestNavigatorState
diff --git a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
index bfbf4ee..32980f7 100644
--- a/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
+++ b/navigation/navigation-compose/src/androidTest/java/androidx/navigation/compose/NavHostTest.kt
@@ -43,7 +43,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -54,6 +53,7 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
index 4957694..2ca5a6f 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavBackStackEntryProvider.kt
@@ -19,10 +19,10 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.saveable.SaveableStateHolder
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.platform.LocalSavedStateRegistryOwner
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.lifecycle.viewmodel.compose.viewModel
 import androidx.navigation.NavBackStackEntry
diff --git a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
index 1b41906..a816657 100644
--- a/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
+++ b/navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHost.kt
@@ -44,7 +44,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
 import androidx.navigation.NavBackStackEntry
 import androidx.navigation.NavDestination
diff --git a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
index 83c3fbe..112d751 100644
--- a/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-client/src/androidTest/java/androidx/privacysandbox/sdkruntime/client/loader/SdkLoaderTest.kt
@@ -17,16 +17,10 @@
 
 import android.content.Context
 import android.os.Build
-import android.os.Bundle
-import android.os.IBinder
 import androidx.privacysandbox.sdkruntime.client.TestSdkConfigs
 import androidx.privacysandbox.sdkruntime.client.config.LocalSdkConfig
-import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
 import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
 import androidx.privacysandbox.sdkruntime.core.Versions
-import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
-import androidx.privacysandbox.sdkruntime.core.controller.LoadSdkCallback
 import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -35,7 +29,7 @@
 import androidx.testutils.assertThrows
 import com.google.common.truth.Truth.assertThat
 import java.io.File
-import java.util.concurrent.Executor
+import java.lang.reflect.Proxy
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -157,45 +151,18 @@
     }
 
     private object NoOpFactory : SdkLoader.ControllerFactory {
-        override fun createControllerFor(sdkConfig: LocalSdkConfig) = NoOpImpl()
-    }
 
-    private class NoOpImpl : SdkSandboxControllerCompat.SandboxControllerImpl {
+        val controllerImplClass = SdkSandboxControllerCompat.SandboxControllerImpl::class.java
 
-        override fun loadSdk(
-            sdkName: String,
-            params: Bundle,
-            executor: Executor,
-            callback: LoadSdkCallback
-        ) {
-            executor.execute {
-                callback.onError(
-                    LoadSdkCompatException(
-                        LoadSdkCompatException.LOAD_SDK_INTERNAL_ERROR,
-                        "NoOp"
-                    )
-                )
-            }
-        }
+        val noOpProxy = Proxy.newProxyInstance(
+            controllerImplClass.classLoader,
+            arrayOf(controllerImplClass)
+        ) { proxy, method, args ->
+            throw UnsupportedOperationException(
+                "Unexpected method call (NoOp) object:$proxy, method: $method, args: $args"
+            )
+        } as SdkSandboxControllerCompat.SandboxControllerImpl
 
-        override fun getSandboxedSdks(): List<SandboxedSdkCompat> {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun registerSdkSandboxActivityHandler(
-            handlerCompat: SdkSandboxActivityHandlerCompat
-        ): IBinder {
-            throw UnsupportedOperationException("NoOp")
-        }
-
-        override fun unregisterSdkSandboxActivityHandler(
-            handlerCompat: SdkSandboxActivityHandlerCompat
-        ) {
-            throw UnsupportedOperationException("NoOp")
-        }
+        override fun createControllerFor(sdkConfig: LocalSdkConfig) = noOpProxy
     }
 }
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/api/api_lint.ignore b/privacysandbox/sdkruntime/sdkruntime-core/api/api_lint.ignore
new file mode 100644
index 0000000..368b5a7
--- /dev/null
+++ b/privacysandbox/sdkruntime/sdkruntime-core/api/api_lint.ignore
@@ -0,0 +1,5 @@
+// Baseline format: 1.0
+DocumentExceptions: androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat#from(android.content.Context):
+    Method SdkSandboxControllerCompat.from appears to be throwing java.lang.UnsupportedOperationException; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
+DocumentExceptions: androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat.Companion#from(android.content.Context):
+    Method Companion.from appears to be throwing java.lang.UnsupportedOperationException; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
index b20baed..84faafc 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/androidTest/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompatLocalTest.kt
@@ -60,16 +60,10 @@
     }
 
     @Test
-    fun loadSdk_withoutLocalImpl_throwsLoadSdkNotFoundException() {
-        val controllerCompat = SdkSandboxControllerCompat.from(context)
-
-        val exception = Assert.assertThrows(LoadSdkCompatException::class.java) {
-            runBlocking {
-                controllerCompat.loadSdk("SDK", Bundle())
-            }
+    fun from_withoutLocalImpl_throwsUnsupportedOperationException() {
+        Assert.assertThrows(UnsupportedOperationException::class.java) {
+            SdkSandboxControllerCompat.from(context)
         }
-
-        assertThat(exception.loadSdkErrorCode).isEqualTo(LoadSdkCompatException.LOAD_SDK_NOT_FOUND)
     }
 
     @Test
@@ -90,10 +84,7 @@
     }
 
     @Test
-    fun loadSdk_withLocalImpl_returnsLoadedSdkFromLocalImpl() {
-        // Emulate loading via client lib with version 5
-        Versions.handShake(5)
-
+    fun loadSdk_returnsLoadedSdkFromLocalImpl() {
         val expectedResult = SandboxedSdkCompat(Binder())
         val stubLocalImpl = TestStubImpl(
             loadSdkResult = expectedResult
@@ -113,10 +104,7 @@
     }
 
     @Test
-    fun loadSdk_withLocalImpl_rethrowsExceptionFromLocalImpl() {
-        // Emulate loading via client lib with version 5
-        Versions.handShake(5)
-
+    fun loadSdk_rethrowsExceptionFromLocalImpl() {
         val expectedError = LoadSdkCompatException(RuntimeException(), Bundle())
         SdkSandboxControllerCompat.injectLocalImpl(
             TestStubImpl(
@@ -135,14 +123,7 @@
     }
 
     @Test
-    fun getSandboxedSdks_withoutLocalImpl_returnsEmptyList() {
-        val controllerCompat = SdkSandboxControllerCompat.from(context)
-        val sandboxedSdks = controllerCompat.getSandboxedSdks()
-        assertThat(sandboxedSdks).isEmpty()
-    }
-
-    @Test
-    fun getSandboxedSdks_withLocalImpl_returnsListFromLocalImpl() {
+    fun getSandboxedSdks_returnsListFromLocalImpl() {
         val expectedResult = listOf(SandboxedSdkCompat(Binder()))
         SdkSandboxControllerCompat.injectLocalImpl(
             TestStubImpl(
@@ -156,13 +137,6 @@
     }
 
     @Test
-    fun getAppOwnedSdkSandboxInterfaces_withoutLocalImpl_returnsEmptyList() {
-        val controllerCompat = SdkSandboxControllerCompat.from(context)
-        val appOwnedInterfaces = controllerCompat.getAppOwnedSdkSandboxInterfaces()
-        assertThat(appOwnedInterfaces).isEmpty()
-    }
-
-    @Test
     fun getAppOwnedSdkSandboxInterfaces_clientApiBelow4_returnsEmptyList() {
         // Emulate loading via client lib with version below 4
         Versions.handShake(3)
@@ -185,7 +159,7 @@
     }
 
     @Test
-    fun getAppOwnedSdkSandboxInterfaces_withLocalImpl_returnsListFromLocalImpl() {
+    fun getAppOwnedSdkSandboxInterfaces_returnsListFromLocalImpl() {
         val expectedResult = listOf(
             AppOwnedSdkSandboxInterfaceCompat(
                 name = "TestSdk",
@@ -205,7 +179,7 @@
     }
 
     @Test
-    fun registerSdkSandboxActivityHandler_withLocalImpl_registerItInLocalImpl() {
+    fun registerSdkSandboxActivityHandler_registerItInLocalImpl() {
         val localImpl = TestStubImpl()
         SdkSandboxControllerCompat.injectLocalImpl(localImpl)
 
@@ -218,7 +192,7 @@
     }
 
     @Test
-    fun unregisterSdkSandboxActivityHandler_withLocalImpl_unregisterItFromLocalImpl() {
+    fun unregisterSdkSandboxActivityHandler_unregisterItFromLocalImpl() {
         val localImpl = TestStubImpl()
         SdkSandboxControllerCompat.injectLocalImpl(localImpl)
 
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
index 07cc5b4..a2ad3e9 100644
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
+++ b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/SdkSandboxControllerCompat.kt
@@ -32,7 +32,6 @@
 import androidx.privacysandbox.sdkruntime.core.Versions
 import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
 import androidx.privacysandbox.sdkruntime.core.controller.impl.LocalImpl
-import androidx.privacysandbox.sdkruntime.core.controller.impl.NoOpImpl
 import androidx.privacysandbox.sdkruntime.core.controller.impl.PlatformUDCImpl
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicBoolean
@@ -160,11 +159,10 @@
         fun from(context: Context): SdkSandboxControllerCompat {
             val clientVersion = Versions.CLIENT_VERSION
             if (clientVersion != null) {
-                val implFromClient = localImpl
-                if (implFromClient != null) {
-                    return SdkSandboxControllerCompat(LocalImpl(implFromClient, clientVersion))
-                }
-                return SdkSandboxControllerCompat(NoOpImpl())
+                val implFromClient = localImpl ?: throw UnsupportedOperationException(
+                    "Shouldn't happen: No controller implementation available"
+                )
+                return SdkSandboxControllerCompat(LocalImpl(implFromClient, clientVersion))
             }
             val platformImpl = PlatformImplFactory.create(context)
             return SdkSandboxControllerCompat(platformImpl)
diff --git a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt b/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt
deleted file mode 100644
index fdb388f..0000000
--- a/privacysandbox/sdkruntime/sdkruntime-core/src/main/java/androidx/privacysandbox/sdkruntime/core/controller/impl/NoOpImpl.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.privacysandbox.sdkruntime.core.controller.impl
-
-import android.os.Bundle
-import android.os.IBinder
-import androidx.privacysandbox.sdkruntime.core.AppOwnedSdkSandboxInterfaceCompat
-import androidx.privacysandbox.sdkruntime.core.LoadSdkCompatException
-import androidx.privacysandbox.sdkruntime.core.SandboxedSdkCompat
-import androidx.privacysandbox.sdkruntime.core.activity.SdkSandboxActivityHandlerCompat
-import androidx.privacysandbox.sdkruntime.core.controller.LoadSdkCallback
-import androidx.privacysandbox.sdkruntime.core.controller.SdkSandboxControllerCompat
-import java.util.concurrent.Executor
-
-/**
- * NoOp implementation for cases when [SdkSandboxControllerCompat] not supported.
- */
-internal class NoOpImpl : SdkSandboxControllerCompat.SandboxControllerImpl {
-
-    override fun loadSdk(
-        sdkName: String,
-        params: Bundle,
-        executor: Executor,
-        callback: LoadSdkCallback
-    ) {
-        executor.execute {
-            callback.onError(
-                LoadSdkCompatException(
-                    LoadSdkCompatException.LOAD_SDK_NOT_FOUND,
-                    "Loading SDK not supported on this device"
-                )
-            )
-        }
-    }
-
-    override fun getSandboxedSdks(): List<SandboxedSdkCompat> = emptyList()
-
-    override fun getAppOwnedSdkSandboxInterfaces(): List<AppOwnedSdkSandboxInterfaceCompat> =
-        emptyList()
-
-    override fun registerSdkSandboxActivityHandler(
-        handlerCompat: SdkSandboxActivityHandlerCompat
-    ):
-        IBinder {
-        throw UnsupportedOperationException("Not supported")
-    }
-
-    override fun unregisterSdkSandboxActivityHandler(
-        handlerCompat: SdkSandboxActivityHandlerCompat
-    ) {
-        throw UnsupportedOperationException("Not supported")
-    }
-}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestFlagConverter.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestFlagConverter.kt
index c905fff..a5aaa9b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestFlagConverter.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/fullfeaturedsdk/output/com/mysdk/RequestFlagConverter.kt
@@ -5,8 +5,10 @@
 public class RequestFlagConverter(
     public val context: Context,
 ) {
+    private val enumValues: List<RequestFlag> = RequestFlag.values().toList()
+
     public fun fromParcelable(parcelable: ParcelableRequestFlag): RequestFlag =
-            RequestFlag.entries[parcelable.variant_ordinal]
+            enumValues[parcelable.variant_ordinal]
 
     public fun toParcelable(annotatedValue: RequestFlag): ParcelableRequestFlag {
         val parcelable = ParcelableRequestFlag()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyEnumConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyEnumConverter.kt
index 9554eaf..9d7fbd8 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyEnumConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/callbacks/output/com/sdkwithcallbacks/MyEnumConverter.kt
@@ -1,8 +1,10 @@
 package com.sdkwithcallbacks
 
 public object MyEnumConverter {
+    private val enumValues: List<MyEnum> = MyEnum.values().toList()
+
     public fun fromParcelable(parcelable: ParcelableMyEnum): MyEnum =
-            MyEnum.entries[parcelable.variant_ordinal]
+            enumValues[parcelable.variant_ordinal]
 
     public fun toParcelable(annotatedValue: MyEnum): ParcelableMyEnum {
         val parcelable = ParcelableMyEnum()
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlagConverter.kt b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlagConverter.kt
index adcb755..06e7bfa 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlagConverter.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/test-data/values/output/com/sdkwithvalues/RequestFlagConverter.kt
@@ -1,8 +1,10 @@
 package com.sdkwithvalues
 
 public object RequestFlagConverter {
+    private val enumValues: List<RequestFlag> = RequestFlag.values().toList()
+
     public fun fromParcelable(parcelable: ParcelableRequestFlag): RequestFlag =
-            RequestFlag.entries[parcelable.variant_ordinal]
+            enumValues[parcelable.variant_ordinal]
 
     public fun toParcelable(annotatedValue: RequestFlag): ParcelableRequestFlag {
         val parcelable = ParcelableRequestFlag()
diff --git a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
index e326d56..1277ce6 100644
--- a/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
+++ b/privacysandbox/tools/tools-core/src/main/java/androidx/privacysandbox/tools/core/generator/ValueConverterFileGenerator.kt
@@ -22,6 +22,7 @@
 import androidx.privacysandbox.tools.core.model.AnnotatedDataClass
 import androidx.privacysandbox.tools.core.model.AnnotatedEnumClass
 import androidx.privacysandbox.tools.core.model.AnnotatedValue
+import androidx.privacysandbox.tools.core.model.Types
 import androidx.privacysandbox.tools.core.model.ValueProperty
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
@@ -64,11 +65,15 @@
                 )
                 addFunction(generateFromParcelable(value))
                 addFunction(generateToParcelable(value))
+                if (value is AnnotatedEnumClass)
+                    addProperty(generateEnumValuesProperty(value))
             }
         }
         return TypeSpec.objectBuilder(value.converterNameSpec()).build() {
             addFunction(generateFromParcelable(value))
             addFunction(generateToParcelable(value))
+            if (value is AnnotatedEnumClass)
+                addProperty(generateEnumValuesProperty(value))
         }
     }
 
@@ -120,7 +125,7 @@
                     addParameter("parcelable", value.parcelableNameSpec())
                     returns(value.type.poetTypeName())
                     addStatement(
-                        "return %T.entries[parcelable.variant_ordinal]",
+                        "return enumValues[parcelable.variant_ordinal]",
                         value.type.poetTypeName()
                     )
                 }
@@ -135,4 +140,16 @@
                 )
             )
         }
+
+    private fun generateEnumValuesProperty(value: AnnotatedEnumClass) =
+        PropertySpec.builder(
+            "enumValues",
+            Types.list(value.type).poetTypeName()
+        )
+            .addModifiers(KModifier.PRIVATE)
+            .initializer(
+                CodeBlock.of(
+                    "%T.values().toList()", value.type.poetClassName()
+                )
+        ).build()
 }
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index dab5c07..b3d110e 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -19,7 +19,7 @@
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.customview:customview:1.0.0")
     implementation("androidx.customview:customview-poolingcontainer:1.0.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index 4bf79fb..6a755ab 100644
--- a/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/androidInstrumentedTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -19,15 +19,16 @@
 import androidx.room.Room
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
 import androidx.test.platform.app.InstrumentationRegistry
+import kotlinx.coroutines.Dispatchers
 
 class SimpleQueryTest : BaseSimpleQueryTest() {
 
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
 
     override fun getRoomDatabase(): SampleDatabase {
-        return Room.inMemoryDatabaseBuilder<SampleDatabase>(
-            context = instrumentation.targetContext,
-        ).setDriver(BundledSQLiteDriver(":memory:"))
+        return Room.inMemoryDatabaseBuilder<SampleDatabase>(instrumentation.targetContext)
+            .setDriver(BundledSQLiteDriver(":memory:"))
+            .setQueryCoroutineContext(Dispatchers.IO)
             .build()
     }
 }
diff --git a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
index 6a2c7e7..836f9c4 100644
--- a/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/commonTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BaseSimpleQueryTest.kt
@@ -18,11 +18,24 @@
 
 import androidx.kruth.assertThat
 import androidx.kruth.assertThrows
+import androidx.room.execSQL
+import androidx.room.immediateTransaction
+import androidx.room.useReaderConnection
+import androidx.room.useWriterConnection
+import androidx.sqlite.SQLiteException
 import kotlin.test.AfterTest
 import kotlin.test.BeforeTest
 import kotlin.test.Test
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.produceIn
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.yield
 
 abstract class BaseSimpleQueryTest {
 
@@ -171,7 +184,7 @@
     }
 
     @Test
-    fun simpleInsertMap() = runTest {
+    fun insertMap() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntity2(1, 2)
         val dao = getRoomDatabase().dao()
@@ -185,7 +198,7 @@
     }
 
     @Test
-    fun simpleMapWithDupeColumns() = runTest {
+    fun mapWithDupeColumns() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntityCopy(1, 2)
         val dao = getRoomDatabase().dao()
@@ -199,7 +212,7 @@
     }
 
     @Test
-    fun simpleInsertNestedMap() = runTest {
+    fun insertNestedMap() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntity2(1, 2)
         val sampleEntity3 = SampleEntity3(1, 2)
@@ -215,7 +228,7 @@
     }
 
     @Test
-    fun simpleInsertNestedMapColumnMap() = runTest {
+    fun insertNestedMapColumnMap() = runTest {
         val sampleEntity1 = SampleEntity(1, 1)
         val sampleEntity2 = SampleEntity2(1, 2)
         val sampleEntity3 = SampleEntity3(1, 2)
@@ -229,4 +242,139 @@
         val map = dao.getSimpleNestedMapColumnMap()
         assertThat(map[sampleEntity1]).isEqualTo(mapOf(Pair(sampleEntity2, sampleEntity3.data3)))
     }
+
+    @Test
+    fun combineInsertAndManualWrite() = runTest {
+        val db = getRoomDatabase()
+        db.useWriterConnection { connection ->
+            db.dao().insertItem(1)
+            connection.execSQL("INSERT INTO SampleEntity (pk) VALUES (2)")
+        }
+        db.useReaderConnection { connection ->
+            val count = connection.usePrepared("SELECT count(*) FROM SampleEntity") {
+                it.step()
+                it.getLong(0)
+            }
+            assertThat(count).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun combineQueryAndManualRead() = runTest {
+        val db = getRoomDatabase()
+        val entity = SampleEntity(1, 10)
+        db.dao().insert(entity)
+        db.useReaderConnection { connection ->
+            assertThat(
+                db.dao().getItemList()
+            ).containsExactly(entity)
+            assertThat(
+                connection.usePrepared("SELECT * FROM SampleEntity") {
+                    buildList {
+                        while (it.step()) {
+                            add(SampleEntity(it.getLong(0), it.getLong(1)))
+                        }
+                    }
+                }
+            ).containsExactly(entity)
+        }
+    }
+
+    @Test
+    fun queriesAreIsolated() = runTest {
+        val db = getRoomDatabase()
+        db.dao().insertItem(22)
+
+        // Validates that Room's coroutine scope provides isolation, if one query fails
+        // it doesn't affect others.
+        val failureQueryScope = CoroutineScope(Job())
+        val successQueryScope = CoroutineScope(Job())
+        val failureDeferred = failureQueryScope.async {
+            db.useReaderConnection { connection ->
+                connection.usePrepared("SELECT * FROM WrongTableName") {
+                    assertThat(it.step()).isFalse()
+                }
+            }
+        }
+        val successDeferred = successQueryScope.async {
+            db.useReaderConnection { connection ->
+                connection.usePrepared("SELECT * FROM SampleEntity") {
+                    assertThat(it.step()).isTrue()
+                    it.getLong(0)
+                }
+            }
+        }
+        assertThrows<SQLiteException> { failureDeferred.await() }
+            .hasMessageThat().contains("no such table: WrongTableName")
+        assertThat(successDeferred.await()).isEqualTo(22)
+    }
+
+    @Test
+    fun queriesAreIsolatedWhenCancelled() = runTest {
+        val db = getRoomDatabase()
+
+        // Validates that Room's coroutine scope provides isolation, if scope doing a query is
+        // cancelled it doesn't affect others.
+        val toBeCancelledScope = CoroutineScope(Job())
+        val notCancelledScope = CoroutineScope(Job())
+        val latch = Mutex(locked = true)
+        val cancelledDeferred = toBeCancelledScope.async {
+            db.useReaderConnection { latch.withLock { } }
+            1
+        }
+        val notCancelledDeferred = notCancelledScope.async {
+            db.useReaderConnection { latch.withLock { } }
+            1
+        }
+
+        yield()
+        toBeCancelledScope.cancel()
+        latch.unlock()
+
+        assertThrows<CancellationException> {
+            cancelledDeferred.await()
+        }
+        assertThat(
+            notCancelledDeferred.await()
+        ).isEqualTo(1)
+    }
+
+    @Test
+    fun queryFlowFromManualWrite() = runTest {
+        val db = getRoomDatabase()
+
+        val channel = db.dao().getItemListFlow().produceIn(this)
+
+        assertThat(channel.receive()).isEmpty()
+
+        // Validates that a write using the connection directly will cause invalidation when
+        // a refresh is requested.
+        db.useWriterConnection { connection ->
+            connection.execSQL("INSERT INTO SampleEntity (pk) VALUES (13)")
+        }
+        db.invalidationTracker.refreshAsync()
+        assertThat(channel.receive()).containsExactly(
+            SampleEntity(13),
+        )
+
+        channel.cancel()
+    }
+
+    @Test
+    fun rollbackDaoQuery() = runTest {
+        val db = getRoomDatabase()
+        db.dao().insertItem(1)
+        db.useWriterConnection { transactor ->
+            transactor.immediateTransaction {
+                db.dao().insertItem(2)
+                rollback(Unit)
+            }
+            val count = transactor.usePrepared("SELECT count(*) FROM SampleEntity") {
+                it.step()
+                it.getLong(0)
+            }
+            assertThat(count).isEqualTo(1)
+        }
+        assertThat(db.dao().getItemList()).containsExactly(SampleEntity(1))
+    }
 }
diff --git a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index df1f8024..ce59cf6 100644
--- a/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/jvmTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -18,12 +18,14 @@
 
 import androidx.room.Room
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlinx.coroutines.Dispatchers
 
 class SimpleQueryTest : BaseSimpleQueryTest() {
 
     override fun getRoomDatabase(): SampleDatabase {
         return Room.inMemoryDatabaseBuilder<SampleDatabase>()
             .setDriver(BundledSQLiteDriver(":memory:"))
+            .setQueryCoroutineContext(Dispatchers.IO)
             .build()
     }
 }
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
index c35594c..7909d4d 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/AutoMigrationTest.kt
@@ -39,7 +39,9 @@
     override fun getTestHelper() = migrationTestHelper
 
     override fun getRoomDatabase(): AutoMigrationDatabase {
-        return Room.databaseBuilder(filename) { AutoMigrationDatabase::class.instantiateImpl() }
+        return Room.databaseBuilder<AutoMigrationDatabase>(filename) {
+            AutoMigrationDatabase::class.instantiateImpl()
+        }
             .setDriver(driver).build()
     }
 
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
index 2411bc0..dd4f1fb 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/BuilderTest.kt
@@ -29,7 +29,9 @@
     private val filename = "/tmp/test-${Random.nextInt()}.db"
 
     override fun getRoomDatabaseBuilder(): RoomDatabase.Builder<SampleDatabase> {
-        return Room.databaseBuilder(filename) { SampleDatabase::class.instantiateImpl() }
+        return Room.databaseBuilder<SampleDatabase>(filename) {
+            SampleDatabase::class.instantiateImpl()
+        }
             .setDriver(BundledSQLiteDriver(filename))
     }
 
diff --git a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
index 6257320..0f15cc8 100644
--- a/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
+++ b/room/integration-tests/multiplatformtestapp/src/nativeTest/kotlin/androidx/room/integration/multiplatformtestapp/test/SimpleQueryTest.kt
@@ -18,12 +18,17 @@
 
 import androidx.room.Room
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.IO
 
 class SimpleQueryTest : BaseSimpleQueryTest() {
 
     override fun getRoomDatabase(): SampleDatabase {
-        return Room.inMemoryDatabaseBuilder { SampleDatabase::class.instantiateImpl() }
+        return Room.inMemoryDatabaseBuilder<SampleDatabase> {
+            SampleDatabase::class.instantiateImpl()
+        }
             .setDriver(BundledSQLiteDriver(":memory:"))
+            .setQueryCoroutineContext(Dispatchers.IO)
             .build()
     }
 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 94cd311..0171412 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -193,6 +193,12 @@
          * respectively.
          */
         fun getArrayName(componentTypeName: XTypeName): XTypeName {
+            componentTypeName.java.let {
+                require(it !is JWildcardTypeName || it.lowerBounds.isEmpty()) {
+                    "Can't have contra-variant component types in Java arrays. Found '$it'."
+                }
+            }
+
             val (java, kotlin) = when (componentTypeName) {
                 PRIMITIVE_BOOLEAN ->
                     JArrayTypeName.of(JTypeName.BOOLEAN) to BOOLEAN_ARRAY
@@ -210,9 +216,16 @@
                     JArrayTypeName.of(JTypeName.FLOAT) to FLOAT_ARRAY
                 PRIMITIVE_DOUBLE ->
                     JArrayTypeName.of(JTypeName.DOUBLE) to DOUBLE_ARRAY
-                else ->
-                    JArrayTypeName.of(componentTypeName.java) to
-                        ARRAY.parameterizedBy(componentTypeName.kotlin)
+                else -> {
+                    componentTypeName.java.let {
+                        if (it is JWildcardTypeName) {
+                            JArrayTypeName.of(it.upperBounds.single())
+                        } else {
+                            JArrayTypeName.of(it)
+                        }
+                    } to
+                    ARRAY.parameterizedBy(componentTypeName.kotlin)
+                }
             }
             return XTypeName(
                 java = java,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 15a9043..a0549e5 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -20,6 +20,7 @@
 import androidx.room.compiler.processing.XExecutableParameterElement
 import androidx.room.compiler.processing.XMemberContainer
 import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE_OR_METHOD_PARAMETER
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
 import androidx.room.compiler.processing.util.sanitizeAsJavaParameterName
@@ -74,7 +75,7 @@
     private fun createAsMemberOf(container: XType?): KspType {
         check(container is KspType?)
         val resolvedType = parameter.type.resolve()
-        return env.wrap(
+        val type = env.wrap(
             originalAnnotations = parameter.type.annotations,
             ksType = parameter.typeAsMemberOf(
                 functionDeclaration = enclosingElement.declaration,
@@ -91,6 +92,13 @@
                 asMemberOf = container,
             )
         )
+        // In KSP2 the varargs have the component type instead of the array type. We make it always
+        // return the array type in XProcessing.
+        return if (isVarArgs() && !type.isArray()) {
+            env.getArrayType(type)
+        } else {
+            type
+        }
     }
 
     override fun kindName(): String {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
index e93dfd6..f42e1f5 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.codegen
 
 import androidx.kruth.assertThat
+import androidx.kruth.assertThrows
 import androidx.room.compiler.processing.XNullability
 import com.squareup.kotlinpoet.INT
 import com.squareup.kotlinpoet.SHORT
@@ -176,4 +177,25 @@
         assertThat(XTypeName.getProducerExtendsName(typeName).kotlin)
             .isEqualTo(XTypeName.UNAVAILABLE_KTYPE_NAME)
     }
+
+    @Test
+    fun arrays() {
+        XTypeName.getArrayName(Number::class.asClassName()).let {
+            assertThat(it.toString(CodeLanguage.JAVA))
+                .isEqualTo("java.lang.Number[]")
+            assertThat(it.toString(CodeLanguage.KOTLIN))
+                .isEqualTo("kotlin.Array<kotlin.Number>")
+        }
+        XTypeName.getArrayName(
+            XTypeName.getProducerExtendsName(Number::class.asClassName())).let {
+            assertThat(it.toString(CodeLanguage.JAVA))
+                .isEqualTo("java.lang.Number[]")
+            assertThat(it.toString(CodeLanguage.KOTLIN))
+                .isEqualTo("kotlin.Array<out kotlin.Number>")
+        }
+        assertThrows<IllegalArgumentException> {
+            XTypeName.getArrayName(XTypeName.getConsumerSuperName(Number::class.asClassName()))
+        }.hasMessageThat().isEqualTo("Can't have contra-variant component types in Java " +
+            "arrays. Found '? super java.lang.Number'.")
+    }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index fab71288..d60da50 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -108,6 +108,7 @@
             package foo.bar;
             interface Baz {
                 void method(String... inputs);
+                void methodPrimitive(int... inputs);
             }
             """.trimIndent()
         )
@@ -115,7 +116,16 @@
             sources = listOf(subject)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
-            assertThat(element.getMethodByJvmName("method").isVarArgs()).isTrue()
+            element.getMethodByJvmName("method").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(String::class.asClassName()))
+            }
+            element.getMethodByJvmName("methodPrimitive").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(XTypeName.PRIMITIVE_INT))
+            }
         }
     }
 
@@ -128,6 +138,7 @@
                 fun method(vararg inputs: String)
                 suspend fun suspendMethod(vararg inputs: String)
                 fun method2(vararg inputs: String, arg: Int)
+                fun methodPrimitive(vararg inputs: Int)
                 fun String.extFun(vararg inputs: String)
             }
             """.trimIndent()
@@ -141,6 +152,10 @@
                 assertThat(method.isVarArgs()).isTrue()
                 assertThat(method.parameters).hasSize(1)
                 assertThat(method.parameters.single().isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("suspendMethod").let { suspendMethod ->
@@ -149,6 +164,12 @@
                 assertThat(
                     suspendMethod.parameters.first { it.name == "inputs" }.isVarArgs()
                 ).isTrue()
+                assertThat(
+                    suspendMethod.parameters.first { it.name == "inputs" }.type.asTypeName()
+                ).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("extFun").let { extFun ->
@@ -157,6 +178,10 @@
                 // kapt messed with parameter names, sometimes the synthetic parameter can use the
                 // second parameter's name.
                 assertThat(extFun.parameters.get(1).isVarArgs()).isTrue()
+                assertThat(extFun.parameters.get(1).type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(
+                        XTypeName.getProducerExtendsName(String::class.asClassName()))
+                )
             }
 
             element.getMethodByJvmName("method2").let { method2 ->
@@ -164,6 +189,19 @@
                 assertThat(method2.parameters).hasSize(2)
                 assertThat(method2.parameters.first { it.name == "inputs" }.isVarArgs())
                     .isTrue()
+                assertThat(method2.parameters.first { it.name == "inputs" }.type.asTypeName())
+                    .isEqualTo(
+                        XTypeName.getArrayName(
+                            XTypeName.getProducerExtendsName(String::class.asClassName()))
+                    )
+            }
+            element.getMethodByJvmName("methodPrimitive").let { method ->
+                assertThat(method.isVarArgs()).isTrue()
+                assertThat(method.parameters).hasSize(1)
+                assertThat(method.parameters.single().isVarArgs()).isTrue()
+                assertThat(method.parameters.single().type.asTypeName()).isEqualTo(
+                    XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+                )
             }
         }
     }
diff --git a/room/room-runtime/api/current.txt b/room/room-runtime/api/current.txt
index fe0f7f8..7b91ca2 100644
--- a/room/room-runtime/api/current.txt
+++ b/room/room-runtime/api/current.txt
@@ -32,6 +32,7 @@
 
   public class InvalidationTracker {
     method @WorkerThread public void addObserver(androidx.room.InvalidationTracker.Observer observer);
+    method public final void refreshAsync();
     method public void refreshVersionsAsync();
     method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer observer);
     field public static final androidx.room.InvalidationTracker.Companion Companion;
@@ -170,6 +171,8 @@
 
   public final class RoomDatabaseKt {
     method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+    method public static suspend <R> Object? useReaderConnection(androidx.room.RoomDatabase, kotlin.jvm.functions.Function2<? super androidx.room.Transactor,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+    method public static suspend <R> Object? useWriterConnection(androidx.room.RoomDatabase, kotlin.jvm.functions.Function2<? super androidx.room.Transactor,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
   }
 
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 0a923121..4ca2f68 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -141,6 +141,7 @@
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void addWeakObserver(androidx.room.InvalidationTracker.Observer observer);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String[] tableNames, boolean inTransaction, java.util.concurrent.Callable<T?> computeFunction);
     method @Deprecated @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public <T> androidx.lifecycle.LiveData<T> createLiveData(String[] tableNames, java.util.concurrent.Callable<T?> computeFunction);
+    method public final void refreshAsync();
     method public void refreshVersionsAsync();
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @WorkerThread public void refreshVersionsSync();
     method @WorkerThread public void removeObserver(androidx.room.InvalidationTracker.Observer observer);
@@ -288,6 +289,8 @@
 
   public final class RoomDatabaseKt {
     method public static kotlinx.coroutines.flow.Flow<java.util.Set<java.lang.String>> invalidationTrackerFlow(androidx.room.RoomDatabase, String[] tables, optional boolean emitInitialState);
+    method public static suspend <R> Object? useReaderConnection(androidx.room.RoomDatabase, kotlin.jvm.functions.Function2<? super androidx.room.Transactor,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
+    method public static suspend <R> Object? useWriterConnection(androidx.room.RoomDatabase, kotlin.jvm.functions.Function2<? super androidx.room.Transactor,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
     method public static suspend <R> Object? withTransaction(androidx.room.RoomDatabase, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
   }
 
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
index f37b490..52ca268 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/InvalidationTracker.android.kt
@@ -148,10 +148,8 @@
      *
      * This function should be called after any write operation is performed on the database,
      * such that tracked tables and its associated observers are notified if invalidated.
-     *
-     * @see sync
      */
-    internal actual fun refreshAsync() {
+    actual fun refreshAsync() {
         implementation.refreshInvalidationAsync(onRefreshScheduled, onRefreshCompleted)
     }
 
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
index 4b5dbc5..f585bab 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomDatabase.android.kt
@@ -611,6 +611,10 @@
 
     /**
      * Use a connection to perform database operations.
+     *
+     * This function is for internal access to the pool, it is an unconfined coroutine function to
+     * be used by Room generated code paths. For the public version see [useReaderConnection] and
+     * [useWriterConnection].
      */
     internal actual suspend fun <R> useConnection(
         isReadOnly: Boolean,
@@ -641,6 +645,8 @@
      * @return A Cursor obtained by running the given query in the Room database.
      */
     open fun query(query: String, args: Array<out Any?>?): Cursor {
+        assertNotMainThread()
+        assertNotSuspendingTransaction()
         return openHelper.writableDatabase.query(SimpleSQLiteQuery(query, args))
     }
 
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
index 8562ad9..413f4e8 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/InvalidationTracker.kt
@@ -92,10 +92,8 @@
      *
      * This function should be called after any write operation is performed on the database,
      * such that tracked tables and its associated observers are notified if invalidated.
-     *
-     * @see sync
      */
-    internal fun refreshAsync()
+    fun refreshAsync()
 
     /**
      * Stops invalidation tracker operations.
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
index d959d4b4..919cf1a 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/RoomDatabase.kt
@@ -22,16 +22,17 @@
 import androidx.room.concurrent.CloseBarrier
 import androidx.room.migration.AutoMigrationSpec
 import androidx.room.migration.Migration
-import androidx.room.util.contains
 import androidx.room.util.isAssignableFrom
 import androidx.sqlite.SQLiteConnection
 import androidx.sqlite.SQLiteDriver
+import androidx.sqlite.SQLiteException
 import kotlin.coroutines.CoroutineContext
 import kotlin.jvm.JvmMultifileClass
 import kotlin.jvm.JvmName
 import kotlin.reflect.KClass
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.withContext
 
 /**
  * Base class for all Room databases. All classes that are annotated with [Database] must
@@ -42,7 +43,7 @@
  *
  * @see Database
  */
-expect abstract class RoomDatabase {
+expect abstract class RoomDatabase() {
 
     /**
      * The invalidation tracker for this database.
@@ -186,6 +187,10 @@
 
     /**
      * Use a connection to perform database operations.
+     *
+     * This function is for internal access to the pool, it is an unconfined coroutine function to
+     * be used by Room generated code paths. For the public version see [useReaderConnection] and
+     * [useWriterConnection].
      */
     internal suspend fun <R> useConnection(isReadOnly: Boolean, block: suspend (Transactor) -> R): R
 
@@ -336,6 +341,69 @@
     }
 }
 
+/**
+ * Acquires a READ connection, suspending while waiting if none is available and then calling the
+ * [block] to use the connection once it is acquired. A [RoomDatabase] will have one or more READ
+ * connections. The connection to use in the [block] is an instance of [Transactor] that provides
+ * the capabilities for performing nested transactions.
+ *
+ * Using the connection after [block] completes is prohibited.
+ *
+ * The connection will be confined to the coroutine on which [block] executes, attempting to use
+ * the connection from a different coroutine will result in an error.
+ *
+ * If the current coroutine calling this function already has a confined connection, then that
+ * connection is used.
+ *
+ * A connection is a limited resource and should not be held for more than it is needed. The best
+ * practice in using connections is to avoid executing long-running computations within the [block].
+ * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
+ * to a timeout.
+ *
+ * @param block The code to use the connection.
+ * @throws SQLiteException when the database is closed or a thread confined connection needs to be
+ * upgraded or there is a timeout acquiring a connection.
+ *
+ * @see [useWriterConnection]
+ */
+suspend fun <R> RoomDatabase.useReaderConnection(
+    block: suspend (Transactor) -> R
+): R = withContext(getCoroutineScope().coroutineContext) {
+    useConnection(isReadOnly = true, block)
+}
+
+/**
+ * Acquires a WRITE connection, suspending while waiting if none is available and then calling the
+ * [block] to use the connection once it is acquired. A [RoomDatabase] will have only one WRITE
+ * connection. The connection to use in the [block] is an instance of [Transactor] that provides the
+ * capabilities for performing nested transactions.
+ *
+ * Using the connection after [block] completes is prohibited.
+ *
+ * The connection will be confined to the coroutine on which [block] executes, attempting to use
+ * the connection from a different coroutine will result in an error.
+ *
+ * If the current coroutine calling this function already has a confined connection, then that
+ * connection is used as long as it isn't required to be upgraded to a writer. If an upgrade is
+ * required then a [SQLiteException] is thrown.
+ *
+ * A connection is a limited resource and should not be held for more than it is needed. The best
+ * practice in using connections is to avoid executing long-running computations within the [block].
+ * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown due
+ * to a timeout.
+ *
+ * @param block The code to use the connection.
+ * @throws SQLiteException when the database is closed or a thread confined connection needs to be
+ * upgraded or there is a timeout acquiring a connection.
+ *
+ * @see [useReaderConnection]
+ */
+suspend fun <R> RoomDatabase.useWriterConnection(
+    block: suspend (Transactor) -> R
+): R = withContext(getCoroutineScope().coroutineContext) {
+    useConnection(isReadOnly = false, block)
+}
+
 internal fun RoomDatabase.validateAutoMigrations(configuration: DatabaseConfiguration) {
     val autoMigrationSpecs = mutableMapOf<KClass<out AutoMigrationSpec>, AutoMigrationSpec>()
     val requiredAutoMigrationSpecs = getRequiredAutoMigrationSpecClasses()
diff --git a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
index 0e2a3bd..168aaa0 100644
--- a/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
+++ b/room/room-runtime/src/commonMain/kotlin/androidx/room/coroutines/ConnectionPool.kt
@@ -43,15 +43,14 @@
      * Using the connection after [block] completes is prohibited.
      *
      * The connection will be confined to the coroutine on which [block] executes, attempting to use
-     * from another coroutine is an error.
+     * the connection from a different coroutine will result in an error.
      *
      * If the current coroutine calling this function already has a confined connection, then that
-     * connection is used as long as the connection isn't required to be upgraded to a writer.
-     * If an upgrade is required then a [SQLiteException] is thrown.
+     * connection is used as long as it isn't required to be upgraded to a writer. If an upgrade is
+     * required then a [SQLiteException] is thrown.
      *
-     * A connection is a resource and shouldn't be held more than it needs to, therefore try not to
-     * do long-running computations within the [block]. If a caller has to wait too long to
-     * acquire a connection a [SQLiteException] will be thrown due to a timeout.
+     * If a caller has to wait too long to acquire a connection a [SQLiteException] will be thrown
+     * due to a timeout.
      *
      * @param isReadOnly Whether to use a reader or a writer connection.
      * @param block The code to use the connection.
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
index 4ec00aa..af3d9ab 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/InvalidationTracker.jvmNative.kt
@@ -94,10 +94,8 @@
      *
      * This function should be called after any write operation is performed on the database,
      * such that tracked tables and its associated observers are notified if invalidated.
-     *
-     * @see sync
      */
-    internal actual fun refreshAsync() {
+    actual fun refreshAsync() {
         implementation.refreshInvalidationAsync()
     }
 
diff --git a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
index d4cd1f5..c478151e 100644
--- a/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
+++ b/room/room-runtime/src/jvmNativeMain/kotlin/androidx/room/RoomDatabase.jvmNative.kt
@@ -242,6 +242,10 @@
 
     /**
      * Use a connection to perform database operations.
+     *
+     * This function is for internal access to the pool, it is an unconfined coroutine function to
+     * be used by Room generated code paths. For the public version see [useReaderConnection] and
+     * [useWriterConnection].
      */
     internal actual suspend fun <R> useConnection(
         isReadOnly: Boolean,
diff --git a/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
index fdbb7ae..9b737b2 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/ImmersiveListSamples.kt
@@ -16,72 +16,84 @@
 
 package androidx.tv.samples
 
-import android.util.Log
 import androidx.annotation.Sampled
+import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Border
+import androidx.tv.material3.ClickableSurfaceDefaults
 import androidx.tv.material3.ExperimentalTvMaterial3Api
-import androidx.tv.material3.ImmersiveList
+import androidx.tv.material3.Surface
 
 @OptIn(ExperimentalTvMaterial3Api::class)
 @Sampled
 @Composable
-private fun SampleImmersiveList() {
-    val immersiveListHeight = 300.dp
-    val cardSpacing = 10.dp
-    val cardWidth = 200.dp
-    val cardHeight = 150.dp
-    val backgrounds = listOf(
-        Color.Red,
-        Color.Blue,
-        Color.Magenta,
-    )
+fun SampleImmersiveList() {
+    val items = remember { listOf(Color.Red, Color.Green, Color.Yellow) }
+    val selectedItem = remember { mutableStateOf<Color?>(null) }
 
-    ImmersiveList(
+    // Container
+    Box(
         modifier = Modifier
-            .height(immersiveListHeight + cardHeight / 2)
-            .fillMaxWidth(),
-        background = { index, _ ->
+            .fillMaxWidth()
+            .height(400.dp)
+    ) {
+        val bgColor = selectedItem.value
+
+        // Background
+        if (bgColor != null) {
             Box(
                 modifier = Modifier
-                    .background(backgrounds[index].copy(alpha = 0.3f))
-                    .height(immersiveListHeight)
                     .fillMaxWidth()
-            )
+                    .aspectRatio(20f / 7)
+                    .background(bgColor)
+            ) {}
         }
-    ) {
-        Row(horizontalArrangement = Arrangement.spacedBy(cardSpacing)) {
-            backgrounds.forEachIndexed { index, backgroundColor ->
-                var isFocused by remember { mutableStateOf(false) }
 
-                Box(
+        // Rows
+        LazyRow(
+            modifier = Modifier.align(Alignment.BottomEnd),
+            horizontalArrangement = Arrangement.spacedBy(20.dp),
+            contentPadding = PaddingValues(20.dp),
+        ) {
+            items(items) { color ->
+                Surface(
+                    onClick = { },
                     modifier = Modifier
-                        .background(backgroundColor)
-                        .width(cardWidth)
-                        .height(cardHeight)
-                        .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
-                        .onFocusChanged { isFocused = it.isFocused }
-                        .immersiveListItem(index)
-                        .clickable {
-                            Log.d("ImmersiveList", "Item $index was clicked")
-                        }
-                )
+                        .width(200.dp)
+                        .aspectRatio(16f / 9)
+                        .onFocusChanged {
+                            if (it.hasFocus) {
+                                selectedItem.value = color
+                            }
+                        },
+                    colors = ClickableSurfaceDefaults.colors(
+                        containerColor = color,
+                        focusedContainerColor = color,
+                    ),
+                    border = ClickableSurfaceDefaults.border(
+                        focusedBorder = Border(
+                            border = BorderStroke(2.dp, Color.White),
+                            inset = 4.dp,
+                        )
+                    )
+                ) {}
             }
         }
     }
diff --git a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
index d09d324..b697996 100644
--- a/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
+++ b/tv/samples/src/main/java/androidx/tv/samples/TabRowSamples.kt
@@ -19,10 +19,8 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -57,7 +55,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     modifier = Modifier.focusRestorer()
   ) {
     tabs.forEachIndexed { index, tab ->
@@ -89,7 +86,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     indicator = { tabPositions, doesTabRowHaveFocus ->
       TabRowDefaults.UnderlinedIndicator(
         currentTabPosition = tabPositions[selectedTabIndex],
@@ -108,7 +104,7 @@
           Text(
             text = tab,
             fontSize = 12.sp,
-            modifier = Modifier.padding(bottom = 4.dp)
+            modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
           )
         }
       }
@@ -137,7 +133,6 @@
 
   TabRow(
     selectedTabIndex = selectedTabIndex,
-    separator = { Spacer(modifier = Modifier.width(12.dp)) },
     modifier = Modifier.focusRestorer()
   ) {
     tabs.forEachIndexed { index, tab ->
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index e29475f..a20cb7d 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -33,13 +33,13 @@
     property public final androidx.tv.material3.Border None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonBorder {
+  @androidx.compose.runtime.Immutable public final class ButtonBorder {
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonColors {
+  @androidx.compose.runtime.Immutable public final class ButtonColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonDefaults {
+  public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -56,15 +56,15 @@
     field public static final androidx.tv.material3.ButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonGlow {
+  @androidx.compose.runtime.Immutable public final class ButtonGlow {
   }
 
   public final class ButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonScale {
+  @androidx.compose.runtime.Immutable public final class ButtonScale {
     field public static final androidx.tv.material3.ButtonScale.Companion Companion;
   }
 
@@ -73,7 +73,7 @@
     property public final androidx.tv.material3.ButtonScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
+  @androidx.compose.runtime.Immutable public final class ButtonShape {
   }
 
   @androidx.compose.runtime.Immutable public final class CardBorder {
@@ -384,7 +384,7 @@
     property public final androidx.tv.material3.Glow None;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class IconButtonDefaults {
+  public final class IconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -406,8 +406,8 @@
   }
 
   public final class IconButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
   public final class IconKt {
@@ -786,7 +786,7 @@
     field public static final androidx.tv.material3.NonInteractiveSurfaceDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedButtonDefaults {
+  public final class OutlinedButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -803,7 +803,7 @@
     field public static final androidx.tv.material3.OutlinedButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedIconButtonDefaults {
+  public final class OutlinedIconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -1005,8 +1005,8 @@
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
@@ -1076,10 +1076,10 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonContentColor {
+  @androidx.compose.runtime.Immutable public final class WideButtonContentColor {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonDefaults {
+  public final class WideButtonDefaults {
     method @androidx.compose.runtime.Composable public void Background(boolean enabled, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.WideButtonContentColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
@@ -1090,8 +1090,8 @@
   }
 
   public final class WideButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index e29475f..a20cb7d 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -33,13 +33,13 @@
     property public final androidx.tv.material3.Border None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonBorder {
+  @androidx.compose.runtime.Immutable public final class ButtonBorder {
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonColors {
+  @androidx.compose.runtime.Immutable public final class ButtonColors {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonDefaults {
+  public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -56,15 +56,15 @@
     field public static final androidx.tv.material3.ButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonGlow {
+  @androidx.compose.runtime.Immutable public final class ButtonGlow {
   }
 
   public final class ButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void Button(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonScale {
+  @androidx.compose.runtime.Immutable public final class ButtonScale {
     field public static final androidx.tv.material3.ButtonScale.Companion Companion;
   }
 
@@ -73,7 +73,7 @@
     property public final androidx.tv.material3.ButtonScale None;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class ButtonShape {
+  @androidx.compose.runtime.Immutable public final class ButtonShape {
   }
 
   @androidx.compose.runtime.Immutable public final class CardBorder {
@@ -384,7 +384,7 @@
     property public final androidx.tv.material3.Glow None;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class IconButtonDefaults {
+  public final class IconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -406,8 +406,8 @@
   }
 
   public final class IconButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void IconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void OutlinedIconButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.ButtonColors colors, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.interaction.MutableInteractionSource? interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> content);
   }
 
   public final class IconKt {
@@ -786,7 +786,7 @@
     field public static final androidx.tv.material3.NonInteractiveSurfaceDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedButtonDefaults {
+  public final class OutlinedButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public androidx.compose.foundation.layout.PaddingValues getButtonWithIconContentPadding();
@@ -803,7 +803,7 @@
     field public static final androidx.tv.material3.OutlinedButtonDefaults INSTANCE;
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class OutlinedIconButtonDefaults {
+  public final class OutlinedIconButtonDefaults {
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonColors colors(optional long containerColor, optional long contentColor, optional long focusedContainerColor, optional long focusedContentColor, optional long pressedContainerColor, optional long pressedContentColor, optional long disabledContainerColor, optional long disabledContentColor);
     method public float getLargeButtonSize();
@@ -1005,8 +1005,8 @@
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional int textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
@@ -1076,10 +1076,10 @@
     property public final androidx.compose.ui.text.TextStyle titleSmall;
   }
 
-  @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonContentColor {
+  @androidx.compose.runtime.Immutable public final class WideButtonContentColor {
   }
 
-  @SuppressCompatibility @androidx.tv.material3.ExperimentalTvMaterial3Api public final class WideButtonDefaults {
+  public final class WideButtonDefaults {
     method @androidx.compose.runtime.Composable public void Background(boolean enabled, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.ButtonBorder border(optional androidx.tv.material3.Border border, optional androidx.tv.material3.Border focusedBorder, optional androidx.tv.material3.Border pressedBorder, optional androidx.tv.material3.Border disabledBorder, optional androidx.tv.material3.Border focusedDisabledBorder);
     method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.WideButtonContentColor contentColor(optional long color, optional long focusedColor, optional long pressedColor, optional long disabledColor);
@@ -1090,8 +1090,8 @@
   }
 
   public final class WideButtonKt {
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.NonRestartableComposable public static void WideButton(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit>? onLongClick, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? subtitle, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional kotlin.jvm.functions.Function0<kotlin.Unit> background, optional androidx.tv.material3.ButtonScale scale, optional androidx.tv.material3.ButtonGlow glow, optional androidx.tv.material3.ButtonShape shape, optional androidx.tv.material3.WideButtonContentColor contentColor, optional float tonalElevation, optional androidx.tv.material3.ButtonBorder border, optional androidx.compose.foundation.layout.PaddingValues contentPadding);
   }
 
 }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
index 68299e8..88440a9 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/IconTest.kt
@@ -148,8 +148,8 @@
 
     @Test
     fun painter_noIntrinsicSize_dimensions() {
-        val width = 24.dp
-        val height = 24.dp
+        val width = 20.dp
+        val height = 20.dp
         val painter = ColorPainter(Color.Red)
         val testTag = "testTag"
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
index 089607a..1c1044f 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Button.kt
@@ -72,7 +72,6 @@
  * still happen internally.
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun Button(
@@ -146,7 +145,6 @@
  * still happen internally.
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun OutlinedButton(
@@ -181,7 +179,6 @@
     )
 }
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 @Composable
 private fun ButtonImpl(
     onClick: () -> Unit,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
index e147851..7c0ee76 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonDefaults.kt
@@ -32,7 +32,6 @@
     val MinHeight = 40.dp
 }
 
-@ExperimentalTvMaterial3Api
 object ButtonDefaults {
     private val ContainerShape = CircleShape
     private val ButtonHorizontalPadding = 16.dp
@@ -54,7 +53,7 @@
     )
 
     /** The default size of the icon when used inside any button. */
-    val IconSize = 18.dp
+    val IconSize = 20.dp
 
     /**
      * The default size of the spacing between an icon and a text when they used inside any button.
@@ -196,7 +195,6 @@
     )
 }
 
-@ExperimentalTvMaterial3Api
 object OutlinedButtonDefaults {
     private val ContainerShape = CircleShape
     private val ButtonHorizontalPadding = 16.dp
@@ -211,7 +209,7 @@
     )
 
     /** The default size of the icon when used inside any button. */
-    val IconSize = 18.dp
+    val IconSize = 20.dp
 
     /**
      * The default size of the spacing between an icon and a text when they used inside any button.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
index 554dea1..129a288 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/ButtonStyles.kt
@@ -25,7 +25,6 @@
 /**
  * Defines [Shape] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonShape internal constructor(
     internal val shape: Shape,
@@ -68,7 +67,6 @@
 /**
  * Defines [Color]s for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonColors internal constructor(
     internal val containerColor: Color,
@@ -124,7 +122,6 @@
 /**
  * Defines [Color]s for all TV [Interaction] states of a WideButton
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class WideButtonContentColor internal constructor(
     internal val contentColor: Color,
@@ -165,7 +162,6 @@
 /**
  * Defines the scale for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonScale internal constructor(
     @FloatRange(from = 0.0) internal val scale: Float,
@@ -221,7 +217,6 @@
 /**
  * Defines [Border] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonBorder internal constructor(
     internal val border: Border,
@@ -265,7 +260,6 @@
 /**
  * Defines [Glow] for all TV [Interaction] states of Button.
  */
-@ExperimentalTvMaterial3Api
 @Immutable
 class ButtonGlow internal constructor(
     internal val glow: Glow,
@@ -300,7 +294,6 @@
 
 private val WideButtonContainerColor = Color.Transparent
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonShape.toClickableSurfaceShape(): ClickableSurfaceShape = ClickableSurfaceShape(
     shape = shape,
     focusedShape = focusedShape,
@@ -309,7 +302,6 @@
     focusedDisabledShape = focusedDisabledShape
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonColors.toClickableSurfaceColors(): ClickableSurfaceColors =
     ClickableSurfaceColors(
         containerColor = containerColor,
@@ -322,7 +314,6 @@
         disabledContentColor = disabledContentColor
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun WideButtonContentColor.toClickableSurfaceColors(): ClickableSurfaceColors =
     ClickableSurfaceColors(
         containerColor = WideButtonContainerColor,
@@ -335,7 +326,6 @@
         disabledContentColor = disabledContentColor
     )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonScale.toClickableSurfaceScale() = ClickableSurfaceScale(
     scale = scale,
     focusedScale = focusedScale,
@@ -344,7 +334,6 @@
     focusedDisabledScale = focusedDisabledScale
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonBorder.toClickableSurfaceBorder() = ClickableSurfaceBorder(
     border = border,
     focusedBorder = focusedBorder,
@@ -353,7 +342,6 @@
     focusedDisabledBorder = focusedDisabledBorder
 )
 
-@OptIn(ExperimentalTvMaterial3Api::class)
 internal fun ButtonGlow.toClickableSurfaceGlow() = ClickableSurfaceGlow(
     glow = glow,
     focusedGlow = focusedGlow,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
index 8d8ce6c..4f4f2a5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Icon.kt
@@ -36,7 +36,6 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
-import androidx.compose.ui.unit.dp
 
 /**
  * A Material Design icon component that draws [imageVector] using [tint], with a default value
@@ -169,5 +168,4 @@
 private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
 
 // Default icon size, for icons with no intrinsic size information
-// TODO(rvighnesh): change this to IconButtonTokens.IconSize when we introduce IconButton
-private val DefaultIconSizeModifier = Modifier.size(24.dp)
+private val DefaultIconSizeModifier = Modifier.size(IconButtonDefaults.MediumIconSize)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
index 790d79b..8108044 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButton.kt
@@ -60,7 +60,6 @@
  * still happen internally.
  * @param content the content of the button, typically an [Icon]
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun IconButton(
@@ -130,7 +129,6 @@
  * still happen internally.
  * @param content the content of the button, typically an [Icon]
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun OutlinedIconButton(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
index d2a3f17..92efdc2 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/IconButtonDefaults.kt
@@ -26,7 +26,6 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.dp
 
-@ExperimentalTvMaterial3Api
 object IconButtonDefaults {
     private val ContainerShape = CircleShape
 
@@ -184,7 +183,6 @@
     )
 }
 
-@ExperimentalTvMaterial3Api
 object OutlinedIconButtonDefaults {
     private val ContainerShape = CircleShape
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
index b773532..6ef08dc 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
@@ -80,13 +80,14 @@
  * @param maxLines an optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
  * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
  * [TextLayoutResult] object that callback provides contains paragraph information, size of the
  * text, baselines and other details. The callback can be used to add additional decoration or
  * functionality to the text. For example, to draw selection around the text.
  * @param style style configuration for the text such as color, font, line height etc.
  */
-@ExperimentalTvMaterial3Api
 @Composable
 fun Text(
     text: String,
@@ -98,11 +99,12 @@
     fontFamily: FontFamily? = null,
     letterSpacing: TextUnit = TextUnit.Unspecified,
     textDecoration: TextDecoration? = null,
-    textAlign: TextAlign = TextAlign.Unspecified,
+    textAlign: TextAlign? = null,
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
@@ -120,7 +122,7 @@
                 color = textColor,
                 fontSize = fontSize,
                 fontWeight = fontWeight,
-                textAlign = textAlign,
+                textAlign = textAlign ?: TextAlign.Unspecified,
                 lineHeight = lineHeight,
                 fontFamily = fontFamily,
                 textDecoration = textDecoration,
@@ -130,7 +132,8 @@
         onTextLayout,
         overflow,
         softWrap,
-        maxLines
+        maxLines,
+        minLines
     )
 }
 
@@ -176,6 +179,8 @@
  * @param maxLines an optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
  * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param inlineContent a map storing composables that replaces certain ranges of the text, used to
  * insert composables into text layout. See [InlineTextContent].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
@@ -184,7 +189,6 @@
  * functionality to the text. For example, to draw selection around the text.
  * @param style style configuration for the text such as color, font, line height etc.
  */
-@ExperimentalTvMaterial3Api
 @Composable
 fun Text(
     text: AnnotatedString,
@@ -196,11 +200,12 @@
     fontFamily: FontFamily? = null,
     letterSpacing: TextUnit = TextUnit.Unspecified,
     textDecoration: TextDecoration? = null,
-    textAlign: TextAlign = TextAlign.Unspecified,
+    textAlign: TextAlign? = null,
     lineHeight: TextUnit = TextUnit.Unspecified,
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent> = mapOf(),
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
@@ -218,7 +223,7 @@
                 color = textColor,
                 fontSize = fontSize,
                 fontWeight = fontWeight,
-                textAlign = textAlign,
+                textAlign = textAlign ?: TextAlign.Unspecified,
                 lineHeight = lineHeight,
                 fontFamily = fontFamily,
                 textDecoration = textDecoration,
@@ -229,6 +234,7 @@
         overflow = overflow,
         softWrap = softWrap,
         maxLines = maxLines,
+        minLines = minLines,
         inlineContent = inlineContent
     )
 }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
index e59da65..36ec04c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/WideButton.kt
@@ -71,7 +71,6 @@
  * content
  * @param content the content of the button
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun WideButton(
@@ -144,7 +143,6 @@
  * @param contentPadding the spacing values to apply internally between the container and the
  * content
  */
-@ExperimentalTvMaterial3Api
 @NonRestartableComposable
 @Composable
 fun WideButton(
@@ -222,7 +220,6 @@
     }
 }
 
-@ExperimentalTvMaterial3Api
 @Composable
 private fun WideButtonImpl(
     onClick: () -> Unit,
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt b/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
index 07402ff..f4ca41c 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/WideButtonDefaults.kt
@@ -43,7 +43,6 @@
     val VerticalContentGap = 4.dp
 }
 
-@ExperimentalTvMaterial3Api
 object WideButtonDefaults {
     private val HorizontalPadding = 16.dp
     private val VerticalPadding = 10.dp
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
index 7091c76..49873f8 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/tokens/TypeScaleTokens.kt
@@ -34,7 +34,7 @@
     val BodySmallFont = TypefaceTokens.Plain
     val BodySmallLineHeight = 16.0.sp
     val BodySmallSize = 12.sp
-    val BodySmallTracking = 0.4.sp
+    val BodySmallTracking = 0.2.sp
     val BodySmallWeight = TypefaceTokens.WeightRegular
     val DisplayLargeFont = TypefaceTokens.Brand
     val DisplayLargeLineHeight = 64.0.sp
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 9d1b092..4982912 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -42,7 +42,7 @@
     implementation("androidx.compose.ui:ui-util:1.6.0")
     implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
     implementation("androidx.core:core:1.12.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     testImplementation(libs.testRules)
     testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt
new file mode 100644
index 0000000..de30899
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/CurvedTestTagTest.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2024 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.foundation
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import org.junit.Rule
+import org.junit.Test
+
+class CurvedTestTagTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun curvedBox_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedBox(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedRow_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedRow(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedColumn_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedColumn(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {
+                    curvedComposable {}
+                }
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+
+    @Test
+    fun curvedComposable_supports_testTag() {
+        rule.setContent {
+            CurvedLayout {
+                curvedComposable(
+                    modifier = CurvedModifier
+                        .testTag(TEST_TAG)
+                ) {}
+            }
+        }
+
+        rule.waitForIdle()
+        rule.onNodeWithTag(TEST_TAG).assertExists()
+    }
+}
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
index f781f3a..33312b2 100644
--- a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/FoundationTest.kt
@@ -16,11 +16,17 @@
 
 package androidx.wear.compose.foundation
 
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
+import androidx.compose.ui.semantics.semantics
 import kotlin.math.abs
 import kotlin.math.atan2
 import kotlin.math.min
@@ -68,6 +74,34 @@
     return histogram
 }
 
+/**
+ * Applies a tag to allow modified curved container element to be found in tests. This is similar to
+ * [Modifier.testTag], but specifically for curved containers.
+ *
+ * This is a convenience method for a [semantics] that sets [SemanticsPropertyReceiver.testTag].
+ * Currently, this supports basic assert operations operations only.
+ *
+ * @param tag The tag to apply to the curved container.
+ */
+public fun CurvedModifier.testTag(
+    tag: String
+) = this.then { child ->
+    TestTagWrapper(child, tag)
+}
+
+private class TestTagWrapper(
+    val child: CurvedChild,
+    val tag: String
+) : BaseCurvedChildWrapper(child) {
+
+    @Composable
+    override fun SubComposition() {
+        Box(modifier = Modifier.testTag(tag)) {
+            super.SubComposition()
+        }
+    }
+}
+
 internal fun checkSpy(dimensions: RadialDimensions, capturedInfo: CapturedInfo) =
     checkCurvedLayoutInfo(dimensions.asCurvedLayoutInfo(), capturedInfo.lastLayoutInfo!!)
 
diff --git a/wear/compose/compose-material-core/build.gradle b/wear/compose/compose-material-core/build.gradle
index 921a918..7f7f700 100644
--- a/wear/compose/compose-material-core/build.gradle
+++ b/wear/compose/compose-material-core/build.gradle
@@ -44,7 +44,7 @@
     implementation("androidx.compose.material:material-ripple:1.6.0")
     implementation("androidx.compose.ui:ui-util:1.6.0")
     implementation(project(":wear:compose:compose-foundation"))
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     androidTestImplementation(project(":compose:ui:ui-test"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 924b9f2..04c1634 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -43,7 +43,7 @@
     implementation(project(":compose:material:material-ripple"))
     implementation("androidx.compose.ui:ui-util:1.6.0")
     implementation(project(":wear:compose:compose-material-core"))
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
     implementation("androidx.lifecycle:lifecycle-common:2.7.0")
 
     androidTestImplementation(project(":compose:ui:ui-test"))
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
index 7e3e7444..665daf1 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/TouchExplorationStateProvider.kt
@@ -28,9 +28,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 
 /**
  * A functional interface for providing the state of touch exploration services. It is strongly
diff --git a/wear/compose/compose-material3/build.gradle b/wear/compose/compose-material3/build.gradle
index 6ba5202..955646f 100644
--- a/wear/compose/compose-material3/build.gradle
+++ b/wear/compose/compose-material3/build.gradle
@@ -44,7 +44,7 @@
     implementation(project(":compose:material:material-ripple"))
     implementation("androidx.compose.ui:ui-util:1.6.0")
     implementation(project(":wear:compose:compose-material-core"))
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     androidTestImplementation(project(":compose:ui:ui-test"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
index 5155500..f88b805 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Ripple.kt
@@ -242,8 +242,8 @@
 }
 
 private val RippleAlpha: RippleAlpha = RippleAlpha(
-    pressedAlpha = 0.12f,
-    focusedAlpha = 0.12f,
+    pressedAlpha = 0.10f,
+    focusedAlpha = 0.10f,
     draggedAlpha = 0.16f,
     hoveredAlpha = 0.08f
 )
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 991acdb..bcf5a0e 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -41,7 +41,7 @@
     implementation(libs.kotlinStdlib)
     implementation("androidx.navigation:navigation-common:2.6.0")
     implementation("androidx.navigation:navigation-compose:2.6.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.3.0")
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
 
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index 7a0c478..9a3ef1a 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -58,7 +58,6 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.semantics.clearAndSetSemantics
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
@@ -67,6 +66,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.wear.compose.integration.demos.common.rsbScroll
 import androidx.wear.compose.material.Button
 import androidx.wear.compose.material.Icon
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
index 72e0f87..b467aa7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoolNodes.java
@@ -54,6 +54,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic boolean node that gets value from the state. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
index b0babff..3a1cc92 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicType.java
@@ -64,4 +64,13 @@
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     @VisibleForTesting
     int getDynamicNodeCount();
+
+    /**
+     * Returns the cost of dynamic nodes that this dynamic type contains. See {@link
+     * DynamicDataNode#getCost()} for more details on node cost.
+     */
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @VisibleForTesting
+    int getDynamicNodeCost();
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 4481752..33a7b2e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -84,6 +84,11 @@
     }
 
     @Override
+    public int getDynamicNodeCost() {
+        return mNodes.stream().mapToInt(DynamicDataNode::getCost).sum();
+    }
+
+    @Override
     public void close() {
         if (Looper.getMainLooper().isCurrentThread()) {
             closeInternal();
@@ -106,6 +111,6 @@
         mNodes.stream()
                 .filter(n -> n instanceof DynamicDataSourceNode)
                 .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
-        mDynamicDataNodesQuotaManager.releaseQuota(getDynamicNodeCount());
+        mDynamicDataNodesQuotaManager.releaseQuota(getDynamicNodeCost());
     }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
index c2a1d58..afc44fe 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ColorNodes.java
@@ -35,8 +35,7 @@
         private final DynamicTypeValueReceiverWithPreUpdate<Integer> mDownstream;
 
         FixedColorNode(
-                FixedColor protoNode,
-                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
+                FixedColor protoNode, DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
             this.mValue = protoNode.getArgb();
             this.mDownstream = downstream;
         }
@@ -55,6 +54,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic color node that gets value from the platform source. */
@@ -120,6 +124,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic color node that gets animatable value from dynamic source. */
@@ -203,5 +212,10 @@
         public DynamicTypeValueReceiverWithPreUpdate<Integer> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
index d0884d1..0817f67 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/ConditionalOpNode.java
@@ -190,4 +190,9 @@
             mDownstream.onData(mLastFalseValue);
         }
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
index d59ecea..92e907f 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DurationNodes.java
@@ -54,6 +54,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic duration node that gets the duration between two time instants. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
index 5bcb602..41d8206 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataBiTransformNode.java
@@ -76,6 +76,11 @@
         this.mTransformer = transformer;
     }
 
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
+
     private class UpstreamCallback<T> implements DynamicTypeValueReceiverWithPreUpdate<T> {
         private boolean mUpstreamPreUpdated = false;
 
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
index d845acb..34e38f7 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataNode.java
@@ -16,6 +16,8 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import androidx.annotation.RestrictTo;
+
 /**
  * Node within a dynamic data pipeline.
  *
@@ -56,4 +58,17 @@
  *
  * @param <O> The data type that this node yields.
  */
-interface DynamicDataNode<O> {}
+interface DynamicDataNode<O> {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int DEFAULT_NODE_COST = 1;
+
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int FIXED_NODE_COST = 0;
+
+    /**
+     * Returns the cost of this node. This value is used to estimate performance impact of a node.
+     * By default, most nodes have a cost of {@link DynamicDataNode#DEFAULT_NODE_COST}.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    int getCost();
+}
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
index 441ee6d..1a8b456 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataSourceNode.java
@@ -28,8 +28,8 @@
 interface DynamicDataSourceNode<T> extends DynamicDataNode<T> {
     /**
      * Called on all source nodes before {@link DynamicDataSourceNode#init()} is called on any node.
-     * This should generally only call {@link
-     * DynamicTypeValueReceiverWithPreUpdate#onPreUpdate()} on all downstream nodes.
+     * This should generally only call {@link DynamicTypeValueReceiverWithPreUpdate#onPreUpdate()}
+     * on all downstream nodes.
      */
     @UiThread
     void preInit();
@@ -44,4 +44,9 @@
     /** Destroy this node. This should cause it to unbind from any data sources. */
     @UiThread
     void destroy();
+
+    @Override
+    default int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
index 160d4bc..0024b26 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
@@ -75,4 +75,9 @@
     public DynamicTypeValueReceiverWithPreUpdate<I> getIncomingCallback() {
         return mCallback;
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 8e73af5..6229642 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -117,9 +117,9 @@
                 public boolean tryAcquireQuota(int quota) {
                     return true;
                 }
+
                 @Override
-                public void releaseQuota(int quota) {
-                }
+                public void releaseQuota(int quota) {}
             };
 
     @NonNull
@@ -389,7 +389,7 @@
     public BoundDynamicType bind(@NonNull DynamicTypeBindingRequest request)
             throws EvaluationException {
         BoundDynamicTypeImpl boundDynamicType = request.callBindOn(this);
-        if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
+        if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCost())) {
             throw new EvaluationException(
                     "Dynamic type expression limit reached. Try making the dynamic type expression"
                             + " shorter or reduce the number of dynamic type expressions.");
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
index 333eec4..9ec6b85 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -64,6 +64,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic float node that gets value from the state. */
@@ -183,6 +188,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic float node that gets animatable value from dynamic source. */
@@ -265,6 +275,11 @@
         public DynamicTypeValueReceiverWithPreUpdate<Float> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     private static boolean isValid(Float value) {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
index c79546b..aa3a242 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/InstantNodes.java
@@ -53,6 +53,11 @@
 
         @Override
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic Instant node that gets value from the platform source. */
@@ -97,6 +102,11 @@
                 mEpochTimePlatformDataSource.unregisterForData(mDownstream);
             }
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic Instant node that gets value from the state. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
index 64e8513..e32fd70 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -75,6 +75,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic integer node that gets value from the platform source. */
@@ -288,6 +293,11 @@
         public void destroy() {
             mQuotaAwareAnimator.stopAnimator();
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic int32 node that gets animatable value from dynamic source. */
@@ -370,6 +380,11 @@
         public DynamicTypeValueReceiverWithPreUpdate<Integer> getInputCallback() {
             return mInputCallback;
         }
+
+        @Override
+        public int getCost() {
+            return DEFAULT_NODE_COST;
+        }
     }
 
     /** Dynamic integer node that gets date-time part from a zoned date-time. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
index 98da705..d232317 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateSourceNode.java
@@ -104,4 +104,9 @@
 
         return new PlatformDataKey<T>(namespace, key);
     }
+
+    @Override
+    public int getCost() {
+        return DEFAULT_NODE_COST;
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
index 1978b04..fa65764 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StringNodes.java
@@ -56,6 +56,11 @@
         @Override
         @UiThread
         public void destroy() {}
+
+        @Override
+        public int getCost() {
+            return FIXED_NODE_COST;
+        }
     }
 
     /** Dynamic string node that gets a value from integer. */
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
index dca1d9e..0abb008 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
@@ -28,6 +28,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
 import androidx.wear.protolayout.expression.PlatformDataKey;
@@ -54,7 +55,7 @@
         ArrayList<Boolean> results = new ArrayList<>();
         DynamicTypeBindingRequest request = createSingleNodeDynamicBoolRequest(results);
         BoundDynamicType boundDynamicType = evaluator.bind(request);
-        assertThat(boundDynamicType.getDynamicNodeCount()).isEqualTo(1);
+        assertThat(boundDynamicType.getDynamicNodeCost()).isEqualTo(1);
     }
 
     @Test
@@ -114,7 +115,7 @@
         boundDynamicType1.close();
         // Retry binding request2
         BoundDynamicType boundDynamicType2 = evaluator.bind(request2);
-        assertThat(boundDynamicType2.getDynamicNodeCount()).isEqualTo(1);
+        assertThat(boundDynamicType2.getDynamicNodeCost()).isEqualTo(1);
     }
 
     @Test
@@ -155,10 +156,14 @@
     @NonNull
     private static DynamicTypeBindingRequest createSingleNodeDynamicBoolRequest(
             ArrayList<Boolean> results) {
+        return createDynamicBoolRequest(DynamicBool.from(new AppDataKey<>("key")), results);
+    }
+
+    @NonNull
+    private static DynamicTypeBindingRequest createDynamicBoolRequest(
+            DynamicBuilders.DynamicBool dynamicBool, ArrayList<Boolean> results) {
         return DynamicTypeBindingRequest.forDynamicBool(
-                DynamicBool.constant(false),
-                new MainThreadExecutor(),
-                new AddToListCallback<>(results));
+                dynamicBool, new MainThreadExecutor(), new AddToListCallback<>(results));
     }
 
     @NonNull
diff --git a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/Chip.java b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/Chip.java
index 2f2a5d5..97cb0f0 100644
--- a/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/Chip.java
+++ b/wear/protolayout/protolayout-material-core/src/main/java/androidx/wear/protolayout/materialcore/Chip.java
@@ -345,8 +345,15 @@
             if (mWidth instanceof DpProp) {
                 return dp(max(((DpProp) mWidth).getValue(), mMinTappableSquareLength.getValue()));
             } else if (mWidth instanceof WrappedDimensionProp) {
+                WrappedDimensionProp widthWrap = ((WrappedDimensionProp) mWidth);
                 return new WrappedDimensionProp.Builder()
-                        .setMinimumSize(mMinTappableSquareLength)
+                        .setMinimumSize(
+                                dp(
+                                        max(
+                                                widthWrap.getMinimumSize() != null
+                                                        ? widthWrap.getMinimumSize().getValue()
+                                                        : 0,
+                                                mMinTappableSquareLength.getValue())))
                         .build();
             } else {
                 return mWidth;
@@ -389,6 +396,13 @@
                 return mCustomContent;
             }
 
+            if (mPrimaryLabelContent == null
+                    && mSecondaryLabelContent == null
+                    && mIconContent != null) {
+                // Icon only variant of chip.
+                return mIconContent;
+            }
+
             Column.Builder column =
                     new Column.Builder()
                             .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
@@ -483,7 +497,12 @@
         if (!getMetadataTag().equals(METADATA_TAG_ICON)) {
             return null;
         }
-        return ((Row) mElement.getContents().get(0)).getContents().get(0);
+        // TODO(b/330165026): Refactor to use bit in the metadata tag like layouts do, instead of
+        // relying on the null here. The primary label can be null in case of icon only CompactChip.
+        LayoutElement topLevel = mElement.getContents().get(0);
+        return topLevel instanceof Row
+                ? ((Row) mElement.getContents().get(0)).getContents().get(0)
+                : topLevel;
     }
 
     @Nullable
@@ -496,6 +515,11 @@
         // In any other case, text (either primary or primary + label) must be present.
         Column content;
         if (metadataTag.equals(METADATA_TAG_ICON)) {
+            if (!(mElement.getContents().get(0) instanceof Row)) {
+                // This is icon only Chip, no label.
+                return null;
+            }
+
             content =
                     (Column)
                             ((Box)
diff --git a/wear/protolayout/protolayout-material/api/current.txt b/wear/protolayout/protolayout-material/api/current.txt
index 38c33fb..fda934c 100644
--- a/wear/protolayout/protolayout-material/api/current.txt
+++ b/wear/protolayout/protolayout-material/api/current.txt
@@ -138,15 +138,20 @@
     method public static androidx.wear.protolayout.material.CompactChip? fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
     method public androidx.wear.protolayout.material.ChipColors getChipColors();
     method public androidx.wear.protolayout.ModifiersBuilders.Clickable getClickable();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getContentDescription();
     method public String? getIconContent();
     method public String getText();
   }
 
   public static final class CompactChip.Builder {
+    ctor public CompactChip.Builder(android.content.Context, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     ctor public CompactChip.Builder(android.content.Context, String, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     method public androidx.wear.protolayout.material.CompactChip build();
     method public androidx.wear.protolayout.material.CompactChip.Builder setChipColors(androidx.wear.protolayout.material.ChipColors);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.CompactChip.Builder setIconContent(String);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setTextContent(String);
   }
 
   public class ProgressIndicatorColors {
@@ -198,6 +203,7 @@
     method public static androidx.wear.protolayout.material.TitleChip? fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
     method public androidx.wear.protolayout.material.ChipColors getChipColors();
     method public androidx.wear.protolayout.ModifiersBuilders.Clickable getClickable();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getContentDescription();
     method public int getHorizontalAlignment();
     method public String? getIconContent();
     method public String getText();
@@ -208,6 +214,8 @@
     ctor public TitleChip.Builder(android.content.Context, String, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     method public androidx.wear.protolayout.material.TitleChip build();
     method public androidx.wear.protolayout.material.TitleChip.Builder setChipColors(androidx.wear.protolayout.material.ChipColors);
+    method public androidx.wear.protolayout.material.TitleChip.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.material.TitleChip.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.TitleChip.Builder setHorizontalAlignment(int);
     method public androidx.wear.protolayout.material.TitleChip.Builder setIconContent(String);
     method public androidx.wear.protolayout.material.TitleChip.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
diff --git a/wear/protolayout/protolayout-material/api/restricted_current.txt b/wear/protolayout/protolayout-material/api/restricted_current.txt
index 38c33fb..fda934c 100644
--- a/wear/protolayout/protolayout-material/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material/api/restricted_current.txt
@@ -138,15 +138,20 @@
     method public static androidx.wear.protolayout.material.CompactChip? fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
     method public androidx.wear.protolayout.material.ChipColors getChipColors();
     method public androidx.wear.protolayout.ModifiersBuilders.Clickable getClickable();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getContentDescription();
     method public String? getIconContent();
     method public String getText();
   }
 
   public static final class CompactChip.Builder {
+    ctor public CompactChip.Builder(android.content.Context, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     ctor public CompactChip.Builder(android.content.Context, String, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     method public androidx.wear.protolayout.material.CompactChip build();
     method public androidx.wear.protolayout.material.CompactChip.Builder setChipColors(androidx.wear.protolayout.material.ChipColors);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.CompactChip.Builder setIconContent(String);
+    method public androidx.wear.protolayout.material.CompactChip.Builder setTextContent(String);
   }
 
   public class ProgressIndicatorColors {
@@ -198,6 +203,7 @@
     method public static androidx.wear.protolayout.material.TitleChip? fromLayoutElement(androidx.wear.protolayout.LayoutElementBuilders.LayoutElement);
     method public androidx.wear.protolayout.material.ChipColors getChipColors();
     method public androidx.wear.protolayout.ModifiersBuilders.Clickable getClickable();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp? getContentDescription();
     method public int getHorizontalAlignment();
     method public String? getIconContent();
     method public String getText();
@@ -208,6 +214,8 @@
     ctor public TitleChip.Builder(android.content.Context, String, androidx.wear.protolayout.ModifiersBuilders.Clickable, androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters);
     method public androidx.wear.protolayout.material.TitleChip build();
     method public androidx.wear.protolayout.material.TitleChip.Builder setChipColors(androidx.wear.protolayout.material.ChipColors);
+    method public androidx.wear.protolayout.material.TitleChip.Builder setContentDescription(androidx.wear.protolayout.TypeBuilders.StringProp);
+    method public androidx.wear.protolayout.material.TitleChip.Builder setContentDescription(CharSequence);
     method public androidx.wear.protolayout.material.TitleChip.Builder setHorizontalAlignment(int);
     method public androidx.wear.protolayout.material.TitleChip.Builder setIconContent(String);
     method public androidx.wear.protolayout.material.TitleChip.Builder setWidth(androidx.wear.protolayout.DimensionBuilders.ContainerDimension);
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 92ec2f7..a5c592e 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -218,6 +218,12 @@
                         .build());
         testCases.put(
                 "compactchip_custom_default_golden" + goldenSuffix,
+                new CompactChip.Builder(context, clickable, deviceParameters)
+                        .setTextContent("Action")
+                        .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
+                        .build());
+        testCases.put(
+                "compactchip_custom_default_deprecated_golden" + goldenSuffix,
                 new CompactChip.Builder(context, "Action", clickable, deviceParameters)
                         .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
                         .build());
@@ -243,6 +249,11 @@
                         .setIconContent(ICON_ID_SMALL)
                         .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
                         .build());
+        testCases.put(
+                "compactchip_icon_only_golden" + goldenSuffix,
+                new CompactChip.Builder(context, clickable, deviceParameters)
+                        .setIconContent(ICON_ID_SMALL)
+                        .build());
 
         testCases.put(
                 "titlechip_default_golden" + goldenSuffix,
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Chip.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Chip.java
index c0854ac..2b144b2 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Chip.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/Chip.java
@@ -103,6 +103,7 @@
         @NonNull private final Context mContext;
         @Nullable private LayoutElement mCustomContent;
         @Nullable private String mImageResourceId = null;
+        private boolean mIsIconOnly = false;
         @Nullable private String mPrimaryLabel = null;
         @Nullable private String mSecondaryLabel = null;
         @Nullable private StringProp mContentDescription = null;
@@ -201,6 +202,7 @@
         @NonNull
         public Builder setPrimaryLabelContent(@NonNull String primaryLabel) {
             this.mPrimaryLabel = primaryLabel;
+            this.mIsIconOnly = false;
             this.mCustomContent = null;
             return this;
         }
@@ -246,6 +248,7 @@
         @NonNull
         public Builder setSecondaryLabelContent(@NonNull String secondaryLabel) {
             this.mSecondaryLabel = secondaryLabel;
+            this.mIsIconOnly = false;
             this.mCustomContent = null;
             return this;
         }
@@ -264,6 +267,20 @@
         }
 
         /**
+         * Sets the content of the {@link Chip} to be *only* icon. Any previously added custom
+         * content will be overridden. Provided icon will be tinted to the given content color from
+         * {@link ChipColors}. This icon should be image with chosen alpha channel and not an actual
+         * image. Should be used only for creating a {@link CompactChip}.
+         */
+        @NonNull
+        Builder setIconOnlyContent(@NonNull String imageResourceId) {
+            this.mImageResourceId = imageResourceId;
+            this.mIsIconOnly = true;
+            this.mCustomContent = null;
+            return this;
+        }
+
+        /**
          * Sets the colors for the {@link Chip}. If set, {@link ChipColors#getBackgroundColor()}
          * will be used for the background of the button, {@link ChipColors#getContentColor()} for
          * main text, {@link ChipColors#getSecondaryContentColor()} for label text and {@link
@@ -356,6 +373,24 @@
         @OptIn(markerClass = ProtoLayoutExperimental.class)
         @SuppressWarnings("deprecation")
         private void setCorrectContent() {
+            if (mImageResourceId != null) {
+                Image icon =
+                        new Image.Builder()
+                                .setResourceId(mImageResourceId)
+                                .setWidth(mIconSize)
+                                .setHeight(mIconSize)
+                                .setColorFilter(
+                                        new ColorFilter.Builder()
+                                                .setTint(mChipColors.getIconColor())
+                                                .build())
+                                .build();
+                mCoreBuilder.setIconContent(icon);
+
+                if (mIsIconOnly) {
+                    return;
+                }
+            }
+
             Text mainTextElement =
                     new Text.Builder(mContext, checkNotNull(mPrimaryLabel))
                             .setTypography(mPrimaryLabelTypography)
@@ -379,20 +414,6 @@
                                 .build();
                 mCoreBuilder.setSecondaryLabelContent(labelTextElement);
             }
-
-            if (mImageResourceId != null) {
-                Image icon =
-                        new Image.Builder()
-                                .setResourceId(mImageResourceId)
-                                .setWidth(mIconSize)
-                                .setHeight(mIconSize)
-                                .setColorFilter(
-                                        new ColorFilter.Builder()
-                                                .setTint(mChipColors.getIconColor())
-                                                .build())
-                                .build();
-                mCoreBuilder.setIconContent(icon);
-            }
         }
 
         private int getCorrectMaxLines() {
@@ -435,10 +456,14 @@
                 iconTintColor = checkNotNull(checkNotNull(icon.getColorFilter()).getTint());
             }
 
-            contentColor = checkNotNull(getPrimaryLabelContentObject()).getColor();
-            Text label = getSecondaryLabelContentObject();
-            if (label != null) {
-                secondaryContentColor = label.getColor();
+
+            Text maybePrimaryLabel = getPrimaryLabelContentObject();
+            if (maybePrimaryLabel != null) {
+                contentColor = checkNotNull(maybePrimaryLabel).getColor();
+                Text label = getSecondaryLabelContentObject();
+                if (label != null) {
+                    secondaryContentColor = label.getColor();
+                }
             }
         }
 
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/ChipDefaults.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/ChipDefaults.java
index d5cfbe1..0441297 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/ChipDefaults.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/ChipDefaults.java
@@ -37,6 +37,11 @@
     @NonNull
     public static final DpProp COMPACT_HEIGHT = dp(32);
 
+    /** The default minimum width for standard {@link CompactChip} */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @NonNull
+    public static final DpProp COMPACT_MIN_WIDTH = dp(52);
+
     /** The minimum size of tappable target area. */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CompactChip.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CompactChip.java
index 9787fa6..40ac894 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CompactChip.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/CompactChip.java
@@ -16,14 +16,17 @@
 
 package androidx.wear.protolayout.material;
 
+import static androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp;
 import static androidx.wear.protolayout.DimensionBuilders.wrap;
 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
 import static androidx.wear.protolayout.material.ChipDefaults.COMPACT_HEIGHT;
 import static androidx.wear.protolayout.material.ChipDefaults.COMPACT_HORIZONTAL_PADDING;
 import static androidx.wear.protolayout.material.ChipDefaults.COMPACT_ICON_SIZE;
+import static androidx.wear.protolayout.material.ChipDefaults.COMPACT_MIN_WIDTH;
 import static androidx.wear.protolayout.material.ChipDefaults.COMPACT_PRIMARY_COLORS;
 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull;
+import static androidx.wear.protolayout.materialcore.Helper.staticString;
 
 import android.content.Context;
 
@@ -35,6 +38,7 @@
 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
 import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.TypeBuilders.StringProp;
 import androidx.wear.protolayout.expression.Fingerprint;
 import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
 import androidx.wear.protolayout.proto.LayoutElementProto;
@@ -79,14 +83,15 @@
     /** Builder class for {@link androidx.wear.protolayout.material.CompactChip}. */
     public static final class Builder implements LayoutElement.Builder {
         @NonNull private final Context mContext;
-        @NonNull private final String mText;
+        @Nullable private String mText;
         @NonNull private final Clickable mClickable;
         @NonNull private final DeviceParameters mDeviceParameters;
         @NonNull private ChipColors mChipColors = COMPACT_PRIMARY_COLORS;
         @Nullable private String mIconResourceId = null;
+        @Nullable private StringProp mContentDescription = null;
 
         /**
-         * Creates a builder for the {@link CompactChip} with associated action and the given text
+         * Creates a builder for the {@link CompactChip} with associated action and the given text.
          *
          * @param context The application's context.
          * @param text The text to be displayed in this compact chip.
@@ -106,6 +111,32 @@
         }
 
         /**
+         * Creates a builder for the {@link CompactChip} with associated action. Please add text,
+         * icon or both content with {@link #setTextContent} and {@link #setIconContent}.
+         *
+         * @param context The application's context.
+         * @param clickable Associated {@link Clickable} for click events. When the CompactChip is
+         *     clicked it will fire the associated action.
+         * @param deviceParameters The device parameters used for styling text.
+         */
+        public Builder(
+                @NonNull Context context,
+                @NonNull Clickable clickable,
+                @NonNull DeviceParameters deviceParameters) {
+            this.mContext = context;
+            this.mClickable = clickable;
+            this.mDeviceParameters = deviceParameters;
+        }
+
+        /** Sets the text for the {@link CompactChip}. */
+        @SuppressWarnings("MissingGetterMatchingBuilder") // Exists as getText
+        @NonNull
+        public Builder setTextContent(@NonNull String text) {
+            this.mText = text;
+            return this;
+        }
+
+        /**
          * Sets the colors for the {@link CompactChip}. If set, {@link
          * ChipColors#getBackgroundColor()} will be used for the background of the button and {@link
          * ChipColors#getContentColor()} for the text. If not set, {@link
@@ -128,33 +159,82 @@
             return this;
         }
 
+        /**
+         * Sets the static content description for the {@link CompactChip}. It is highly recommended
+         * to provide this for chip containing an icon.
+         */
+        @NonNull
+        public Builder setContentDescription(@NonNull CharSequence contentDescription) {
+            return setContentDescription(staticString(contentDescription.toString()));
+        }
+
+        /**
+         * Sets the content description for the {@link CompactChip}. It is highly recommended to
+         * provide this for chip containing an icon.
+         *
+         * <p>While this field is statically accessible from 1.0, it's only bindable since version
+         * 1.2 and renderers supporting version 1.2 will use the dynamic value (if set).
+         */
+        @NonNull
+        public Builder setContentDescription(@NonNull StringProp contentDescription) {
+            this.mContentDescription = contentDescription;
+            return this;
+        }
+
+
         /** Constructs and returns {@link CompactChip} with the provided content and look. */
         @NonNull
         @Override
         @OptIn(markerClass = ProtoLayoutExperimental.class)
         public CompactChip build() {
+            if (mText == null && mIconResourceId == null) {
+                throw new IllegalArgumentException("At least one of text or icon must be set.");
+            }
+
             Chip.Builder chipBuilder =
                     new Chip.Builder(mContext, mClickable, mDeviceParameters)
                             .setChipColors(mChipColors)
-                            .setContentDescription(mText)
+                            .setContentDescription(
+                                    mContentDescription == null
+                                            ? staticString(mText == null ? "" : mText)
+                                            : mContentDescription)
                             .setHorizontalAlignment(getCorrectHorizontalAlignment())
-                            .setWidth(wrap())
+                            .setWidth(resolveWidth())
                             .setHeight(COMPACT_HEIGHT)
                             .setMaxLines(1)
-                            .setHorizontalPadding(COMPACT_HORIZONTAL_PADDING)
-                            .setPrimaryLabelContent(mText)
-                            .setPrimaryLabelTypography(Typography.TYPOGRAPHY_CAPTION1)
-                            .setIsPrimaryLabelScalable(false);
+                            .setHorizontalPadding(COMPACT_HORIZONTAL_PADDING);
+
+            if (mText != null) {
+                chipBuilder
+                        .setPrimaryLabelContent(mText)
+                        .setPrimaryLabelTypography(Typography.TYPOGRAPHY_CAPTION1)
+                        .setIsPrimaryLabelScalable(false);
+            }
 
             if (mIconResourceId != null) {
-                chipBuilder.setIconContent(mIconResourceId).setIconSize(COMPACT_ICON_SIZE);
+                if (mText != null) {
+                    chipBuilder.setIconContent(mIconResourceId);
+                } else {
+                    chipBuilder.setIconOnlyContent(mIconResourceId);
+                }
+                chipBuilder.setIconSize(COMPACT_ICON_SIZE);
             }
 
             return new CompactChip(chipBuilder.build());
         }
 
+        private WrappedDimensionProp resolveWidth() {
+            // Min width applies to icon only CompactChip.
+            return mText == null
+                    // Icon only CompactChip.
+                    ? new WrappedDimensionProp.Builder().setMinimumSize(COMPACT_MIN_WIDTH).build()
+                    : wrap();
+        }
+
         private int getCorrectHorizontalAlignment() {
-            return mIconResourceId == null ? HORIZONTAL_ALIGN_CENTER : HORIZONTAL_ALIGN_START;
+            return mIconResourceId == null || mText == null
+                    ? HORIZONTAL_ALIGN_CENTER
+                    : HORIZONTAL_ALIGN_START;
         }
     }
 
@@ -170,7 +250,12 @@
         return mElement.getChipColors();
     }
 
-    /** Returns text content of this Chip. */
+    /**
+     * Returns text content of this Chip if it was set. If the text content wasn't set (either with
+     * {@link Builder#setTextContent} or constructor, this method will throw.
+     *
+     * @throws NullPointerException when no text content was set to the chip.
+     */
     @NonNull
     public String getText() {
         return checkNotNull(mElement.getPrimaryLabelContent());
@@ -203,6 +288,12 @@
         return coreChip == null ? null : new CompactChip(new Chip(coreChip));
     }
 
+    /** Returns content description of this CompactChip. */
+    @Nullable
+    public StringProp getContentDescription() {
+        return mElement.getContentDescription();
+    }
+
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
diff --git a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/TitleChip.java b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/TitleChip.java
index 25de00b..92140a6 100644
--- a/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/TitleChip.java
+++ b/wear/protolayout/protolayout-material/src/main/java/androidx/wear/protolayout/material/TitleChip.java
@@ -24,6 +24,7 @@
 import static androidx.wear.protolayout.material.ChipDefaults.TITLE_HORIZONTAL_PADDING;
 import static androidx.wear.protolayout.material.ChipDefaults.TITLE_PRIMARY_COLORS;
 import static androidx.wear.protolayout.materialcore.Helper.checkNotNull;
+import static androidx.wear.protolayout.materialcore.Helper.staticString;
 
 import android.content.Context;
 
@@ -38,6 +39,7 @@
 import androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignment;
 import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
 import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.TypeBuilders.StringProp;
 import androidx.wear.protolayout.expression.Fingerprint;
 import androidx.wear.protolayout.expression.ProtoLayoutExperimental;
 import androidx.wear.protolayout.proto.LayoutElementProto;
@@ -89,6 +91,7 @@
         @NonNull private final DeviceParameters mDeviceParameters;
         @NonNull private ChipColors mChipColors = TITLE_PRIMARY_COLORS;
         @HorizontalAlignment private int mHorizontalAlign = HORIZONTAL_ALIGN_UNDEFINED;
+        @Nullable private StringProp mContentDescription = null;
 
         // Indicates that the width isn't set, so it will be automatically set by Chip.Builder
         // constructor.
@@ -166,6 +169,28 @@
             return this;
         }
 
+        /**
+         * Sets the static content description for the {@link TitleChip}. It is highly recommended
+         * to provide this for chip containing an icon.
+         */
+        @NonNull
+        public Builder setContentDescription(@NonNull CharSequence contentDescription) {
+            return setContentDescription(staticString(contentDescription.toString()));
+        }
+
+        /**
+         * Sets the content description for the {@link TitleChip}. It is highly recommended to
+         * provide this for chip containing an icon.
+         *
+         * <p>While this field is statically accessible from 1.0, it's only bindable since version
+         * 1.2 and renderers supporting version 1.2 will use the dynamic value (if set).
+         */
+        @NonNull
+        public Builder setContentDescription(@NonNull StringProp contentDescription) {
+            this.mContentDescription = contentDescription;
+            return this;
+        }
+
         /** Constructs and returns {@link TitleChip} with the provided content and look. */
         @NonNull
         @Override
@@ -174,7 +199,10 @@
             Chip.Builder chipBuilder =
                     new Chip.Builder(mContext, mClickable, mDeviceParameters)
                             .setChipColors(mChipColors)
-                            .setContentDescription(mText)
+                            .setContentDescription(
+                                    mContentDescription == null
+                                            ? staticString(mText)
+                                            : mContentDescription)
                             .setHeight(TITLE_HEIGHT)
                             .setMaxLines(1)
                             .setHorizontalPadding(TITLE_HORIZONTAL_PADDING)
@@ -255,6 +283,12 @@
         return coreChip == null ? null : new TitleChip(new Chip(coreChip));
     }
 
+    /** Returns content description of this TitleChip. */
+    @Nullable
+    public StringProp getContentDescription() {
+        return mElement.getContentDescription();
+    }
+
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
     @Override
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
index 173cce2..81025a7 100644
--- a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/CompactChipTest.java
@@ -22,15 +22,19 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
+
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import android.content.Context;
 import android.graphics.Color;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.ColorBuilders.ColorProp;
 import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
 import androidx.wear.protolayout.LayoutElementBuilders.Box;
 import androidx.wear.protolayout.LayoutElementBuilders.Column;
@@ -59,31 +63,64 @@
     @Test
     public void testCompactChipDefault() {
         CompactChip compactChip =
-                new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS).build();
+                new CompactChip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setTextContent(MAIN_TEXT)
+                        .build();
 
         assertChip(compactChip, ChipDefaults.COMPACT_PRIMARY_COLORS, /* iconId= */ null);
+        assertThat(compactChip.getText()).isEqualTo(MAIN_TEXT);
     }
 
     @Test
     public void testCompactChipCustomColor() {
         CompactChip compactChip =
-                new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+                new CompactChip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setTextContent(MAIN_TEXT)
                         .setChipColors(COLORS)
                         .build();
 
         assertChip(compactChip, COLORS, /* iconId= */ null);
+        assertThat(compactChip.getText()).isEqualTo(MAIN_TEXT);
     }
 
     @Test
     public void testCompactChipIconCustomColor() {
         String iconId = "icon_id";
+        String description = "This is CompactChip with icon and text";
         CompactChip compactChip =
-                new CompactChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
+                new CompactChip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
                         .setChipColors(COLORS)
+                        .setTextContent(MAIN_TEXT)
                         .setIconContent(iconId)
+                        .setContentDescription(description)
                         .build();
 
-        assertChip(compactChip, COLORS, /* iconId= */ iconId);
+        assertChip(compactChip, COLORS, /* iconId= */ iconId, description);
+        assertThat(compactChip.getText()).isEqualTo(MAIN_TEXT);
+    }
+
+    @Test
+    public void testCompactChipIconOnly() {
+        String iconId = "icon_id";
+        String description = "This is CompactChip with icon only";
+        ColorProp defaultColor = new ColorProp.Builder(0).build();
+        CompactChip compactChip =
+                new CompactChip.Builder(CONTEXT, CLICKABLE, DEVICE_PARAMETERS)
+                        .setChipColors(COLORS)
+                        .setIconContent(iconId)
+                        .setContentDescription(description)
+                        .build();
+
+        assertChip(
+                compactChip,
+                new ChipColors(
+                        COLORS.getBackgroundColor(),
+                        COLORS.getIconColor(),
+                        defaultColor,
+                        defaultColor),
+                iconId,
+                description);
+        assertThrows(NullPointerException.class, compactChip::getText);
     }
 
     @Test
@@ -118,28 +155,43 @@
 
     private void assertChip(
             CompactChip actualCompactChip, ChipColors colors, @Nullable String iconId) {
-        assertChipIsEqual(actualCompactChip, colors, iconId);
-        assertFromLayoutElementChipIsEqual(actualCompactChip, colors, iconId);
+        assertChip(actualCompactChip, colors, iconId, /* contentDescription= */ MAIN_TEXT);
+    }
+
+    private void assertChip(
+            CompactChip actualCompactChip,
+            ChipColors colors,
+            @Nullable String iconId,
+            @NonNull String contentDescription) {
+        assertChipIsEqual(actualCompactChip, colors, iconId, contentDescription);
+        assertFromLayoutElementChipIsEqual(actualCompactChip, colors, iconId, contentDescription);
         assertThat(CompactChip.fromLayoutElement(actualCompactChip)).isEqualTo(actualCompactChip);
     }
 
     private void assertChipIsEqual(
-            CompactChip actualCompactChip, ChipColors colors, @Nullable String iconId) {
+            CompactChip actualCompactChip,
+            ChipColors colors,
+            @Nullable String iconId,
+            @NonNull String contentDescription) {
         String expectedTag = iconId == null ? METADATA_TAG_TEXT : METADATA_TAG_ICON;
         assertThat(actualCompactChip.getMetadataTag()).isEqualTo(expectedTag);
         assertThat(actualCompactChip.getClickable().toProto()).isEqualTo(CLICKABLE.toProto());
         assertThat(areChipColorsEqual(actualCompactChip.getChipColors(), colors)).isTrue();
-        assertThat(actualCompactChip.getText()).isEqualTo(MAIN_TEXT);
         assertThat(actualCompactChip.getIconContent()).isEqualTo(iconId);
+        assertThat(actualCompactChip.getContentDescription().getValue())
+                .isEqualTo(contentDescription);
     }
 
     private void assertFromLayoutElementChipIsEqual(
-            CompactChip chip, ChipColors colors, @Nullable String iconId) {
+            CompactChip chip,
+            ChipColors colors,
+            @Nullable String iconId,
+            @NonNull String contentDescription) {
         Box box = new Box.Builder().addContent(chip).build();
 
         CompactChip newChip = CompactChip.fromLayoutElement(box.getContents().get(0));
 
         assertThat(newChip).isNotNull();
-        assertChipIsEqual(newChip, colors, iconId);
+        assertChipIsEqual(newChip, colors, iconId, contentDescription);
     }
 }
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
index 3463ba3..4570ab7 100644
--- a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/TitleChipTest.java
@@ -75,13 +75,16 @@
     @Test
     public void testTitleChipCustom() {
         DpProp width = dp(150);
+        String description = "Test description";
         TitleChip titleChip =
                 new TitleChip.Builder(CONTEXT, MAIN_TEXT, CLICKABLE, DEVICE_PARAMETERS)
                         .setChipColors(COLORS)
                         .setWidth(width)
+                        .setContentDescription(description)
                         .build();
 
         assertChip(titleChip, COLORS, width, /* iconId= */ null);
+        assertThat(titleChip.getContentDescription().getValue()).isEqualTo(description);
     }
 
     @Test
diff --git a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
index 303637a..8700295 100644
--- a/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
+++ b/wear/protolayout/protolayout-material/src/test/java/androidx/wear/protolayout/material/layouts/PrimaryLayoutTest.java
@@ -56,7 +56,8 @@
             new DeviceParameters.Builder().setScreenWidthDp(192).setScreenHeightDp(192).build();
     private static final LayoutElement CONTENT = new Box.Builder().build();
     private static final CompactChip PRIMARY_CHIP =
-            new CompactChip.Builder(CONTEXT, "Compact", CLICKABLE, DEVICE_PARAMETERS).build();
+            new CompactChip.Builder(CONTEXT, "Compact", CLICKABLE, DEVICE_PARAMETERS)
+                    .build();
     private static final Text PRIMARY_LABEL = new Text.Builder(CONTEXT, "Primary label").build();
     private static final Text SECONDARY_LABEL =
             new Text.Builder(CONTEXT, "Secondary label").build();
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
index 84a7180..d9b90e4 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/NodeInfo.java
@@ -25,7 +25,6 @@
 import androidx.annotation.UiThread;
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.ArraySet;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.pipeline.BoundDynamicType;
 import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest;
 import androidx.wear.protolayout.expression.pipeline.QuotaManager;
@@ -33,6 +32,7 @@
 import androidx.wear.protolayout.proto.ModifiersProto.AnimatedVisibility;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger.InnerCase;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.PositionIdTree.TreeNode;
 
 import java.util.ArrayList;
@@ -148,7 +148,9 @@
     @VisibleForTesting
     @SuppressWarnings("RestrictTo")
     int size() {
-        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCount).sum();
+        return mActiveBoundTypes.stream()
+                .mapToInt(BoundDynamicType::getDynamicNodeCount)
+                .sum();
     }
 
     /** Play the animation with the given trigger type. */
@@ -241,10 +243,10 @@
                         + mResolvedAvds.stream().filter(avd -> avd.mDrawable.isRunning()).count());
     }
 
-    /** Returns how many expression nodes evaluated. */
+    /** Returns the cost of evaluated expression nodes. */
     @VisibleForTesting
-    public int getExpressionNodesCount() {
-        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCount).sum();
+    public int getExpressionDynamicNodesCost() {
+        return mActiveBoundTypes.stream().mapToInt(BoundDynamicType::getDynamicNodeCost).sum();
     }
 
     /** Stores the {@link AnimatedVisibility} associated with this node. */
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
index 5579a88..8a933ec 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
@@ -40,7 +40,6 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.PlatformDataKey;
 import androidx.wear.protolayout.expression.pipeline.BoundDynamicType;
 import androidx.wear.protolayout.expression.pipeline.DynamicTypeBindingRequest;
@@ -65,6 +64,7 @@
 import androidx.wear.protolayout.proto.ModifiersProto.ExitTransition;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TypesProto.BoolProp;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.NodeInfo.ResolvedAvd;
 
 import com.google.common.collect.ImmutableList;
@@ -1161,11 +1161,11 @@
                         .sum();
     }
 
-    /** Returns How many dynamic data nodes exist in the pipeline. */
+    /** Returns the cost of nodes existing in the pipeline. */
     @VisibleForTesting
-    public int getDynamicExpressionsNodesCount() {
+    public int getDynamicExpressionsNodesCost() {
         return mPositionIdTree.getAllNodes().stream()
-                .mapToInt(NodeInfo::getExpressionNodesCount)
+                .mapToInt(NodeInfo::getExpressionDynamicNodesCost)
                 .sum();
     }
 
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index befefb4..096c2aa 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -41,7 +41,6 @@
 import androidx.annotation.Nullable;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -73,7 +72,6 @@
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedColor;
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedFloat;
 import androidx.wear.protolayout.expression.proto.FixedProto.FixedInt32;
-import androidx.wear.protolayout.expression.proto.FixedProto.FixedString;
 import androidx.wear.protolayout.proto.ColorProto.ColorProp;
 import androidx.wear.protolayout.proto.DimensionProto.DegreesProp;
 import androidx.wear.protolayout.proto.DimensionProto.DpProp;
@@ -85,6 +83,7 @@
 import androidx.wear.protolayout.proto.TriggerProto.OnVisibleTrigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger;
 import androidx.wear.protolayout.proto.TriggerProto.Trigger.InnerCase;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline.PipelineMaker;
 import androidx.wear.protolayout.renderer.inflater.DefaultAndroidSeekableAnimatedImageResourceByResIdResolver;
 import androidx.wear.protolayout.renderer.inflater.ResourceResolvers.ResourceAccessException;
@@ -841,7 +840,7 @@
         String staticValue = "static";
 
         AtomicReference<String> currentValue = new AtomicReference<>();
-        FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 3);
+        FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 2);
 
         ProtoLayoutDynamicDataPipeline pipeline =
                 new ProtoLayoutDynamicDataPipeline(
@@ -872,7 +871,7 @@
         makePipelineForDynamicString(
                 pipeline, dynamicString, staticValue, "posId", currentValue::set);
         pipeline.initNewLayout();
-        expect.that(pipeline.getDynamicExpressionsNodesCount()).isEqualTo(3);
+        expect.that(pipeline.getDynamicExpressionsNodesCost()).isEqualTo(2);
         // No quota left
         expect.that(quotaManager.getRemainingQuota()).isEqualTo(0);
         expect.that(currentValue.get()).isEqualTo(expectedOutput);
@@ -880,8 +879,6 @@
 
     @Test
     public void newLayout_noExpressionNodesQuota_useStaticData() {
-
-        String dynamicValue = "dynamic";
         String staticValue = "static";
         AtomicReference<String> currentValue = new AtomicReference<>();
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(/* quotaCap= */ 0);
@@ -894,7 +891,8 @@
 
         DynamicString dynamicString =
                 DynamicString.newBuilder()
-                        .setFixed(FixedString.newBuilder().setValue(dynamicValue).build())
+                        .setInt32FormatOp(
+                                Int32FormatOp.newBuilder().setInput(fixedDynamicInt32(1)).build())
                         .build();
 
         makePipelineForDynamicString(
@@ -909,7 +907,7 @@
     public void newLayout_removeNodeInfo_releaseQuota() {
 
         int quota = 8;
-        DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(4);
+        DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(5);
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
 
         ProtoLayoutDynamicDataPipeline pipeline =
@@ -945,8 +943,10 @@
         String nodeInfo2 = "posId2.1";
         String nodeInfo3 = "posId3.1";
         int quota = 8;
-        DynamicBool expressionWith5Nodes = buildBoolExpressionWithFixedNumberOfNodes(5);
-        DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(1);
+        // Cost = 5
+        DynamicBool expressionWith5Nodes = buildBoolExpressionWithFixedNumberOfNodes(6);
+        // Cost = 1
+        DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(2);
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
 
         ProtoLayoutDynamicDataPipeline pipeline =
@@ -956,7 +956,7 @@
                         new FixedQuotaManagerImpl(MAX_VALUE),
                         quotaManager);
 
-        // Adding an expressions with 5 dynamic nodes to nodeInfo1.
+        // Adding an expressions cost = 4 to nodeInfo1.
         makePipelineForDynamicBool(pipeline, expressionWith5Nodes, nodeInfo1);
         pipeline.initNewLayout();
 
@@ -975,11 +975,11 @@
 
         // Remove nodeInfo1 and add nodeInfo3. nodeInfo2 still in the pipeline.
         pipeline.mPositionIdTree.removeChildNodesFor(parentOfNode1);
-        // Adding an expressions with 1 dynamic node to nodeInfo3.
+        // Adding an expressions cost = 1 to nodeInfo3.
         makePipelineForDynamicBool(pipeline, expressionWith1Nodes, nodeInfo3);
 
         pipeline.initNewLayout();
-        // Now the pipeline will have a total expressionNodesCount of 6 = 5 + 1 nodeInfo2 (failed to
+        // Now the pipeline will have a total expression cost of 6 = 5 + 1 nodeInfo2 (failed to
         // bound previously) and nodeInfo3(new) should be able to bound
         expect.that(quotaManager.getRemainingQuota()).isEqualTo(2);
         expect.that(pipeline.mPositionIdTree.get(nodeInfo3).getFailedBindingRequest().size())
@@ -992,8 +992,11 @@
     public void newLayout_multipleBound_noEnoughDynamicNodesQuota_satisfyOnlyFewBounds() {
 
         int quota = 11;
-        DynamicBool expressionWith12Nodes = buildBoolExpressionWithFixedNumberOfNodes(12);
+        // Cost = 12
+        DynamicBool expressionWith12Nodes = buildBoolExpressionWithFixedNumberOfNodes(13);
+        // Cost = 3
         DynamicBool expressionWith4Nodes = buildBoolExpressionWithFixedNumberOfNodes(4);
+        // Cost = 0
         DynamicBool expressionWith1Nodes = buildBoolExpressionWithFixedNumberOfNodes(1);
 
         FixedQuotaManagerImpl quotaManager = new FixedQuotaManagerImpl(quota);
@@ -1013,7 +1016,7 @@
 
         pipeline.initNewLayout();
 
-        // expressionWith12Nodes related BoundType should file to bind.
+        // expressionWith12Nodes related BoundType should fail to bind.
         expect.that(
                         pipeline.mPositionIdTree
                                 .findFirst((node) -> node.getPosId().equals("posId1.0"))
@@ -1062,6 +1065,10 @@
                 .build();
     }
 
+    /**
+     * If count is equal to 1, this returns a FixedBool. Otherwise, it returns a dynamic expression
+     * containing one FixedBool and (count-1) NotBoolOp nodes.
+     */
     private static DynamicBool buildBoolExpressionWithFixedNumberOfNodes(int count) {
         if (count < 1) {
             throw new IllegalArgumentException();
diff --git a/wear/tiles/tiles-renderer/api/current.txt b/wear/tiles/tiles-renderer/api/current.txt
index d4f5f16..7746fcc 100644
--- a/wear/tiles/tiles-renderer/api/current.txt
+++ b/wear/tiles/tiles-renderer/api/current.txt
@@ -46,11 +46,25 @@
     ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+    ctor public TileRenderer(androidx.wear.tiles.renderer.TileRenderer.Config);
     method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
     method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
     method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
   }
 
+  public static final class TileRenderer.Config {
+    method public java.util.concurrent.Executor getLoadActionExecutor();
+    method public java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!> getLoadActionListener();
+    method public int getTilesTheme();
+    method public android.content.Context getUiContext();
+  }
+
+  public static final class TileRenderer.Config.Builder {
+    ctor public TileRenderer.Config.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+    method public androidx.wear.tiles.renderer.TileRenderer.Config build();
+    method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder setTilesTheme(@StyleRes int);
+  }
+
   @Deprecated public static interface TileRenderer.LoadActionListener {
     method @Deprecated public void onClick(androidx.wear.tiles.StateBuilders.State);
   }
diff --git a/wear/tiles/tiles-renderer/api/restricted_current.txt b/wear/tiles/tiles-renderer/api/restricted_current.txt
index d4f5f16..7746fcc 100644
--- a/wear/tiles/tiles-renderer/api/restricted_current.txt
+++ b/wear/tiles/tiles-renderer/api/restricted_current.txt
@@ -46,11 +46,25 @@
     ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     ctor @Deprecated public TileRenderer(android.content.Context, androidx.wear.tiles.LayoutElementBuilders.Layout, @StyleRes int, androidx.wear.tiles.ResourceBuilders.Resources, java.util.concurrent.Executor, androidx.wear.tiles.renderer.TileRenderer.LoadActionListener);
     ctor public TileRenderer(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+    ctor public TileRenderer(androidx.wear.tiles.renderer.TileRenderer.Config);
     method @Deprecated public android.view.View? inflate(android.view.ViewGroup);
     method public com.google.common.util.concurrent.ListenableFuture<android.view.View!> inflateAsync(androidx.wear.protolayout.LayoutElementBuilders.Layout, androidx.wear.protolayout.ResourceBuilders.Resources, android.view.ViewGroup);
     method public void setState(java.util.Map<androidx.wear.protolayout.expression.AppDataKey<?>!,androidx.wear.protolayout.expression.DynamicDataBuilders.DynamicDataValue<?>!>);
   }
 
+  public static final class TileRenderer.Config {
+    method public java.util.concurrent.Executor getLoadActionExecutor();
+    method public java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!> getLoadActionListener();
+    method public int getTilesTheme();
+    method public android.content.Context getUiContext();
+  }
+
+  public static final class TileRenderer.Config.Builder {
+    ctor public TileRenderer.Config.Builder(android.content.Context, java.util.concurrent.Executor, java.util.function.Consumer<androidx.wear.protolayout.StateBuilders.State!>);
+    method public androidx.wear.tiles.renderer.TileRenderer.Config build();
+    method public androidx.wear.tiles.renderer.TileRenderer.Config.Builder setTilesTheme(@StyleRes int);
+  }
+
   @Deprecated public static interface TileRenderer.LoadActionListener {
     method @Deprecated public void onClick(androidx.wear.tiles.StateBuilders.State);
   }
diff --git a/wear/tiles/tiles-renderer/lint-baseline.xml b/wear/tiles/tiles-renderer/lint-baseline.xml
index 8294d8d..0177db4 100644
--- a/wear/tiles/tiles-renderer/lint-baseline.xml
+++ b/wear/tiles/tiles-renderer/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha09" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha09)" variant="all" version="8.4.0-alpha09">
+<issues format="6" by="lint 8.4.0-alpha12" type="baseline" client="gradle" dependencies="false" name="AGP (8.4.0-alpha12)" variant="all" version="8.4.0-alpha12">
 
     <issue
         id="UnspecifiedRegisterReceiverFlag"
@@ -174,6 +174,96 @@
 
     <issue
         id="RestrictedApiAndroidX"
+        message="Layout can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="        @Nullable private final LayoutElementProto.Layout mLayout;"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Resources can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="        @Nullable private final ResourceProto.Resources mResources;"
+        errorLine2="                                ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Layout can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="                @Nullable LayoutElementProto.Layout layout,"
+        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Resources can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="                @Nullable ResourceProto.Resources resources) {"
+        errorLine2="                          ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Layout can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="        public LayoutElementProto.Layout getLayout() {"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Resources can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="        public ResourceProto.Resources getResources() {"
+        errorLine2="               ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Layout can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="            @Nullable private LayoutElementProto.Layout mLayout;"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Resources can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="            @Nullable private ResourceProto.Resources mResources;"
+        errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Layout can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="            public Builder setLayout(@NonNull LayoutElementProto.Layout layout) {"
+        errorLine2="                                              ~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
+        message="Resources can only be accessed from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
+        errorLine1="            public Builder setResources(@NonNull ResourceProto.Resources resources) {"
+        errorLine2="                                                 ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/wear/tiles/renderer/TileRenderer.java"/>
+    </issue>
+
+    <issue
+        id="RestrictedApiAndroidX"
         message="Timeline.toProto can only be called from within the same library group (referenced groupId=`androidx.wear.protolayout` from groupId=`androidx.wear.tiles`)"
         errorLine1="        mCache = new TilesTimelineCacheInternal(timeline.toProto());"
         errorLine2="                                                         ~~~~~~~">
diff --git a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
index c9dcd7b..763c021 100644
--- a/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
+++ b/wear/tiles/tiles-renderer/src/androidTest/java/androidx/wear/tiles/renderer/test/TileRendererGoldenTest.java
@@ -41,6 +41,7 @@
 import androidx.wear.protolayout.proto.ResourceProto.Resources;
 import androidx.wear.protolayout.protobuf.ByteString;
 import androidx.wear.tiles.renderer.TileRenderer;
+import androidx.wear.tiles.renderer.TileRenderer.Config;
 
 import com.google.protobuf.TextFormat;
 
@@ -236,9 +237,11 @@
 
         TileRenderer renderer =
                 new TileRenderer(
-                        appContext,
-                        ContextCompat.getMainExecutor(getApplicationContext()),
-                        i -> {});
+                        new Config.Builder(
+                                        appContext,
+                                        ContextCompat.getMainExecutor(getApplicationContext()),
+                                        i -> {})
+                                .build());
 
         View firstChild =
                 renderer.inflateAsync(
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 82e1946..92dfccf 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -89,9 +89,9 @@
      * @param resources The resources for the Tile.
      * @param loadActionExecutor Executor for {@code loadActionListener}.
      * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
-     * @deprecated Use {@link #TileRenderer(Context, Executor, Consumer)} which accepts Layout and
-     *     Resources in {@link #inflateAsync(LayoutElementBuilders.Layout,
-     *     ResourceBuilders.Resources, ViewGroup)} method.
+     * @deprecated Use {@link #TileRenderer(Config)} which accepts Layout and Resources in {@link
+     *     #inflateAsync(LayoutElementBuilders.Layout, ResourceBuilders.Resources, ViewGroup)}
+     *     method.
      */
     @Deprecated
     public TileRenderer(
@@ -119,9 +119,9 @@
      * @param resources The resources for the Tile.
      * @param loadActionExecutor Executor for {@code loadActionListener}.
      * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
-     * @deprecated Use {@link #TileRenderer(Context, Executor, Consumer)} which accepts Layout and
-     *     Resources in {@link #inflateAsync(LayoutElementBuilders.Layout,
-     *     ResourceBuilders.Resources, ViewGroup)} method.
+     * @deprecated Use {@link #TileRenderer(Config)} which accepts Layout and Resources in {@link
+     *     #inflateAsync(LayoutElementBuilders.Layout, ResourceBuilders.Resources, ViewGroup)}
+     *     method.
      */
     @Deprecated
     public TileRenderer(
@@ -141,6 +141,10 @@
     }
 
     /**
+     * Constructor for {@link TileRenderer}.
+     *
+     * <p>It is recommended to use the new {@link #TileRenderer(Config)} constructor instead.
+     *
      * @param uiContext A {@link Context} suitable for interacting with the UI.
      * @param loadActionExecutor Executor for {@code loadActionListener}.
      * @param loadActionListener Listener for clicks that will cause the contents to be reloaded.
@@ -158,6 +162,21 @@
                 /* resources= */ null);
     }
 
+    /**
+     * Constructor for {@link TileRenderer}.
+     *
+     * @param config A {@link Config} to create a {@link TileRenderer} instance.
+     */
+    public TileRenderer(@NonNull Config config) {
+        this(
+                config.getUiContext(),
+                config.getTilesTheme(),
+                config.getLoadActionExecutor(),
+                config.getLoadActionListener(),
+                /* layout= */ null,
+                /* resources= */ null);
+    }
+
     private TileRenderer(
             @NonNull Context uiContext,
             @StyleRes int tilesTheme,
@@ -271,4 +290,93 @@
         ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
         return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
     }
+
+    /** Config class for {@link TileRenderer}. */
+    public static final class Config {
+        @NonNull private final Context mUiContext;
+        @NonNull private final Executor mLoadActionExecutor;
+        @NonNull private final Consumer<StateBuilders.State> mLoadActionListener;
+
+        @StyleRes int mTilesTheme;
+
+        Config(
+                @NonNull Context uiContext,
+                @NonNull Executor loadActionExecutor,
+                @NonNull Consumer<StateBuilders.State> loadActionListener,
+                @StyleRes int tilesTheme) {
+            this.mUiContext = uiContext;
+            this.mLoadActionExecutor = loadActionExecutor;
+            this.mLoadActionListener = loadActionListener;
+            this.mTilesTheme = tilesTheme;
+        }
+
+        /** Returns the {@link Context} suitable for interacting with the UI. */
+        @NonNull
+        public Context getUiContext() {
+            return mUiContext;
+        }
+
+        /** Returns the {@link Executor} for {@code loadActionListener}. */
+        @NonNull
+        public Executor getLoadActionExecutor() {
+            return mLoadActionExecutor;
+        }
+
+        /** Returns the Listener for clicks that will cause the contents to be reloaded. */
+        @NonNull
+        public Consumer<StateBuilders.State> getLoadActionListener() {
+            return mLoadActionListener;
+        }
+
+        /**
+         * Returns the theme to use for this Tile instance. This can be used to customise things
+         * like the default font family.
+         */
+        public int getTilesTheme() {
+            return mTilesTheme;
+        }
+
+        /** Builder class for {@link Config}. */
+        public static final class Builder {
+            @NonNull private final Context mUiContext;
+            @NonNull private final Executor mLoadActionExecutor;
+            @NonNull private final Consumer<StateBuilders.State> mLoadActionListener;
+
+            @StyleRes int mTilesTheme = 0; // Default theme.
+
+            /**
+             * Builder for the {@link Config} class.
+             *
+             * @param uiContext A {@link Context} suitable for interacting with the UI.
+             * @param loadActionExecutor Executor for {@code loadActionListener}.
+             * @param loadActionListener Listener for clicks that will cause the contents to be
+             *     reloaded.
+             */
+            public Builder(
+                    @NonNull Context uiContext,
+                    @NonNull Executor loadActionExecutor,
+                    @NonNull Consumer<StateBuilders.State> loadActionListener) {
+                this.mUiContext = uiContext;
+                this.mLoadActionExecutor = loadActionExecutor;
+                this.mLoadActionListener = loadActionListener;
+            }
+
+            /**
+             * Sets the theme to use for this Tile instance. This can be used to customise things
+             * like the default font family.
+             */
+            @NonNull
+            public Builder setTilesTheme(@StyleRes int tilesTheme) {
+                mTilesTheme = tilesTheme;
+                return this;
+            }
+
+            /** Builds {@link Config} object. */
+            @NonNull
+            public Config build() {
+                return new Config(
+                        mUiContext, mLoadActionExecutor, mLoadActionListener, mTilesTheme);
+            }
+        }
+    }
 }