Merge "Rename AppFunctionEntity to AppFunctionSerializable" into androidx-main
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
index 865af3a..d37793f 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/KotlinAnnotationsDetectorTest.kt
@@ -163,8 +163,6 @@
                 javaSample("sample.kotlin.UseKtExperimentalFromJava")
             )
 
-        // TODO(b/210881073): Access to annotated property `field` is still not detected.
-
         val expected =
             """
 src/sample/kotlin/UseKtExperimentalFromJava.java:28: Error: This declaration is opt-in and its usage should be marked with @sample.kotlin.ExperimentalKotlinAnnotation or @OptIn(markerClass = sample.kotlin.ExperimentalKotlinAnnotation.class) [UnsafeOptInUsageError]
diff --git a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
index c2c6356..25a9639 100644
--- a/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
+++ b/annotation/annotation-experimental-lint/src/test/kotlin/androidx/annotation/experimental/lint/RequiresOptInDetectorTest.kt
@@ -229,8 +229,6 @@
                 javaSample("sample.optin.UseKtExperimentalFromJava")
             )
 
-        // TODO(b/210881073): Access to annotated property `field` is still not detected.
-
         val expected =
             """
 src/sample/optin/UseKtExperimentalFromJava.java:28: Error: This declaration is opt-in and its usage should be marked with @sample.optin.ExperimentalKotlinAnnotation or @OptIn(markerClass = sample.optin.ExperimentalKotlinAnnotation.class) [UnsafeOptInUsageError]
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
index 81f6291..6151d45 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/AppFunctionCompiler.kt
@@ -19,6 +19,7 @@
 import androidx.appfunctions.compiler.core.ProcessingException
 import androidx.appfunctions.compiler.core.logException
 import androidx.appfunctions.compiler.processors.AppFunctionIdProcessor
+import androidx.appfunctions.compiler.processors.AppFunctionInventoryProcessor
 import com.google.devtools.ksp.processing.KSPLogger
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.processing.SymbolProcessor
@@ -51,10 +52,9 @@
 
         override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
             val idProcessor = AppFunctionIdProcessor(environment.codeGenerator)
+            val inventoryProcessor = AppFunctionInventoryProcessor(environment.codeGenerator)
             return AppFunctionCompiler(
-                listOf(
-                    idProcessor,
-                ),
+                listOf(idProcessor, inventoryProcessor),
                 environment.logger,
             )
         }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
index f3f5fbb..fe0d3fa 100644
--- a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/core/IntrospectionHelper.kt
@@ -22,6 +22,8 @@
 object IntrospectionHelper {
     // Package names
     private const val APP_FUNCTIONS_PACKAGE_NAME = "androidx.appfunctions"
+    private const val APP_FUNCTIONS_INTERNAL_PACKAGE_NAME = "androidx.appfunctions.internal"
+    private const val APP_FUNCTIONS_METADATA_PACKAGE_NAME = "androidx.appfunctions.metadata"
 
     // Annotation classes
     object AppFunctionAnnotation {
@@ -30,4 +32,8 @@
 
     // Classes
     val APP_FUNCTION_CONTEXT_CLASS = ClassName(APP_FUNCTIONS_PACKAGE_NAME, "AppFunctionContext")
+    val APP_FUNCTION_INVENTORY_CLASS =
+        ClassName(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, "AppFunctionInventory")
+    val APP_FUNCTION_METADATA_CLASS =
+        ClassName(APP_FUNCTIONS_METADATA_PACKAGE_NAME, "AppFunctionMetadata")
 }
diff --git a/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt
new file mode 100644
index 0000000..cedc597
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/main/java/androidx/appfunctions/compiler/processors/AppFunctionInventoryProcessor.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2025 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.appfunctions.compiler.processors
+
+import androidx.appfunctions.compiler.AppFunctionCompiler
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
+import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver.AnnotatedAppFunctions
+import androidx.appfunctions.compiler.core.IntrospectionHelper
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.KModifier
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import com.squareup.kotlinpoet.asClassName
+import com.squareup.kotlinpoet.buildCodeBlock
+
+/**
+ * Generates implementations for the AppFunctionInventory interface.
+ *
+ * It resolves all functions in a class annotated with `@AppFunction`, and generates the
+ * corresponding metadata for those functions.
+ */
+class AppFunctionInventoryProcessor(
+    private val codeGenerator: CodeGenerator,
+) : SymbolProcessor {
+    override fun process(resolver: Resolver): List<KSAnnotated> {
+        val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
+        val appFunctionClasses = appFunctionSymbolResolver.resolveAnnotatedAppFunctions()
+        for (appFunctionClass in appFunctionClasses) {
+            generateAppFunctionInventoryClass(appFunctionClass)
+        }
+        return emptyList()
+    }
+
+    private fun generateAppFunctionInventoryClass(appFunctionClass: AnnotatedAppFunctions) {
+        val originalPackageName = appFunctionClass.classDeclaration.packageName.asString()
+        val originalClassName = appFunctionClass.classDeclaration.simpleName.asString()
+
+        val inventoryClassName = getAppFunctionInventoryClassName(originalClassName)
+        val inventoryClassBuilder = TypeSpec.classBuilder(inventoryClassName)
+        inventoryClassBuilder.addSuperinterface(IntrospectionHelper.APP_FUNCTION_INVENTORY_CLASS)
+        inventoryClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
+        inventoryClassBuilder.addProperty(buildFunctionIdToMetadataMapProperty())
+
+        val fileSpec =
+            FileSpec.builder(originalPackageName, inventoryClassName)
+                .addType(inventoryClassBuilder.build())
+                .build()
+        codeGenerator
+            .createNewFile(
+                Dependencies(
+                    aggregating = false,
+                    checkNotNull(appFunctionClass.classDeclaration.containingFile)
+                ),
+                originalPackageName,
+                inventoryClassName
+            )
+            .bufferedWriter()
+            .use { fileSpec.writeTo(it) }
+    }
+
+    /** Creates the `functionIdToMetadataMap` property of the `AppFunctionInventory`. */
+    private fun buildFunctionIdToMetadataMapProperty(): PropertySpec {
+        return PropertySpec.builder(
+                "functionIdToMetadataMap",
+                Map::class.asClassName()
+                    .parameterizedBy(
+                        String::class.asClassName(),
+                        IntrospectionHelper.APP_FUNCTION_METADATA_CLASS
+                    ),
+            )
+            .addModifiers(KModifier.OVERRIDE)
+            // TODO: Actually build map properties
+            .initializer(buildCodeBlock { addStatement("mapOf()") })
+            .build()
+    }
+
+    private fun getAppFunctionInventoryClassName(functionClassName: String): String {
+        return "$%s_AppFunctionInventory_Impl".format(functionClassName)
+    }
+}
diff --git a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
index 3e628c7..8860052 100644
--- a/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
+++ b/appfunctions/appfunctions-compiler/src/test/java/androidx/appfunctions/compiler/AppFunctionCompilerTest.kt
@@ -43,7 +43,7 @@
     }
 
     @Test
-    fun testSimpleFunction() {
+    fun testSimpleFunction_genAppFunctionIds_success() {
         val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
 
         compilationTestHelper.assertSuccessWithContent(
@@ -54,7 +54,7 @@
     }
 
     @Test
-    fun testMissingFirstParameter() {
+    fun testMissingFirstParameter_hasCompileError() {
         val report =
             compilationTestHelper.compileAll(sourceFileNames = listOf("MissingFirstParameter.KT"))
 
@@ -68,7 +68,7 @@
     }
 
     @Test
-    fun testIncorrectFirstParameter() {
+    fun testIncorrectFirstParameter_hasCompileError() {
         val report =
             compilationTestHelper.compileAll(sourceFileNames = listOf("IncorrectFirstParameter.KT"))
 
@@ -80,4 +80,15 @@
                 "    ^"
         )
     }
+
+    @Test
+    fun testSimpleFunction_genAppFunctionInventoryImpl_success() {
+        val report = compilationTestHelper.compileAll(sourceFileNames = listOf("SimpleFunction.KT"))
+
+        compilationTestHelper.assertSuccessWithContent(
+            report = report,
+            expectGeneratedFileName = "SimpleFunction_AppFunctionInventory_Impl.kt",
+            goldenFileName = "$%s".format("SimpleFunction_AppFunctionInventory_Impl.KT")
+        )
+    }
 }
diff --git a/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT b/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT
new file mode 100644
index 0000000..e7653db3
--- /dev/null
+++ b/appfunctions/appfunctions-compiler/src/test/test-data/output/$SimpleFunction_AppFunctionInventory_Impl.KT
@@ -0,0 +1,12 @@
+package com.testdata
+
+import androidx.appfunctions.`internal`.AppFunctionInventory
+import androidx.appfunctions.metadata.AppFunctionMetadata
+import javax.`annotation`.processing.Generated
+import kotlin.String
+import kotlin.collections.Map
+
+@Generated("androidx.appfunctions.compiler.AppFunctionCompiler")
+public class `$SimpleFunction_AppFunctionInventory_Impl` : AppFunctionInventory {
+  override val functionIdToMetadataMap: Map<String, AppFunctionMetadata> = mapOf()
+}
diff --git a/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
new file mode 100644
index 0000000..59c4bca
--- /dev/null
+++ b/appfunctions/appfunctions-runtime/src/main/java/androidx/appfunctions/internal/AppFunctionInvoker.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2025 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.appfunctions.internal
+
+import androidx.annotation.RestrictTo
+import androidx.appfunctions.AppFunctionContext
+
+/**
+ * An interface for invoking app functions.
+ *
+ * This interface defines a contract for invoking a set of AppFunctions declared in an application.
+ * Each AppFunction implementation class has a corresponding generated Invoker class that implements
+ * this interface.
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface AppFunctionInvoker {
+    /**
+     * Sets of function ids that the invoker supports.
+     *
+     * For example, consider the following AppFunction implementation class:
+     * ```kotlin
+     * class NoteFunctions : CreateNote, EditNote {
+     *   @AppFunction
+     *   override suspend fun createNote() : Note { ... }
+     *
+     *   @AppFunction
+     *   override suspend fun editNote() : Note { ... }
+     * }
+     * ```
+     *
+     * The set of supported Ids would include the respective function id for the `createNote` and
+     * `editNote` functions.
+     */
+    public val supportedFunctionIds: Set<String>
+
+    /**
+     * Invokes an AppFunction identified by [functionIdentifier], with [parameters].
+     *
+     * @throws [androidx.appfunctions.AppFunctionException] with error code
+     *   [androidx.appfunctions.AppFunctionException.ERROR_FUNCTION_NOT_FOUND] if called with
+     *   invalid function identifier or code
+     *   [androidx.appfunctions.AppFunctionException.ERROR_INVALID_ARGUMENT] if called with invalid
+     *   parameters.
+     */
+    public suspend fun unsafeInvoke(
+        appFunctionContext: AppFunctionContext,
+        functionIdentifier: String,
+        parameters: Map<String, Any?>,
+    ): Any?
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index aea21e9..7322275 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -698,9 +698,14 @@
             project,
             kotlinMultiplatformAndroidComponentsExtension
         )
-        // Propagate the compileSdk value into minCompileSdk.
-        kotlinMultiplatformAndroidTarget.aarMetadata.minCompileSdk =
-            kotlinMultiplatformAndroidTarget.compileSdk
+        kotlinMultiplatformAndroidComponentsExtension.apply {
+            finalizeDsl {
+                // Propagate the compileSdk value into minCompileSdk. Must be done after the DSL in
+                // build.gradle files (that sets compileSdk in the first place) is evaluated.
+                kotlinMultiplatformAndroidTarget.aarMetadata.minCompileSdk =
+                    kotlinMultiplatformAndroidTarget.compileSdk
+            }
+        }
         project.disableStrictVersionConstraints()
 
         project.configureProjectForApiTasks(AndroidMultiplatformApiTaskConfig, androidXExtension)
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
index 28281b1..dfeb5d2 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/testing/TestUseCaseCamera.kt
@@ -102,6 +102,7 @@
                 cameraQuirks,
                 ZslControlNoOpImpl(),
                 NoOpTemplateParamsOverride,
+                cameraMetadata,
             )
         val cameraGraph = cameraPipe.create(cameraGraphConfig)
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 0a33480..a999884 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.ImageFormat
+import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.params.OutputConfiguration
@@ -32,6 +33,7 @@
 import androidx.camera.camera2.pipe.CameraGraph.OperatingMode
 import androidx.camera.camera2.pipe.CameraGraph.RepeatingRequestRequirementsBeforeCapture.CompletionBehavior.AT_LEAST
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.CameraStream
 import androidx.camera.camera2.pipe.InputStream
@@ -125,6 +127,7 @@
 constructor(
     private val cameraPipe: CameraPipe,
     private val cameraDevices: CameraDevices,
+    private val cameraMetadata: CameraMetadata?,
     @GuardedBy("lock") private val cameraCoordinator: CameraCoordinator,
     private val callbackMap: CameraCallbackMap,
     private val requestListener: ComboRequestListener,
@@ -658,7 +661,7 @@
         streamConfigMap: MutableMap<CameraStream.Config, DeferrableSurface>,
         isExtensions: Boolean = false,
     ): CameraGraph.Config {
-        return Companion.createCameraGraphConfig(
+        return createCameraGraphConfig(
             sessionConfigAdapter,
             streamConfigMap,
             callbackMap,
@@ -667,6 +670,7 @@
             cameraQuirks,
             zslControl,
             templateParamsOverride,
+            cameraMetadata,
             isExtensions,
         )
     }
@@ -909,6 +913,7 @@
             cameraQuirks: CameraQuirks,
             zslControl: ZslControl,
             templateParamsOverride: TemplateParamsOverride,
+            cameraMetadata: CameraMetadata?,
             isExtensions: Boolean = false,
         ): CameraGraph.Config {
             var containsVideo = false
@@ -964,7 +969,8 @@
                             streamUseCase =
                                 getStreamUseCase(
                                     deferrableSurface,
-                                    sessionConfigAdapter.surfaceToStreamUseCaseMap
+                                    sessionConfigAdapter.surfaceToStreamUseCaseMap,
+                                    cameraMetadata,
                                 ),
                             streamUseHint =
                                 getStreamUseHint(
@@ -1058,9 +1064,26 @@
 
         private fun getStreamUseCase(
             deferrableSurface: DeferrableSurface,
-            mapping: Map<DeferrableSurface, Long>
+            mapping: Map<DeferrableSurface, Long>,
+            cameraMetadata: CameraMetadata?,
         ): OutputStream.StreamUseCase? {
-            return mapping[deferrableSurface]?.let { OutputStream.StreamUseCase(it) }
+            val expectedStreamUseCase =
+                mapping[deferrableSurface]?.let { OutputStream.StreamUseCase(it) }
+            return if (
+                Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+                    expectedStreamUseCase != null &&
+                    cameraMetadata
+                        ?.get(CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES)
+                        ?.contains(expectedStreamUseCase.value) == true
+            ) {
+                expectedStreamUseCase
+            } else {
+                Log.warn {
+                    "Expected stream use case for $deferrableSurface, " +
+                        "$expectedStreamUseCase cannot be set!"
+                }
+                null
+            }
         }
 
         private fun getStreamUseHint(
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index de2b4c5..743e748 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -689,6 +689,7 @@
                         emptySet(),
                         mapOf(fakeCameraBackend.id to listOf(fakeCameraMetadata))
                     ),
+                cameraMetadata = fakeCameraMetadata,
                 cameraCoordinator = CameraCoordinatorAdapter(cameraPipe, cameraPipe.cameras()),
                 callbackMap = CameraCallbackMap(),
                 requestListener = ComboRequestListener(),
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
index bb2cb3f..cf93534 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Streams.kt
@@ -439,6 +439,7 @@
             public val VIDEO_RECORD: StreamUseCase = StreamUseCase(3)
             public val PREVIEW_VIDEO_STILL: StreamUseCase = StreamUseCase(4)
             public val VIDEO_CALL: StreamUseCase = StreamUseCase(5)
+            public val CROPPED_RAW: StreamUseCase = StreamUseCase(6)
         }
     }
 
@@ -466,12 +467,12 @@
      */
     public fun isValidForHighSpeedOperatingMode(): Boolean {
         return this.streamUseCase == null ||
-            this.streamUseCase == OutputStream.StreamUseCase.DEFAULT ||
-            this.streamUseCase == OutputStream.StreamUseCase.PREVIEW ||
-            this.streamUseCase == OutputStream.StreamUseCase.VIDEO_RECORD ||
+            this.streamUseCase == DEFAULT ||
+            this.streamUseCase == StreamUseCase.PREVIEW ||
+            this.streamUseCase == StreamUseCase.VIDEO_RECORD ||
             this.streamUseHint == null ||
-            this.streamUseHint == OutputStream.StreamUseHint.DEFAULT ||
-            this.streamUseHint == OutputStream.StreamUseHint.VIDEO_RECORD
+            this.streamUseHint == StreamUseHint.DEFAULT ||
+            this.streamUseHint == StreamUseHint.VIDEO_RECORD
     }
 }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
index 028a1eb..5618494 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionFactory.kt
@@ -22,7 +22,6 @@
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper.Companion.SURFACE_GROUP_ID_NONE
 import androidx.camera.camera2.pipe.config.Camera2ControllerScope
@@ -184,21 +183,13 @@
     private val threads: Threads,
     private val streamGraph: StreamGraphImpl,
     private val graphConfig: CameraGraph.Config,
-    private val camera2MetadataProvider: Camera2MetadataProvider
 ) : CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
         surfaces: Map<StreamId, Surface>,
         captureSessionState: CaptureSessionState
     ): Map<StreamId, OutputConfigurationWrapper> {
-        val outputs =
-            buildOutputConfigurations(
-                graphConfig,
-                streamGraph,
-                surfaces,
-                camera2MetadataProvider,
-                cameraDevice.cameraId
-            )
+        val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
         if (outputs.all.isEmpty()) {
             Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
             captureSessionState.onSessionFinalized()
@@ -240,7 +231,6 @@
     private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
     private val streamGraph: StreamGraphImpl,
-    private val camera2MetadataProvider: Camera2MetadataProvider
 ) : CaptureSessionFactory {
     override fun create(
         cameraDevice: CameraDeviceWrapper,
@@ -259,14 +249,7 @@
                 else -> graphConfig.sessionMode.mode
             }
 
-        val outputs =
-            buildOutputConfigurations(
-                graphConfig,
-                streamGraph,
-                surfaces,
-                camera2MetadataProvider,
-                cameraDevice.cameraId
-            )
+        val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
         if (outputs.all.isEmpty()) {
             Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
             captureSessionState.onSessionFinalized()
@@ -315,8 +298,6 @@
     graphConfig: CameraGraph.Config,
     streamGraph: StreamGraphImpl,
     surfaces: Map<StreamId, Surface>,
-    camera2MetadataProvider: Camera2MetadataProvider,
-    cameraId: CameraId
 ): OutputConfigurations {
     val allOutputs = arrayListOf<OutputConfigurationWrapper>()
     val deferredOutputs = mutableMapOf<StreamId, OutputConfigurationWrapper>()
@@ -362,8 +343,6 @@
                         } else {
                             null
                         },
-                    cameraId = cameraId,
-                    camera2MetadataProvider = camera2MetadataProvider
                 )
             if (output == null) {
                 Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
@@ -399,8 +378,6 @@
                     } else {
                         null
                     },
-                cameraId = cameraId,
-                camera2MetadataProvider = camera2MetadataProvider
             )
         if (output == null) {
             Log.warn { "Failed to create AndroidOutputConfiguration for $outputConfig" }
@@ -476,14 +453,7 @@
             }
         }
 
-        val outputs =
-            buildOutputConfigurations(
-                graphConfig,
-                streamGraph,
-                surfaces,
-                camera2MetadataProvider,
-                cameraDevice.cameraId
-            )
+        val outputs = buildOutputConfigurations(graphConfig, streamGraph, surfaces)
 
         if (outputs.all.isEmpty()) {
             Log.warn { "Failed to create OutputConfigurations for $graphConfig" }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index 90d3918..c823908 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -161,8 +161,6 @@
             surfaceSharing: Boolean = false,
             surfaceGroupId: Int = SURFACE_GROUP_ID_NONE,
             physicalCameraId: CameraId? = null,
-            cameraId: CameraId? = null,
-            camera2MetadataProvider: Camera2MetadataProvider? = null,
         ): OutputConfigurationWrapper? {
             check(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
 
@@ -261,14 +259,9 @@
                 }
             }
 
-            if (streamUseCase != null && cameraId != null && camera2MetadataProvider != null) {
+            if (streamUseCase != null) {
                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
-                    val cameraMetadata = camera2MetadataProvider.awaitCameraMetadata(cameraId)
-                    val availableStreamUseCases =
-                        Api33Compat.getAvailableStreamUseCases(cameraMetadata)
-                    if (availableStreamUseCases?.contains(streamUseCase.value) == true) {
-                        Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
-                    }
+                    Api33Compat.setStreamUseCase(configuration, streamUseCase.value)
                 }
             }
 
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index 96d36ea..ab6cf33 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -50,6 +50,7 @@
 import android.app.Instrumentation;
 import android.content.Context;
 import android.graphics.Rect;
+import android.hardware.camera2.CameraCaptureSession;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CaptureRequest;
@@ -64,12 +65,14 @@
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
 import androidx.camera.camera2.internal.util.TestUtil;
+import androidx.camera.camera2.interop.Camera2Interop;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraXConfig;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageProxy;
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -431,6 +434,92 @@
 
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void setTorchStrengthLevel_valueUpdated()
+            throws ExecutionException, InterruptedException {
+        assumeTrue(mHasFlashUnit);
+
+        // Arrange
+        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+        imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+        mCamera = CameraUtil.createCameraAndAttachUseCase(
+                ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+                imageAnalysis);
+        Camera2CameraControlImpl camera2CameraControlImpl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+        camera2CameraControlImpl.enableTorch(true).get();
+
+        // Act
+        int maxStrength = mCamera.getCameraInfo().getMaxTorchStrengthLevel();
+        int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
+        // If the default strength is the max, set the strength to 1, otherwise, set to max.
+        int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
+        camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+
+        // Assert: the customized strength is applied
+        Camera2ImplConfig camera2Config = new Camera2ImplConfig(
+                camera2CameraControlImpl.getSessionConfig().getImplementationOptions());
+        assertThat(camera2Config.getCaptureRequestOption(
+                CaptureRequest.FLASH_STRENGTH_LEVEL, -1))
+                .isEqualTo(customizedStrength);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void setTorchStrengthLevel_throwExceptionIfLessThanOne()
+            throws ExecutionException, InterruptedException {
+        assumeTrue(mHasFlashUnit);
+
+        // Arrange
+        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+        imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+        mCamera = CameraUtil.createCameraAndAttachUseCase(
+                ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+                imageAnalysis);
+        Camera2CameraControlImpl camera2CameraControlImpl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+        camera2CameraControlImpl.enableTorch(true).get();
+
+        // Act & Assert
+        try {
+            camera2CameraControlImpl.setTorchStrengthLevelAsync(0).get();
+        } catch (ExecutionException e) {
+            assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+            return;
+        }
+
+        fail("setTorchStrength didn't fail with an IllegalArgumentException.");
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void setTorchStrengthLevel_throwExceptionIfLargerThanMax()
+            throws ExecutionException, InterruptedException {
+        assumeTrue(mHasFlashUnit);
+
+        // Arrange
+        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder().build();
+        imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
+        mCamera = CameraUtil.createCameraAndAttachUseCase(
+                ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+                imageAnalysis);
+        Camera2CameraControlImpl camera2CameraControlImpl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+        camera2CameraControlImpl.enableTorch(true).get();
+
+        // Act & Assert
+        try {
+            camera2CameraControlImpl.setTorchStrengthLevelAsync(
+                    mCamera.getCameraInfo().getMaxTorchStrengthLevel() + 1).get();
+        } catch (ExecutionException e) {
+            assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
+            return;
+        }
+
+        fail("setTorchStrength didn't fail with an IllegalArgumentException.");
+    }
+
     @SdkSuppress(minSdkVersion = 35)
     @Test
     public void enableLowLightBoost_aeModeSetAndRequestUpdated() throws InterruptedException {
@@ -516,6 +605,57 @@
                 ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void capture_torchAsFlash_shouldUseDefaultTorchStrength()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        assumeTrue(mHasFlashUnit);
+
+        // Arrange: explicitly set flash type to use torch as flash
+        ImageCapture.Builder imageCaptureBuilder = new ImageCapture.Builder().setFlashType(
+                ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH).setFlashMode(
+                ImageCapture.FLASH_MODE_ON);
+        CameraCaptureSession.CaptureCallback captureCallback = mock(
+                CameraCaptureSession.CaptureCallback.class);
+        new Camera2Interop.Extender<>(imageCaptureBuilder).setSessionCaptureCallback(
+                captureCallback);
+        ImageCapture imageCapture = imageCaptureBuilder.build();
+        mCamera = CameraUtil.createCameraAndAttachUseCase(
+                ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+                imageCapture);
+        Camera2CameraControlImpl camera2CameraControlImpl =
+                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+
+        // Act
+        int maxStrength = mCamera.getCameraInfo().getMaxTorchStrengthLevel();
+        int defaultStrength = mCamera.getCameraInfo().getTorchStrengthLevel().getValue();
+        // If the default strength is the max, set the strength to 1, otherwise, set to max.
+        int customizedStrength = defaultStrength == maxStrength ? 1 : maxStrength;
+        camera2CameraControlImpl.setTorchStrengthLevelAsync(customizedStrength).get();
+
+        // Assert: the capture uses default torch strength
+        CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
+        captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+        captureConfigBuilder.addSurface(imageCapture.getSessionConfig().getSurfaces().get(0));
+
+        camera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_ON);
+        camera2CameraControlImpl.submitStillCaptureRequests(
+                        Arrays.asList(captureConfigBuilder.build()),
+                        ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+                        ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH)
+                .get(5, TimeUnit.SECONDS);
+        ArgumentCaptor<CaptureRequest> captureRequestCaptor =
+                ArgumentCaptor.forClass(CaptureRequest.class);
+        verify(captureCallback, timeout(5000).atLeastOnce())
+                .onCaptureCompleted(any(), captureRequestCaptor.capture(), any());
+        List<CaptureRequest> results = captureRequestCaptor.getAllValues();
+        for (CaptureRequest result : results) {
+            // None of the capture capture should be sent with the customized strength.
+            assertThat(result.get(CaptureRequest.FLASH_STRENGTH_LEVEL)).isNotEqualTo(
+                    customizedStrength);
+        }
+    }
+
     private void captureTest(int captureMode, int flashType)
             throws ExecutionException, InterruptedException, TimeoutException {
         ImageCapture imageCapture = new ImageCapture.Builder().build();
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
index a1b57bc..de994f6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/TorchControlDeviceTest.java
@@ -19,6 +19,7 @@
 import static org.junit.Assume.assumeTrue;
 
 import android.content.Context;
+import android.os.Build;
 
 import androidx.camera.camera2.Camera2Config;
 import androidx.camera.camera2.internal.util.TestUtil;
@@ -63,6 +64,8 @@
 
     private TorchControl mTorchControl;
     private CameraUseCaseAdapter mCamera;
+    private Camera2CameraControlImpl mCameraControl;
+    private boolean mIsTorchStrengthSupported;
 
     @Before
     public void setUp() {
@@ -83,10 +86,9 @@
         // Make ImageAnalysis active.
         imageAnalysis.setAnalyzer(CameraXExecutors.mainThreadExecutor(), ImageProxy::close);
         mCamera = CameraUtil.createCameraAndAttachUseCase(context, cameraSelector, imageAnalysis);
-        Camera2CameraControlImpl cameraControl =
-                TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
-
-        mTorchControl = cameraControl.getTorchControl();
+        mCameraControl = TestUtil.getCamera2CameraControlImpl(mCamera.getCameraControl());
+        mTorchControl = mCameraControl.getTorchControl();
+        mIsTorchStrengthSupported = mCamera.getCameraInfo().getMaxTorchStrengthLevel() > 1;
     }
 
     @After
@@ -113,4 +115,30 @@
         // Future should return with no exception
         future.get();
     }
+
+    @Test(timeout = 5000L)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOnLevel()
+            throws ExecutionException, InterruptedException {
+        assumeTrue(mIsTorchStrengthSupported);
+
+        // Arrange: turn on the torch
+        mTorchControl.enableTorch(true).get();
+
+        // Act & Assert: the future completes
+        mTorchControl.setTorchStrengthLevel(
+                mCamera.getCameraInfo().getMaxTorchStrengthLevel()).get();
+    }
+
+    @Test(timeout = 5000L)
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public void setTorchStrengthLevel_futureCompleteWhenTorchIsOffLevel()
+            throws ExecutionException, InterruptedException {
+        assumeTrue(mIsTorchStrengthSupported);
+
+        // Arrange: the torch is default off
+        // Act & Assert: the future completes
+        mTorchControl.setTorchStrengthLevel(
+                mCamera.getCameraInfo().getMaxTorchStrengthLevel()).get();
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 6188489..8a2a439 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -34,6 +34,7 @@
 import android.util.Rational;
 
 import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
 import androidx.annotation.OptIn;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
@@ -139,7 +140,10 @@
     private ImageCapture.ScreenFlash mScreenFlash;
 
     // use volatile modifier to make these variables in sync in all threads.
-    private volatile boolean mIsTorchOn = false;
+    @TorchControl.TorchStateInternal
+    private volatile int mTorchState = TorchControl.OFF;
+    @IntRange(from = 1)
+    private volatile int mTorchStrength;
     private volatile boolean mIsLowLightBoostOn = false;
     @ImageCapture.FlashMode
     private volatile int mFlashMode = FLASH_MODE_OFF;
@@ -205,6 +209,7 @@
                 this, scheduler, mExecutor, cameraQuirks);
         mZoomControl = new ZoomControl(this, mCameraCharacteristics, mExecutor);
         mTorchControl = new TorchControl(this, mCameraCharacteristics, mExecutor);
+        mTorchStrength = mCameraCharacteristics.getDefaultTorchStrengthLevel();
         mLowLightBoostControl = new LowLightBoostControl(this, mCameraCharacteristics, mExecutor);
         if (Build.VERSION.SDK_INT >= 23) {
             mZslControl = new ZslControlImpl(mCameraCharacteristics, mExecutor);
@@ -504,6 +509,30 @@
         return mExposureControl.setExposureCompensationIndex(exposure);
     }
 
+    @Override
+    public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+            @IntRange(from = 1) int torchStrengthLevel) {
+        if (!isControlInUse()) {
+            return Futures.immediateFailedFuture(
+                    new OperationCanceledException("Camera is not active."));
+        }
+        if (torchStrengthLevel < 1
+                || torchStrengthLevel > mCameraCharacteristics.getMaxTorchStrengthLevel()) {
+            return Futures.immediateFailedFuture(new IllegalArgumentException(
+                    "The specified torch strength is not within the valid range."));
+        }
+        return Futures.nonCancellationPropagating(mTorchControl.setTorchStrengthLevel(
+                Math.min(torchStrengthLevel, mCameraCharacteristics.getMaxTorchStrengthLevel())));
+    }
+
+    @ExecutedBy("mExecutor")
+    void setTorchStrengthLevelInternal(@IntRange(from = 1) int torchStrengthLevel) {
+        mTorchStrength = torchStrengthLevel;
+        if (isTorchOn()) {
+            updateSessionConfigSynchronous();
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public @NonNull ListenableFuture<List<Void>> submitStillCaptureRequests(
@@ -654,14 +683,14 @@
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
-    void enableTorchInternal(boolean torch) {
+    void enableTorchInternal(@TorchControl.TorchStateInternal int torchState) {
         // When low-light boost is on, any torch related operations will be ignored.
         if (mIsLowLightBoostOn) {
             return;
         }
 
-        mIsTorchOn = torch;
-        if (!torch) {
+        mTorchState = torchState;
+        if (torchState == TorchControl.OFF) {
             // On some devices, needs to reset the AE/flash state to ensure that the torch can be
             // turned off.
             resetAeFlashState();
@@ -677,11 +706,11 @@
         }
 
         // Forces turn off torch before enabling low-light boost.
-        if (lowLightBoost && mIsTorchOn) {
+        if (lowLightBoost && isTorchOn()) {
             // On some devices, needs to reset the AE/flash state to ensure that the torch can be
             // turned off.
             resetAeFlashState();
-            mIsTorchOn = false;
+            mTorchState = TorchControl.OFF;
             mTorchControl.forceUpdateTorchStateToOff();
         }
 
@@ -706,7 +735,7 @@
 
     @ExecutedBy("mExecutor")
     boolean isTorchOn() {
-        return mIsTorchOn;
+        return mTorchState != TorchControl.OFF;
     }
 
     @ExecutedBy("mExecutor")
@@ -746,9 +775,20 @@
 
         if (mIsLowLightBoostOn) {
             aeMode = CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY;
-        } else if (mIsTorchOn) {
+        } else if (isTorchOn()) {
             builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_MODE,
                     CaptureRequest.FLASH_MODE_TORCH, Config.OptionPriority.REQUIRED);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+                if (mTorchState == TorchControl.ON) {
+                    builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_STRENGTH_LEVEL,
+                            mTorchStrength, Config.OptionPriority.REQUIRED);
+                } else if (mTorchState == TorchControl.USED_AS_FLASH) {
+                    // If torch is used as flash, use the default torch strength instead.
+                    builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_STRENGTH_LEVEL,
+                            mCameraCharacteristics.getDefaultTorchStrengthLevel(),
+                            Config.OptionPriority.REQUIRED);
+                }
+            }
         } else {
             switch (mFlashMode) {
                 case FLASH_MODE_OFF:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index 81a5d90..aeb7058 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -38,6 +38,7 @@
 
 import androidx.annotation.FloatRange;
 import androidx.annotation.GuardedBy;
+import androidx.annotation.IntRange;
 import androidx.annotation.OptIn;
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
@@ -110,6 +111,8 @@
     @GuardedBy("mLock")
     private @Nullable RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
     @GuardedBy("mLock")
+    private @Nullable RedirectableLiveData<Integer> mRedirectTorchStrengthLiveData = null;
+    @GuardedBy("mLock")
     private @Nullable RedirectableLiveData<Integer> mRedirectLowLightBoostStateLiveData = null;
     @GuardedBy("mLock")
     private @Nullable RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
@@ -161,6 +164,11 @@
                         mCamera2CameraControlImpl.getTorchControl().getTorchState());
             }
 
+            if (mRedirectTorchStrengthLiveData != null) {
+                mRedirectTorchStrengthLiveData.redirectTo(
+                        mCamera2CameraControlImpl.getTorchControl().getTorchStrengthLevel());
+            }
+
             if (mRedirectLowLightBoostStateLiveData != null) {
                 mRedirectLowLightBoostStateLiveData.redirectTo(mCamera2CameraControlImpl
                         .getLowLightBoostControl().getLowLightBoostState());
@@ -709,4 +717,29 @@
 
         return mPhysicalCameraInfos;
     }
+
+    @Override
+    @IntRange(from = 1)
+    public int getMaxTorchStrengthLevel() {
+        return mCameraCharacteristicsCompat.getMaxTorchStrengthLevel();
+    }
+
+    @Override
+    public @NonNull LiveData<Integer> getTorchStrengthLevel() {
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectTorchStrengthLiveData == null) {
+                    mRedirectTorchStrengthLiveData = new RedirectableLiveData<>(
+                            mCameraCharacteristicsCompat.getDefaultTorchStrengthLevel());
+                }
+                return mRedirectTorchStrengthLiveData;
+            }
+
+            if (mRedirectTorchStrengthLiveData != null) {
+                return mRedirectTorchStrengthLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getTorchControl().getTorchStrengthLevel();
+        }
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 42a38eb..c05cdc7 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -635,7 +635,8 @@
                     mIsExecuted = true;
 
                     ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
-                        mCameraControl.getTorchControl().enableTorchInternal(completer, true);
+                        mCameraControl.getTorchControl().enableTorchInternal(completer,
+                                TorchControl.USED_AS_FLASH);
                         return "TorchOn";
                     });
                     return FutureChain.from(future).transformAsync(
@@ -667,7 +668,7 @@
         @Override
         public void postCapture() {
             if (mIsExecuted) {
-                mCameraControl.getTorchControl().enableTorchInternal(null, false);
+                mCameraControl.getTorchControl().enableTorchInternal(null, TorchControl.OFF);
                 Logger.d(TAG, "Turning off torch");
                 if (mTriggerAePrecapture) {
                     mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
@@ -795,7 +796,7 @@
                                     return "EnableTorchInternal";
                                 }
                                 Logger.d(TAG, "ScreenFlashTask#preCapture: enable torch");
-                                mCameraControl.enableTorchInternal(true);
+                                mCameraControl.enableTorchInternal(TorchControl.USED_AS_FLASH);
                                 completer.set(null);
                                 return "EnableTorchInternal";
                             }),
@@ -828,7 +829,7 @@
         public void postCapture() {
             Logger.d(TAG, "ScreenFlashTask#postCapture");
             if (mUseFlashModeTorchFor3aUpdate.shouldUseFlashModeTorch()) {
-                mCameraControl.enableTorchInternal(false);
+                mCameraControl.enableTorchInternal(TorchControl.OFF);
             }
             mCameraControl.getFocusMeteringControl().enableExternalFlashAeMode(false).addListener(
                     () -> Log.d(TAG, "enableExternalFlashAeMode disabled"), mExecutor
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index d4c634e..7ea9db9 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -18,7 +18,11 @@
 
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.os.Build;
 
+import androidx.annotation.IntDef;
+import androidx.annotation.IntRange;
 import androidx.camera.camera2.internal.annotation.CameraExecutor;
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.workaround.FlashAvailabilityChecker;
@@ -38,6 +42,8 @@
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.concurrent.Executor;
 
 /**
@@ -52,14 +58,32 @@
     private static final String TAG = "TorchControl";
     static final int DEFAULT_TORCH_STATE = TorchState.OFF;
 
+    /** Torch is off. */
+    static final int OFF = 0;
+    /** Torch is turned on explicitly by {@link #enableTorch(boolean)}. */
+    static final int ON = 1;
+    /** Torch is turned on as flash by the capture pipeline. */
+    static final int USED_AS_FLASH = 2;
+
+    /** The internal torch state. */
+    @IntDef({OFF, ON, USED_AS_FLASH})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface TorchStateInternal {
+    }
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     private final Camera2CameraControlImpl mCamera2CameraControlImpl;
     private final MutableLiveData<Integer> mTorchState;
+    private final MutableLiveData<Integer> mTorchStrength;
     private final boolean mHasFlashUnit;
     @CameraExecutor
     private final Executor mExecutor;
 
     private boolean mIsActive;
+    private boolean mIsTorchStrengthSupported;
+    private int mDefaultTorchStrength;
+    private int mTargetTorchStrength;
+    private CallbackToFutureAdapter.Completer<Void> mTorchStrengthCompleter;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
             CallbackToFutureAdapter.Completer<Void> mEnableTorchCompleter;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -79,7 +103,13 @@
         mExecutor = executor;
 
         mHasFlashUnit = FlashAvailabilityChecker.isFlashAvailable(cameraCharacteristics::get);
+        mIsTorchStrengthSupported =
+                mHasFlashUnit && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+                        && cameraCharacteristics.getMaxTorchStrengthLevel() > 1;
+        mDefaultTorchStrength = cameraCharacteristics.getDefaultTorchStrengthLevel();
+        mTargetTorchStrength = mDefaultTorchStrength;
         mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
+        mTorchStrength = new MutableLiveData<>(mDefaultTorchStrength);
         Camera2CameraControlImpl.CaptureResultListener captureResultListener = captureResult -> {
             if (mEnableTorchCompleter != null) {
                 CaptureRequest captureRequest = captureResult.getRequest();
@@ -92,6 +122,16 @@
                     mEnableTorchCompleter = null;
                 }
             }
+            if (mIsTorchStrengthSupported
+                    && Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
+                    && mTorchStrengthCompleter != null) {
+                Integer torchStrength = captureResult.get(CaptureResult.FLASH_STRENGTH_LEVEL);
+
+                if (torchStrength != null && torchStrength == mTargetTorchStrength) {
+                    mTorchStrengthCompleter.set(null);
+                    mTorchStrengthCompleter = null;
+                }
+            }
             // Return false to keep getting captureResult.
             return false;
         };
@@ -114,8 +154,10 @@
         if (!isActive) {
             if (mTargetTorchEnabled) {
                 mTargetTorchEnabled = false;
-                mCamera2CameraControlImpl.enableTorchInternal(false);
-                setLiveDataValue(mTorchState, TorchState.OFF);
+                mTargetTorchStrength = mDefaultTorchStrength;
+                mCamera2CameraControlImpl.enableTorchInternal(OFF);
+                setTorchState(OFF);
+                setLiveDataValue(mTorchStrength, mDefaultTorchStrength);
             }
 
             if (mEnableTorchCompleter != null) {
@@ -123,6 +165,12 @@
                         new OperationCanceledException("Camera is not active."));
                 mEnableTorchCompleter = null;
             }
+
+            if (mTorchStrengthCompleter != null) {
+                mTorchStrengthCompleter.setException(
+                        new OperationCanceledException("Camera is not active."));
+                mTorchStrengthCompleter = null;
+            }
         }
     }
 
@@ -154,11 +202,11 @@
             return Futures.immediateFailedFuture(new IllegalStateException("No flash unit"));
         }
 
-        setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
+        @TorchStateInternal int torchState = enabled ? ON : OFF;
+        setTorchState(torchState);
 
         return CallbackToFutureAdapter.getFuture(completer -> {
-            mExecutor.execute(
-                    () -> enableTorchInternal(completer, enabled));
+            mExecutor.execute(() -> enableTorchInternal(completer, torchState));
             return "enableTorch: " + enabled;
         });
     }
@@ -175,9 +223,14 @@
         return mTorchState;
     }
 
+    @NonNull LiveData<Integer> getTorchStrengthLevel() {
+        return mTorchStrength;
+    }
+
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @ExecutedBy("mExecutor")
-    void enableTorchInternal(@Nullable Completer<Void> completer, boolean enabled) {
+    void enableTorchInternal(@Nullable Completer<Void> completer,
+            @TorchStateInternal int torchState) {
         if (!mHasFlashUnit) {
             if (completer != null) {
                 completer.setException(new IllegalStateException("No flash unit"));
@@ -186,7 +239,7 @@
         }
 
         if (!mIsActive) {
-            setLiveDataValue(mTorchState, TorchState.OFF);
+            setTorchState(OFF);
             if (completer != null) {
                 completer.setException(new OperationCanceledException("Camera is not active."));
             }
@@ -201,9 +254,9 @@
             return;
         }
 
-        mTargetTorchEnabled = enabled;
-        mCamera2CameraControlImpl.enableTorchInternal(enabled);
-        setLiveDataValue(mTorchState, enabled ? TorchState.ON : TorchState.OFF);
+        mTargetTorchEnabled = torchState != OFF;
+        mCamera2CameraControlImpl.enableTorchInternal(torchState);
+        setTorchState(torchState);
         if (mEnableTorchCompleter != null) {
             mEnableTorchCompleter.setException(new OperationCanceledException(
                     "There is a new enableTorch being set"));
@@ -211,6 +264,54 @@
         mEnableTorchCompleter = completer;
     }
 
+    ListenableFuture<Void> setTorchStrengthLevel(@IntRange(from = 1) int torchStrengthLevel) {
+        if (!mIsTorchStrengthSupported) {
+            return Futures.immediateFailedFuture(new UnsupportedOperationException(
+                    "Setting torch strength is not supported on the device."));
+        }
+
+        setLiveDataValue(mTorchStrength, torchStrengthLevel);
+
+        return CallbackToFutureAdapter.getFuture(completer -> {
+            mExecutor.execute(
+                    () -> setTorchStrengthLevelInternal(completer, torchStrengthLevel));
+            return "setTorchStrength: " + torchStrengthLevel;
+        });
+    }
+
+    @ExecutedBy("mExecutor")
+    void setTorchStrengthLevelInternal(@Nullable Completer<Void> completer,
+            @IntRange(from = 1) int torchStrengthLevel) {
+        if (!mIsTorchStrengthSupported) {
+            if (completer != null) {
+                completer.setException(new UnsupportedOperationException(
+                        "Setting torch strength is not supported on the device."));
+            }
+            return;
+        }
+
+        if (!mIsActive) {
+            if (completer != null) {
+                completer.setException(new OperationCanceledException("Camera is not active."));
+            }
+            return;
+        }
+
+        mTargetTorchStrength = torchStrengthLevel;
+        mCamera2CameraControlImpl.setTorchStrengthLevelInternal(torchStrengthLevel);
+        if (!mCamera2CameraControlImpl.isTorchOn() && completer != null) {
+            // Complete the future if the torch is not on. The new strength will be applied next
+            // time it's turned on.
+            completer.set(null);
+        } else {
+            if (mTorchStrengthCompleter != null) {
+                mTorchStrengthCompleter.setException(new OperationCanceledException(
+                        "There is a new torch strength being set."));
+            }
+            mTorchStrengthCompleter = completer;
+        }
+    }
+
     /**
      * Force update the torch state to OFF.
      *
@@ -225,7 +326,25 @@
         }
 
         mTargetTorchEnabled = false;
-        setLiveDataValue(mTorchState, TorchState.OFF);
+        setTorchState(OFF);
+    }
+
+    private void setTorchState(@TorchStateInternal int internalState) {
+        @TorchState.State int state;
+        switch (internalState) {
+            case ON:
+                state = TorchState.ON;
+                break;
+            case USED_AS_FLASH:
+                // If torch is turned on as flash, it's considered off because it's not used for
+                // torch purpose.
+                // Fall-through
+            case OFF:
+                // Fall-through
+            default:
+                state = TorchState.OFF;
+        }
+        setLiveDataValue(mTorchState, state);
     }
 
     private <T> void setLiveDataValue(@NonNull MutableLiveData<T> liveData, T value) {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
index d92dd68..1f2cc60 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/CameraCharacteristicsCompat.java
@@ -131,6 +131,28 @@
     }
 
     /**
+     * Returns the default torch strength level.
+     */
+    public int getDefaultTorchStrengthLevel() {
+        Integer defaultLevel = null;
+        if (Build.VERSION.SDK_INT >= 35) {
+            defaultLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL);
+        }
+        return defaultLevel == null ? 1 : defaultLevel;
+    }
+
+    /**
+     * Returns the maximum torch strength level.
+     */
+    public int getMaxTorchStrengthLevel() {
+        Integer maxLevel = null;
+        if (Build.VERSION.SDK_INT >= 35) {
+            maxLevel = get(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL);
+        }
+        return maxLevel == null ? 1 : maxLevel;
+    }
+
+    /**
      * Obtains the {@link StreamConfigurationMapCompat} which contains the output sizes related
      * workarounds in it.
      */
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 529c505..e7fcc70 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -137,6 +137,8 @@
     @RequiresApi(33)
     private static final DynamicRangeProfiles CAMERA0_DYNAMIC_RANGE_PROFILES =
             new DynamicRangeProfiles(new long[]{DynamicRangeProfiles.HLG10, 0, 0});
+    private static final int CAMERA0_DEFAULT_TORCH_STRENGTH = 25;
+    private static final int CAMERA0_MAX_TORCH_STRENGTH = 50;
 
     private static final String CAMERA1_ID = "1";
     private static final int CAMERA1_SUPPORTED_HARDWARE_LEVEL =
@@ -876,6 +878,44 @@
         assertThat(supportedDynamicRanges).containsExactly(SDR, HLG_10_BIT);
     }
 
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_torchStrengthPropagateCorrectly()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
+                mCameraManagerCompat);
+        int strength = 30;
+        when(mMockTorchControl.getTorchStrengthLevel()).thenReturn(new MutableLiveData<>(strength));
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
+
+        assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(strength);
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnDefaultTorchStrength()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getTorchStrengthLevel().getValue()).isEqualTo(
+                CAMERA0_DEFAULT_TORCH_STRENGTH);
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @Test
+    public void apiVersionMet_canReturnMaxTorchStrength()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(CAMERA0_MAX_TORCH_STRENGTH);
+    }
+
     @Config(minSdk = 33)
     @Test
     public void apiVersionMet_canReturnSupportedDynamicRanges_fromFullySpecified()
@@ -902,6 +942,17 @@
         assertThat(supportedDynamicRanges).containsExactly(SDR);
     }
 
+    @Config(maxSdk = Build.VERSION_CODES.VANILLA_ICE_CREAM - 1)
+    @Test
+    public void apiVersionNotMet_returnMaxTorchStrengthOne()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final CameraInfo cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
+
+        assertThat(cameraInfo.getMaxTorchStrengthLevel()).isEqualTo(1);
+    }
+
     @Config(maxSdk = 32)
     @Test
     public void apiVersionNotMet_queryHdrDynamicRangeNotSupported()
@@ -1096,6 +1147,10 @@
                             CONTROL_AE_MODE_ON_LOW_LIGHT_BOOST_BRIGHTNESS_PRIORITY
                     }
             );
+            shadowCharacteristics0.set(CameraCharacteristics.FLASH_TORCH_STRENGTH_DEFAULT_LEVEL,
+                    CAMERA0_DEFAULT_TORCH_STRENGTH);
+            shadowCharacteristics0.set(CameraCharacteristics.FLASH_TORCH_STRENGTH_MAX_LEVEL,
+                    CAMERA0_MAX_TORCH_STRENGTH);
         }
 
         // Mock the request capability
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
index 39b8743..87ddf67 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ScreenFlashTaskTest.kt
@@ -353,8 +353,8 @@
             return screenFlash
         }
 
-        override fun enableTorchInternal(torch: Boolean) {
-            isTorchEnabled = torch
+        override fun enableTorchInternal(torchState: Int) {
+            isTorchEnabled = torchState != TorchControl.OFF
         }
 
         override fun addCaptureResultListener(listener: CaptureResultListener) {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
index 6b25593..09718fb 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraControl.java
@@ -17,6 +17,7 @@
 package androidx.camera.core;
 
 import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
 import androidx.annotation.RestrictTo;
 import androidx.camera.core.impl.utils.futures.Futures;
 
@@ -233,6 +234,29 @@
     @NonNull ListenableFuture<Integer> setExposureCompensationIndex(int value);
 
     /**
+     * Sets torch strength level.
+     *
+     * <p>The torch strength level only applies on the case that torch is turned on by
+     * {@link #enableTorch(boolean)} and doesn't affect other usages of the flash unit.
+     *
+     * <p>Use the value returned by {@link CameraInfo#getMaxTorchStrengthLevel()} to set the maximum
+     * level the device can provide and use {@code 1} to set the minimum level. If a level
+     * greater than the maximum value or less than {@code 1} is set, the returned
+     * {@link ListenableFuture} will fail with an {@link IllegalArgumentException} and it won't
+     * modify the torch strength.
+     *
+     * @param torchStrengthLevel The desired torch strength level.
+     * @return a {@link ListenableFuture} that is completed when the torch strength has been
+     * applied.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    default @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+            @IntRange(from = 1) int torchStrengthLevel) {
+        return Futures.immediateFailedFuture(new UnsupportedOperationException(
+                "Setting torch strength is not supported on the device."));
+    }
+
+    /**
      * An exception representing a failure that the operation is canceled which might be caused by
      * a new value is set or camera is closed.
      *
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
index efbb3c3..91b2f00 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraInfo.java
@@ -23,6 +23,7 @@
 import android.view.Surface;
 
 import androidx.annotation.FloatRange;
+import androidx.annotation.IntRange;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.StringDef;
@@ -425,6 +426,29 @@
         return Collections.emptySet();
     }
 
+    /**
+     * Returns the maximum torch strength level.
+     *
+     * @return The maximum strength level. If the device doesn't support configuring torch
+     * strength, returns {@code 1}.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    @IntRange(from = 1)
+    default int getMaxTorchStrengthLevel() {
+        return 1;
+    }
+
+    /**
+     * Returns the {@link LiveData} of the torch strength level.
+     *
+     * <p>The value of the {@link LiveData} will be the default torch strength level of this
+     * device if {@link CameraControl#setTorchStrengthLevelAsync(int)} hasn't been called.
+     */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    default @NonNull LiveData<Integer> getTorchStrengthLevel() {
+        return new MutableLiveData<>(1);
+    }
+
     @StringDef(open = true, value = {IMPLEMENTATION_TYPE_UNKNOWN,
             IMPLEMENTATION_TYPE_CAMERA2_LEGACY, IMPLEMENTATION_TYPE_CAMERA2,
             IMPLEMENTATION_TYPE_FAKE})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java b/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
index c7da97c..23f50af 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/TorchState.java
@@ -32,8 +32,7 @@
     private TorchState() {
     }
 
-    /**
-     */
+    /** The camera flash torch state. */
     @IntDef({OFF, ON})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
index cf17fc7..b39ebca 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraControl.java
@@ -18,6 +18,7 @@
 
 import android.graphics.Rect;
 
+import androidx.annotation.IntRange;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.FocusMeteringResult;
@@ -83,6 +84,12 @@
     }
 
     @Override
+    public @NonNull ListenableFuture<Void> setTorchStrengthLevelAsync(
+            @IntRange(from = 1) int torchStrengthLevel) {
+        return mCameraControlInternal.setTorchStrengthLevelAsync(torchStrengthLevel);
+    }
+
+    @Override
     @ImageCapture.FlashMode
     public int getFlashMode() {
         return mCameraControlInternal.getFlashMode();
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
index fd752fd..10d896a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ForwardingCameraInfo.java
@@ -73,6 +73,16 @@
     }
 
     @Override
+    public @NonNull LiveData<Integer> getTorchStrengthLevel() {
+        return mCameraInfoInternal.getTorchStrengthLevel();
+    }
+
+    @Override
+    public int getMaxTorchStrengthLevel() {
+        return mCameraInfoInternal.getMaxTorchStrengthLevel();
+    }
+
+    @Override
     public boolean isLowLightBoostSupported() {
         return mCameraInfoInternal.isLowLightBoostSupported();
     }
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
index a93558e..befe1c0 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/VideoRecordingTest.kt
@@ -342,6 +342,7 @@
         videoCapture.setViewPortCropRect(cropRect)
 
         checkAndBindUseCases(preview, videoCapture)
+        val calculatedCropRect = videoCapture.cropRect!!
 
         // TODO(b/264936115): In stream sharing (VirtualCameraAdapter), children's ViewPortCropRect
         //  is ignored and override to the parent size, the cropRect is also rotated. Skip the test
@@ -352,7 +353,7 @@
         val result = recordingSession.createRecording(recorder = recorder).recordAndVerify()
 
         // Verify.
-        val resolution = rectToSize(videoCapture.cropRect!!)
+        val resolution = rectToSize(calculatedCropRect)
         verifyVideoResolution(
             context,
             result.file,
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt
new file mode 100644
index 0000000..a83099b
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/EllipsizeRedrawDemo.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.demos.text
+
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+
+@Preview
+@Composable
+fun EllipsizeRedrawDemo() {
+    val transition = rememberInfiniteTransition("padding")
+    val padding = transition.animateFloat(0f, 50f, infiniteRepeatable(tween(10000)))
+    Box(
+        modifier =
+            Modifier.padding(top = padding.value.dp)
+                .fillMaxSize()
+                .border(1.dp, Color.Blue)
+                .padding(24.dp)
+                .border(1.dp, Color.Cyan),
+        contentAlignment = Alignment.Center
+    ) {
+        Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+            BasicText("If b/389707025 fixed, the following should display with no animation")
+
+            // reset everything at 49
+            key(padding.value.toInt() / 49) {
+                BasicText(
+                    text = "I will definitely fill the screen!".repeat(10),
+                    modifier = Modifier.border(1.dp, Color.Red),
+                    style =
+                        TextStyle(
+                            textAlign = TextAlign.Center,
+                            letterSpacing = 1.sp,
+                            lineHeight = 24.sp,
+                            lineHeightStyle =
+                                LineHeightStyle(
+                                    alignment = LineHeightStyle.Alignment.Center,
+                                    trim = LineHeightStyle.Trim.None,
+                                    mode = LineHeightStyle.Mode.Fixed
+                                ),
+                        ),
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 1,
+                    color = { Color.Black }
+                )
+                BasicText(
+                    text = "I will definitely fill the screen!".repeat(10),
+                    modifier = Modifier.border(1.dp, Color.Red),
+                    style =
+                        TextStyle(
+                            textAlign = TextAlign.Center,
+                            letterSpacing = 1.sp,
+                            lineHeight = 24.sp,
+                            lineHeightStyle =
+                                LineHeightStyle(
+                                    alignment = LineHeightStyle.Alignment.Center,
+                                    trim = LineHeightStyle.Trim.None,
+                                    mode = LineHeightStyle.Mode.Fixed
+                                ),
+                        ),
+                    overflow = TextOverflow.Ellipsis,
+                    maxLines = 3,
+                    color = { Color.Black }
+                )
+            }
+        }
+    }
+}
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 fe61539..22d952e2 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
@@ -92,6 +92,9 @@
                             ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
                             ComposableDemo("Get last character after clip") {
                                 LastClippedCharacterDemo()
+                            },
+                            ComposableDemo("Ellipses plays well with redraw") {
+                                EllipsizeRedrawDemo()
                             }
                         )
                     ),
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
index 855071a..bdc4452 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/BasicTextScreenshotTest.kt
@@ -18,8 +18,17 @@
 
 import android.os.Build
 import androidx.compose.foundation.GOLDEN_FOUNDATION
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -30,6 +39,10 @@
 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.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -50,6 +63,50 @@
     private val textTag = "text"
 
     @Test
+    fun basicTextEllipsisCentered_leadingMarginCorrect_doesntMarch_b389707025() {
+        val padding = mutableStateOf(0.dp)
+        rule.setContent {
+            Box(
+                modifier =
+                    Modifier.padding(top = padding.value).fillMaxSize().border(1.dp, Color.Blue),
+                contentAlignment = Alignment.Center
+            ) {
+                Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+                    BasicText(
+                        text = "I will definitely fill the screen!".repeat(10),
+                        modifier = Modifier.border(1.dp, Color.Red).testTag(textTag),
+                        style =
+                            TextStyle(
+                                textAlign = TextAlign.Center,
+                                letterSpacing = 1.sp,
+                                lineHeight = 24.sp,
+                                lineHeightStyle =
+                                    LineHeightStyle(
+                                        alignment = LineHeightStyle.Alignment.Center,
+                                        trim = LineHeightStyle.Trim.None,
+                                        mode = LineHeightStyle.Mode.Fixed
+                                    ),
+                            ),
+                        overflow = TextOverflow.Ellipsis,
+                        maxLines = 1,
+                        color = { Color.Black }
+                    )
+                }
+            }
+        }
+
+        repeat(2) {
+            // these repaints cause the reproduction of b/389707025
+            rule.waitForIdle()
+            padding.value = padding.value + 1.dp
+        }
+        rule
+            .onNodeWithTag(textTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, "leadingMarginForEllipsis")
+    }
+
+    @Test
     fun multiStyleText_setFontWeight() {
         rule.setContent {
             BasicText(
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index ee62807..a3501ba 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -1036,10 +1036,10 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingToolbarState FloatingToolbarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingToolbarState rememberFloatingToolbarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
   }
 
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index ee62807..a3501ba 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -1036,10 +1036,10 @@
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi public static androidx.compose.material3.FloatingToolbarState FloatingToolbarState(float initialOffsetLimit, float initialOffset, float initialContentOffset);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void HorizontalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @Deprecated @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? leadingContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? trailingContent, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
-    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static void VerticalFloatingToolbar(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> floatingActionButton, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material3.FloatingToolbarColors colors, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.material3.FloatingToolbarScrollBehavior? scrollBehavior, optional androidx.compose.ui.graphics.Shape shape, optional int floatingActionButtonPosition, optional androidx.compose.animation.core.FiniteAnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method @SuppressCompatibility @androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @androidx.compose.runtime.Composable public static androidx.compose.material3.FloatingToolbarState rememberFloatingToolbarState(optional float initialOffsetLimit, optional float initialOffset, optional float initialContentOffset);
   }
 
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index c025935..dce4bf4 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -111,6 +111,7 @@
                 implementation("androidx.compose.material:material-icons-core:1.7.5")
                 implementation(project(":test:screenshot:screenshot"))
                 implementation(project(":core:core"))
+                implementation("androidx.compose.ui:ui-tooling:1.4.1")
                 implementation(libs.espressoCore)
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
diff --git a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
index be06c8a..6d3b4e5 100644
--- a/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
+++ b/compose/material3/material3/integration-tests/material3-catalog/src/main/java/androidx/compose/material3/catalog/library/model/Examples.kt
@@ -40,6 +40,8 @@
 import androidx.compose.material3.samples.ButtonSample
 import androidx.compose.material3.samples.ButtonWithIconSample
 import androidx.compose.material3.samples.CardSample
+import androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
+import androidx.compose.material3.samples.CenteredVerticalFloatingToolbarWithFabSample
 import androidx.compose.material3.samples.CheckboxSample
 import androidx.compose.material3.samples.CheckboxWithTextSample
 import androidx.compose.material3.samples.ChipGroupReflowSample
@@ -888,11 +890,11 @@
             HorizontalFloatingToolbarWithFabSample()
         },
         Example(
-            name = "VerticalFloatingToolbarWithFabSample",
+            name = "CenteredHorizontalFloatingToolbarWithFabSample",
             description = FloatingToolbarsExampleDescription,
             sourceUrl = FloatingToolbarsExampleSourceUrl,
         ) {
-            VerticalFloatingToolbarWithFabSample()
+            CenteredHorizontalFloatingToolbarWithFabSample()
         },
         Example(
             name = "HorizontalFloatingToolbarAsScaffoldFabSample",
@@ -901,6 +903,20 @@
         ) {
             HorizontalFloatingToolbarAsScaffoldFabSample()
         },
+        Example(
+            name = "VerticalFloatingToolbarWithFabSample",
+            description = FloatingToolbarsExampleDescription,
+            sourceUrl = FloatingToolbarsExampleSourceUrl,
+        ) {
+            VerticalFloatingToolbarWithFabSample()
+        },
+        Example(
+            name = "CenteredVerticalFloatingToolbarWithFabSample",
+            description = FloatingToolbarsExampleDescription,
+            sourceUrl = FloatingToolbarsExampleSourceUrl,
+        ) {
+            CenteredVerticalFloatingToolbarWithFabSample()
+        },
     )
 
 private const val ExtendedFABExampleDescription = "Extended FAB examples"
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
index ed11f6b..ccaa941 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/FloatingToolbarSamples.kt
@@ -61,8 +61,7 @@
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment.Companion.BottomCenter
-import androidx.compose.ui.Alignment.Companion.CenterEnd
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -107,7 +106,7 @@
                     }
                 }
                 HorizontalFloatingToolbar(
-                    modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+                    modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
                     expanded = expanded || isTouchExplorationEnabled,
                     leadingContent = { leadingContent() },
                     trailingContent = { trailingContent() },
@@ -157,7 +156,7 @@
                     }
                 }
                 HorizontalFloatingToolbar(
-                    modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+                    modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
                     expanded = true,
                     leadingContent = { leadingContent() },
                     trailingContent = { trailingContent() },
@@ -214,7 +213,7 @@
                     }
                 }
                 VerticalFloatingToolbar(
-                    modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+                    modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
                     expanded = expanded || isTouchExplorationEnabled,
                     leadingContent = { leadingContent() },
                     trailingContent = { trailingContent() },
@@ -264,7 +263,7 @@
                     }
                 }
                 VerticalFloatingToolbar(
-                    modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+                    modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
                     expanded = true,
                     leadingContent = { leadingContent() },
                     trailingContent = { trailingContent() },
@@ -330,7 +329,9 @@
                         Icon(Icons.Filled.Add, "Localized description")
                     }
                 },
-                modifier = Modifier.align(BottomCenter).offset(y = -ScreenOffset),
+                modifier =
+                    Modifier.align(Alignment.BottomEnd)
+                        .offset(x = -ScreenOffset, y = -ScreenOffset),
                 colors = vibrantColors,
                 content = {
                     IconButton(onClick = { /* doSomething() */ }) {
@@ -355,6 +356,62 @@
 @Preview
 @Sampled
 @Composable
+fun CenteredHorizontalFloatingToolbarWithFabSample() {
+    val context = LocalContext.current
+    val isTouchExplorationEnabled = remember {
+        val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+        am.isEnabled && am.isTouchExplorationEnabled
+    }
+    val exitAlwaysScrollBehavior =
+        FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = Bottom)
+    val vibrantColors = FloatingToolbarDefaults.vibrantFloatingToolbarColors()
+    Scaffold(modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior)) { innerPadding ->
+        Box(Modifier.padding(innerPadding)) {
+            Column(
+                Modifier.fillMaxWidth()
+                    .padding(horizontal = 16.dp)
+                    .verticalScroll(rememberScrollState())
+            ) {
+                Text(text = remember { LoremIpsum().values.first() })
+            }
+            HorizontalFloatingToolbar(
+                // Always expanded as the toolbar is bottom-centered. We will use a
+                // FloatingToolbarScrollBehavior to hide both the toolbar and its FAB on scroll.
+                expanded = true,
+                floatingActionButton = {
+                    // Match the FAB to the vibrantColors. See also StandardFloatingActionButton.
+                    FloatingToolbarDefaults.VibrantFloatingActionButton(
+                        onClick = { /* doSomething() */ },
+                    ) {
+                        Icon(Icons.Filled.Add, "Localized description")
+                    }
+                },
+                modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
+                colors = vibrantColors,
+                scrollBehavior = if (!isTouchExplorationEnabled) exitAlwaysScrollBehavior else null,
+                content = {
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Person, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.MoreVert, contentDescription = "Localized description")
+                    }
+                },
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
 fun VerticalFloatingToolbarWithFabSample() {
     val context = LocalContext.current
     val isTouchExplorationEnabled = remember {
@@ -396,7 +453,9 @@
                         Icon(Icons.Filled.Add, "Localized description")
                     }
                 },
-                modifier = Modifier.align(CenterEnd).offset(x = -ScreenOffset),
+                modifier =
+                    Modifier.align(Alignment.BottomEnd)
+                        .offset(x = -ScreenOffset, y = -ScreenOffset),
                 colors = vibrantColors,
                 content = {
                     IconButton(onClick = { /* doSomething() */ }) {
@@ -421,6 +480,62 @@
 @Preview
 @Sampled
 @Composable
+fun CenteredVerticalFloatingToolbarWithFabSample() {
+    val context = LocalContext.current
+    val isTouchExplorationEnabled = remember {
+        val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
+        am.isEnabled && am.isTouchExplorationEnabled
+    }
+    val exitAlwaysScrollBehavior =
+        FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+    val vibrantColors = FloatingToolbarDefaults.vibrantFloatingToolbarColors()
+    Scaffold(modifier = Modifier.nestedScroll(exitAlwaysScrollBehavior)) { innerPadding ->
+        Box(Modifier.padding(innerPadding)) {
+            Column(
+                Modifier.fillMaxWidth()
+                    .padding(horizontal = 16.dp)
+                    .verticalScroll(rememberScrollState())
+            ) {
+                Text(text = remember { LoremIpsum().values.first() })
+            }
+            VerticalFloatingToolbar(
+                // Always expanded as the toolbar is right-centered. We will use a
+                // FloatingToolbarScrollBehavior to hide both the toolbar and its FAB on scroll.
+                expanded = true,
+                floatingActionButton = {
+                    // Match the FAB to the vibrantColors. See also StandardFloatingActionButton.
+                    FloatingToolbarDefaults.VibrantFloatingActionButton(
+                        onClick = { /* doSomething() */ },
+                    ) {
+                        Icon(Icons.Filled.Add, "Localized description")
+                    }
+                },
+                modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
+                colors = vibrantColors,
+                scrollBehavior = if (!isTouchExplorationEnabled) exitAlwaysScrollBehavior else null,
+                content = {
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Person, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Edit, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    }
+                    IconButton(onClick = { /* doSomething() */ }) {
+                        Icon(Icons.Filled.MoreVert, contentDescription = "Localized description")
+                    }
+                },
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Preview
+@Sampled
+@Composable
 fun HorizontalFloatingToolbarAsScaffoldFabSample() {
     val context = LocalContext.current
     val isTouchExplorationEnabled = remember {
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
index b9b0ac3..de628ad 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/FloatingToolbarTest.kt
@@ -26,25 +26,32 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
 import androidx.compose.material3.FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
 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.testutils.assertContainsColor
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertCountEquals
@@ -64,6 +71,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -92,7 +100,7 @@
         val scrollHeightOffsetDp = 20.dp
         var scrollHeightOffsetPx = 0f
         var containerSizePx = 0f
-        val screenOffsetDp = FloatingToolbarDefaults.ScreenOffset
+        val screenOffsetDp = ScreenOffset
         var screenOffsetPx = 0f
 
         rule.setMaterialContent(lightColorScheme()) {
@@ -143,7 +151,7 @@
         val scrollHeightOffsetDp = 20.dp
         var scrollHeightOffsetPx = 0f
         var containerSizePx = 0f
-        val screenOffsetDp = FloatingToolbarDefaults.ScreenOffset
+        val screenOffsetDp = ScreenOffset
         var screenOffsetPx = 0f
 
         rule.setMaterialContent(lightColorScheme()) {
@@ -918,6 +926,76 @@
         rule.runOnIdle { assertThat(expanded).isEqualTo(false) }
     }
 
+    @Test
+    fun verticalFloatingToolbar_scrollBehavior() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scrollBehavior =
+                FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+            Scaffold(modifier = Modifier.nestedScroll(scrollBehavior).testTag(mainLayoutTag)) {
+                innerPadding ->
+                Box(Modifier.padding(innerPadding)) {
+                    Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+                        Text(text = remember { LoremIpsum().values.first() })
+                    }
+                    VerticalFloatingToolbar(
+                        expanded = true,
+                        floatingActionButton = { ToolbarFab() },
+                        modifier = Modifier.align(Alignment.CenterEnd).offset(x = -ScreenOffset),
+                        scrollBehavior = scrollBehavior,
+                    ) {
+                        ToolbarContent()
+                    }
+                }
+            }
+        }
+
+        // Check that the FAB and a sample from the toolbar content are displayed.
+        rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsDisplayed()
+        rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsDisplayed()
+
+        // Swipe the content up to collapse the FloatingToolbar.
+        rule.onNodeWithTag(mainLayoutTag).performTouchInput { swipeUp(bottom, bottom - 1000) }
+        rule.waitForIdle()
+        // Check that the FAB and a sample from the toolbar content are not displayed.
+        rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsNotDisplayed()
+        rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsNotDisplayed()
+    }
+
+    @Test
+    fun horizontalFloatingToolbar_scrollBehavior() {
+        rule.setMaterialContent(lightColorScheme()) {
+            val scrollBehavior =
+                FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End)
+            Scaffold(modifier = Modifier.nestedScroll(scrollBehavior).testTag(mainLayoutTag)) {
+                innerPadding ->
+                Box(Modifier.padding(innerPadding)) {
+                    Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) {
+                        Text(text = remember { LoremIpsum().values.first() })
+                    }
+                    HorizontalFloatingToolbar(
+                        expanded = true,
+                        floatingActionButton = { ToolbarFab() },
+                        modifier = Modifier.align(Alignment.BottomCenter).offset(y = -ScreenOffset),
+                        scrollBehavior = scrollBehavior,
+                    ) {
+                        ToolbarContent()
+                    }
+                }
+            }
+        }
+
+        // Check that the FAB and a sample from the toolbar content are displayed.
+        rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsDisplayed()
+        rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsDisplayed()
+
+        // Swipe the content up to collapse the FloatingToolbar.
+        rule.onNodeWithTag(mainLayoutTag).performTouchInput { swipeUp(bottom, bottom - 1000) }
+        rule.waitForIdle()
+        // Check that the FAB and a sample from the toolbar content are not displayed.
+        rule.onNodeWithTag(FloatingActionButtonTestTag).assertIsNotDisplayed()
+        rule.onNodeWithTag(FloatingToolbarContentLastItemTestTag).assertIsNotDisplayed()
+    }
+
     @Composable
     private fun VerticalNestedScrollTestContent(
         onExpanded: () -> Unit,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
index e0e61a2..6ac3dec 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingToolbar.kt
@@ -51,12 +51,15 @@
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.FloatingToolbarDefaults.horizontalEnterTransition
 import androidx.compose.material3.FloatingToolbarDefaults.horizontalExitTransition
+import androidx.compose.material3.FloatingToolbarDefaults.standardFloatingToolbarColors
 import androidx.compose.material3.FloatingToolbarDefaults.verticalEnterTransition
 import androidx.compose.material3.FloatingToolbarDefaults.verticalExitTransition
+import androidx.compose.material3.FloatingToolbarDefaults.vibrantFloatingToolbarColors
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Bottom
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.End
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Start
 import androidx.compose.material3.FloatingToolbarExitDirection.Companion.Top
+import androidx.compose.material3.FloatingToolbarState.Companion.Saver
 import androidx.compose.material3.tokens.ColorSchemeKeyTokens
 import androidx.compose.material3.tokens.ElevationTokens
 import androidx.compose.material3.tokens.FabBaselineTokens
@@ -262,11 +265,22 @@
  * other content, and even in a `Scaffold`'s floating action button slot. Its [expanded] flag
  * controls the visibility of the actions with a slide animations.
  *
- * Note that if your app uses a `Snackbar`, it's best to position the toolbar in a `Scaffold`'s FAB
- * slot. This ensures the `Snackbar` appears above the toolbar, preventing any visual overlap or
- * interference.
+ * In case the toolbar is aligned to the right or the left of the screen, you may apply a
+ * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
+ * state when scrolling occurs, as this sample shows:
  *
  * @sample androidx.compose.material3.samples.HorizontalFloatingToolbarWithFabSample
+ *
+ * In case the toolbar is positioned along a center edge of the screen (like top or bottom center),
+ * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
+ * order to hide or show the entire component, as this sample shows:
+ *
+ * @sample androidx.compose.material3.samples.CenteredHorizontalFloatingToolbarWithFabSample
+ *
+ * Note that if your app uses a `Snackbar`, it's best to position the toolbar in a `Scaffold`'s FAB
+ * slot. This ensures the `Snackbar` appears above the toolbar, preventing any visual overlap or
+ * interference. See this sample:
+ *
  * @sample androidx.compose.material3.samples.HorizontalFloatingToolbarAsScaffoldFabSample
  * @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
  *   and the toolbar content are organized horizontally. Otherwise, only the FAB is visible.
@@ -279,6 +293,12 @@
  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
  *   [floatingActionButton] for more information on the right FAB to use for proper styling.
  * @param contentPadding the padding applied to the content of this floating toolbar.
+ * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
+ *   automatically react to scrolling. If your toolbar is positioned along a center edge of the
+ *   screen (like top or bottom center), it's best to use this scroll behavior to make the entire
+ *   toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
+ *   off-center, which may occur in this case when using the [expanded] flag to simply expand or
+ *   collapse the toolbar.
  * @param shape the shape used for this floating toolbar content.
  * @param floatingActionButtonPosition the position of the floating toolbar's floating action
  *   button. By default, the FAB is placed at the end of the toolbar (i.e. aligned to the right in
@@ -296,6 +316,7 @@
     modifier: Modifier = Modifier,
     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
+    scrollBehavior: FloatingToolbarScrollBehavior? = null,
     shape: Shape = FloatingToolbarDefaults.ContainerShape,
     floatingActionButtonPosition: FloatingToolbarHorizontalFabPosition =
         FloatingToolbarHorizontalFabPosition.End,
@@ -308,6 +329,7 @@
         colors = colors,
         toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
         toolbarContentPadding = contentPadding,
+        scrollBehavior = scrollBehavior,
         toolbarShape = shape,
         animationSpec = animationSpec,
         fab = floatingActionButton,
@@ -469,7 +491,17 @@
  * other content, and its [expanded] flag controls the visibility of the actions with a slide
  * animations.
  *
+ * In case the toolbar is aligned to the top or the bottom of the screen, you may apply a
+ * [FloatingToolbarDefaults.floatingToolbarVerticalNestedScroll] `Modifier` to update the [expanded]
+ * state when scrolling occurs, as this sample shows:
+ *
  * @sample androidx.compose.material3.samples.VerticalFloatingToolbarWithFabSample
+ *
+ * In case the toolbar is positioned along a center edge of the screen (like left or right center),
+ * it's recommended to maintain the expanded state on scroll and to attach a [scrollBehavior] in
+ * order to hide or show the entire component, as this sample shows:
+ *
+ * @sample androidx.compose.material3.samples.CenteredVerticalFloatingToolbarWithFabSample
  * @param expanded whether the floating toolbar is expanded or not. In its expanded state, the FAB
  *   and the toolbar content are organized vertically. Otherwise, only the FAB is visible.
  * @param floatingActionButton a floating action button to be displayed by the toolbar. It's
@@ -481,6 +513,12 @@
  *   [FloatingToolbarDefaults.vibrantFloatingToolbarColors] which you can use or modify. See also
  *   [floatingActionButton] for more information on the right FAB to use for proper styling.
  * @param contentPadding the padding applied to the content of this floating toolbar.
+ * @param scrollBehavior a [FloatingToolbarScrollBehavior]. If provided, this FloatingToolbar will
+ *   automatically react to scrolling. If your toolbar is positioned along a center edge of the
+ *   screen (like left or right center), it's best to use this scroll behavior to make the entire
+ *   toolbar scroll off-screen as the user scrolls. This would prevent the FAB from appearing
+ *   off-center, which may occur in this case when using the [expanded] flag to simply expand or
+ *   collapse the toolbar.
  * @param shape the shape used for this floating toolbar content.
  * @param floatingActionButtonPosition the position of the floating toolbar's floating action
  *   button. By default, the FAB is placed at the bottom of the toolbar (i.e. aligned to the
@@ -498,6 +536,7 @@
     modifier: Modifier = Modifier,
     colors: FloatingToolbarColors = FloatingToolbarDefaults.standardFloatingToolbarColors(),
     contentPadding: PaddingValues = FloatingToolbarDefaults.ContentPadding,
+    scrollBehavior: FloatingToolbarScrollBehavior? = null,
     shape: Shape = FloatingToolbarDefaults.ContainerShape,
     floatingActionButtonPosition: FloatingToolbarVerticalFabPosition =
         FloatingToolbarVerticalFabPosition.Bottom,
@@ -510,6 +549,7 @@
         colors = colors,
         toolbarToFabGap = FloatingToolbarDefaults.ToolbarToFabGap,
         toolbarContentPadding = contentPadding,
+        scrollBehavior = scrollBehavior,
         toolbarShape = shape,
         animationSpec = animationSpec,
         fab = floatingActionButton,
@@ -1489,6 +1529,7 @@
     colors: FloatingToolbarColors,
     toolbarToFabGap: Dp,
     toolbarContentPadding: PaddingValues,
+    scrollBehavior: FloatingToolbarScrollBehavior?,
     toolbarShape: Shape,
     animationSpec: FiniteAnimationSpec<Float>,
     fab: @Composable () -> Unit,
@@ -1514,7 +1555,12 @@
             fab()
         },
         modifier =
-            modifier.defaultMinSize(minHeight = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+            modifier
+                .defaultMinSize(minHeight = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+                .then(
+                    scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
+                        ?: Modifier
+                )
     ) { measurables, constraints ->
         val toolbarMeasurable = measurables[0]
         val fabMeasurable = measurables[1]
@@ -1596,6 +1642,7 @@
     colors: FloatingToolbarColors,
     toolbarToFabGap: Dp,
     toolbarContentPadding: PaddingValues,
+    scrollBehavior: FloatingToolbarScrollBehavior?,
     toolbarShape: Shape,
     animationSpec: FiniteAnimationSpec<Float>,
     fab: @Composable () -> Unit,
@@ -1621,7 +1668,12 @@
             fab()
         },
         modifier =
-            modifier.defaultMinSize(minWidth = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+            modifier
+                .defaultMinSize(minWidth = FloatingToolbarDefaults.FabSizeRange.endInclusive)
+                .then(
+                    scrollBehavior?.let { with(it) { Modifier.floatingScrollBehavior() } }
+                        ?: Modifier
+                )
     ) { measurables, constraints ->
         val toolbarMeasurable = measurables[0]
         val fabMeasurable = measurables[1]
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
index 5226452..f11dc72 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TimePicker.kt
@@ -190,8 +190,7 @@
 import kotlinx.coroutines.launch
 
 /**
- * <a href="https://m3.material.io/components/time-pickers/overview" class="external"
- * target="_blank">Material Design time picker</a>.
+ * [Material Design time picker](https://m3.material.io/components/time-pickers/overview)
  *
  * Time pickers help users select and set a specific time.
  *
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
index a851fb5..7c54156 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntegrationIndentationFixTest.kt
@@ -17,7 +17,12 @@
 package androidx.compose.ui.text
 
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.text.font.toFontFamily
+import androidx.compose.ui.text.style.LineHeightStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.text.style.TextOverflow
@@ -30,6 +35,7 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -76,6 +82,49 @@
     }
 
     @Test
+    fun drawParagraphIndentsCorrectly_whenPaintedRepeatedly() {
+        val width1 = charWidth * 3
+        val subject =
+            Paragraph(
+                text = ltrChar.repeat(repeatCount),
+                style =
+                    TextStyle(
+                        textAlign = TextAlign.Center,
+                        letterSpacing = 1.sp,
+                        lineHeight = 24.sp,
+                        lineHeightStyle =
+                            LineHeightStyle(
+                                alignment = LineHeightStyle.Alignment.Center,
+                                trim = LineHeightStyle.Trim.None,
+                                mode = LineHeightStyle.Mode.Fixed
+                            ),
+                    ),
+                maxLines = lastLine + 1,
+                overflow = TextOverflow.Ellipsis,
+                constraints = Constraints(maxWidth = width1),
+                density = Density(density = 1f),
+                fontFamilyResolver = UncachedFontFamilyResolver(getInstrumentation().context)
+            )
+
+        val width = subject.width.ceilToInt()
+        val height = subject.height.ceilToInt()
+        val bitmap = ImageBitmap(width, height)
+        val canvas = Canvas(bitmap)
+        val paint = Paint()
+        paint.color = Color.Black
+        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+
+        subject.paint(canvas, Color.Blue)
+        val initialPixels = bitmap.dumpFirstCharPixels()
+        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+
+        subject.paint(canvas, Color.Blue)
+        val finalPixels = bitmap.dumpFirstCharPixels()
+
+        assertThat(initialPixels).isEqualTo(finalPixels)
+    }
+
+    @Test
     fun getLineLeftAndGetLineRight_Rtl() {
         val paragraph = paragraph(rtlChar.repeat(repeatCount))
         for (line in 0 until paragraph.lineCount) {
@@ -353,4 +402,10 @@
                 UncachedFontFamilyResolver(InstrumentationRegistry.getInstrumentation().context)
         )
     }
+
+    private fun ImageBitmap.dumpFirstCharPixels(): IntArray {
+        val out = IntArray(charWidth * charWidth)
+        readPixels(out, width = charWidth, height = charWidth)
+        return out
+    }
 }
diff --git a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
index ba2d38b..13a0595 100644
--- a/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
+++ b/compose/ui/ui-text/src/androidInstrumentedTest/kotlin/androidx/compose/ui/text/ParagraphIntrinsicsAsyncTypefaceTest.kt
@@ -113,7 +113,7 @@
             spanStyles,
             listOf(),
             fontFamilyResolver,
-            Density(1f)
+            Density(1f),
         )
     }
 }
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 49d12ab..f1880be 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
@@ -67,6 +67,7 @@
 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_CHARACTER
 import androidx.compose.ui.text.android.LayoutCompat.TEXT_GRANULARITY_WORD
 import androidx.compose.ui.text.android.TextLayout
+import androidx.compose.ui.text.android.hasSpan
 import androidx.compose.ui.text.android.selection.getWordEnd
 import androidx.compose.ui.text.android.selection.getWordStart
 import androidx.compose.ui.text.android.style.IndentationFixSpan
@@ -689,11 +690,13 @@
                 textAlign != TextAlign.Justify)
     }
 
-@OptIn(InternalPlatformTextApi::class)
+// this _will_ be called multiple times on the same ParagraphIntrinsics
 private fun CharSequence.attachIndentationFixSpan(): CharSequence {
     if (isEmpty()) return this
-    val spannable = if (this is Spannable) this else SpannableString(this)
-    spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
+    val spannable = this as? Spannable ?: SpannableString(this)
+    if (!spannable.hasSpan(IndentationFixSpan::class.java)) {
+        spannable.setSpan(IndentationFixSpan(), spannable.length - 1, spannable.length - 1)
+    }
     return spannable
 }
 
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt
new file mode 100644
index 0000000..6dc2a1c
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/FakeViewStructure.kt
@@ -0,0 +1,369 @@
+/*
+ * 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.benchmark.autofill
+
+import android.graphics.Matrix
+import android.graphics.Rect
+import android.os.Build
+import android.os.Bundle
+import android.os.LocaleList
+import android.os.Parcel
+import android.view.View
+import android.view.ViewStructure
+import android.view.autofill.AutofillId
+import android.view.autofill.AutofillValue
+import androidx.annotation.GuardedBy
+import androidx.annotation.RequiresApi
+
+/**
+ * A fake implementation of [ViewStructure] to use in tests.
+ *
+ * @param virtualId An ID that is unique for each viewStructure node in the viewStructure tree.
+ * @param packageName The package name of the app (Used as an autofill heuristic).
+ * @param typeName The type name of the view's identifier, or null if there is none.
+ * @param entryName The entry name of the view's identifier, or null if there is none.
+ * @param children A list of [ViewStructure]s that are children of the current [ViewStructure].
+ * @param bounds The bounds (Dimensions) of the component represented by this [ViewStructure].
+ * @param autofillId The [autofillId] for the parent component. The same autofillId is used for
+ *   other child components.
+ * @param autofillType The data type. Can be one of the following: [View.AUTOFILL_TYPE_DATE],
+ *   [View.AUTOFILL_TYPE_LIST], [View.AUTOFILL_TYPE_TEXT], [View.AUTOFILL_TYPE_TOGGLE] or
+ *   [View.AUTOFILL_TYPE_NONE].
+ * @param autofillHints The autofill hint. If this value not specified, we use heuristics to
+ *   determine what data to use while performing autofill.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+internal data class FakeViewStructure(
+    var virtualId: Int = 0,
+    var packageName: String? = null,
+    var typeName: String? = null,
+    var entryName: String? = null,
+    var children: MutableList<FakeViewStructure> = mutableListOf(),
+    var bounds: Rect? = null,
+    private val autofillId: AutofillId? =
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) generateAutofillId() else null,
+    internal var autofillType: Int = View.AUTOFILL_TYPE_NONE,
+    internal var autofillHints: Array<out String> = arrayOf()
+) : ViewStructure() {
+
+    private var activated: Boolean = false
+    private var alpha: Float = 1f
+    private var autofillOptions: Array<CharSequence>? = null
+    private var autofillValue: AutofillValue? = null
+    private var className: String? = null
+    private var contentDescription: CharSequence? = null
+    private var dataIsSensitive: Boolean = false
+    private var elevation: Float = 0f
+    private var extras: Bundle = Bundle()
+    private var hint: CharSequence? = null
+    private var htmlInfo: HtmlInfo? = null
+    private var inputType: Int = 0
+    private var isEnabled: Boolean = true
+    private var isAccessibilityFocused: Boolean = false
+    private var isCheckable: Boolean = false
+    private var isChecked: Boolean = false
+    private var isClickable: Boolean = true
+    private var isContextClickable: Boolean = false
+    private var isFocused: Boolean = false
+    private var isFocusable: Boolean = false
+    private var isLongClickable: Boolean = false
+    private var isOpaque: Boolean = false
+    private var selected: Boolean = false
+    private var text: CharSequence = ""
+    private var textLines: IntArray? = null
+    private var transformation: Matrix? = null
+    private var visibility: Int = View.VISIBLE
+    private var webDomain: String? = null
+
+    internal companion object {
+        @GuardedBy("this") private var previousId = 0
+        private val NO_SESSION = 0
+
+        // Android API level 26 introduced Autofill. Prior to API level 26, no autofill ID will be
+        // provided.
+        @RequiresApi(Build.VERSION_CODES.O)
+        @Synchronized
+        private fun generateAutofillId(): AutofillId {
+            var autofillId: AutofillId? = null
+            useParcel { parcel ->
+                parcel.writeInt(++previousId) // View Id.
+                parcel.writeInt(NO_SESSION) // Flag.
+                parcel.setDataPosition(0)
+                autofillId = AutofillId.CREATOR.createFromParcel(parcel)
+            }
+            return autofillId ?: error("Could not generate autofill id")
+        }
+    }
+
+    override fun getChildCount() = children.count()
+
+    override fun addChildCount(childCount: Int): Int {
+        repeat(childCount) { children.add(FakeViewStructure(autofillId = autofillId)) }
+        return children.count() - childCount
+    }
+
+    override fun newChild(index: Int): FakeViewStructure {
+        if (index >= children.count()) error("Call addChildCount() before calling newChild()")
+        return children[index]
+    }
+
+    override fun getAutofillId() = autofillId
+
+    override fun setAutofillId(rootId: AutofillId, virtualId: Int) {
+        this.virtualId = virtualId
+    }
+
+    override fun setId(
+        virtualId: Int,
+        packageName: String?,
+        typeName: String?,
+        entryName: String?
+    ) {
+        this.virtualId = virtualId
+        this.packageName = packageName
+        this.typeName = typeName
+        this.entryName = entryName
+    }
+
+    override fun setAutofillType(autofillType: Int) {
+        this.autofillType = autofillType
+    }
+
+    override fun setAutofillHints(autofillHints: Array<out String>?) {
+        autofillHints?.let { this.autofillHints = it }
+    }
+
+    override fun setDimens(left: Int, top: Int, x: Int, y: Int, width: Int, height: Int) {
+        this.bounds = Rect(left, top, width - left, height - top)
+    }
+
+    override fun equals(other: Any?) =
+        other is FakeViewStructure &&
+            other.virtualId == virtualId &&
+            other.packageName == packageName &&
+            other.typeName == typeName &&
+            other.entryName == entryName &&
+            other.autofillType == autofillType &&
+            other.autofillHints.contentEquals(autofillHints) &&
+            other.bounds.contentEquals(bounds) &&
+            other.activated == activated &&
+            other.alpha == alpha &&
+            other.autofillOptions.contentEquals(autofillOptions) &&
+            other.autofillValue == autofillValue &&
+            other.className == className &&
+            other.children.count() == children.count() &&
+            other.contentDescription == contentDescription &&
+            other.dataIsSensitive == dataIsSensitive &&
+            other.elevation == elevation &&
+            other.hint == hint &&
+            other.htmlInfo == htmlInfo &&
+            other.inputType == inputType &&
+            other.isEnabled == isEnabled &&
+            other.isCheckable == isCheckable &&
+            other.isChecked == isChecked &&
+            other.isClickable == isClickable &&
+            other.isContextClickable == isContextClickable &&
+            other.isAccessibilityFocused == isAccessibilityFocused &&
+            other.isFocused == isFocused &&
+            other.isLongClickable == isLongClickable &&
+            other.isOpaque == isOpaque &&
+            other.isFocusable == isFocusable &&
+            other.selected == selected &&
+            other.text == text &&
+            other.textLines.contentEquals(textLines) &&
+            other.transformation == transformation &&
+            other.visibility == visibility &&
+            other.webDomain == webDomain
+
+    override fun hashCode() = super.hashCode()
+
+    override fun getExtras() = extras
+
+    override fun getHint() = hint ?: ""
+
+    override fun getText() = text
+
+    override fun hasExtras() = !extras.isEmpty
+
+    override fun setActivated(p0: Boolean) {
+        activated = p0
+    }
+
+    override fun setAccessibilityFocused(p0: Boolean) {
+        isAccessibilityFocused = p0
+    }
+
+    override fun setAlpha(p0: Float) {
+        alpha = p0
+    }
+
+    override fun setAutofillOptions(p0: Array<CharSequence>?) {
+        autofillOptions = p0
+    }
+
+    override fun setAutofillValue(p0: AutofillValue?) {
+        autofillValue = p0
+    }
+
+    override fun setCheckable(p0: Boolean) {
+        isCheckable = p0
+    }
+
+    override fun setChecked(p0: Boolean) {
+        isChecked = p0
+    }
+
+    override fun setClassName(p0: String?) {
+        className = p0
+    }
+
+    override fun setClickable(p0: Boolean) {
+        isClickable = p0
+    }
+
+    override fun setContentDescription(p0: CharSequence?) {
+        contentDescription = p0
+    }
+
+    override fun setContextClickable(p0: Boolean) {
+        isContextClickable = p0
+    }
+
+    override fun setDataIsSensitive(p0: Boolean) {
+        dataIsSensitive = p0
+    }
+
+    override fun setElevation(p0: Float) {
+        elevation = p0
+    }
+
+    override fun setEnabled(p0: Boolean) {
+        isEnabled = p0
+    }
+
+    override fun setFocusable(p0: Boolean) {
+        isFocusable = p0
+    }
+
+    override fun setFocused(p0: Boolean) {
+        isFocused = p0
+    }
+
+    override fun setHtmlInfo(p0: HtmlInfo) {
+        htmlInfo = p0
+    }
+
+    override fun setHint(p0: CharSequence?) {
+        hint = p0
+    }
+
+    override fun setInputType(p0: Int) {
+        inputType = p0
+    }
+
+    override fun setLongClickable(p0: Boolean) {
+        isLongClickable = p0
+    }
+
+    override fun setOpaque(p0: Boolean) {
+        isOpaque = p0
+    }
+
+    override fun setSelected(p0: Boolean) {
+        selected = p0
+    }
+
+    override fun setText(p0: CharSequence?) {
+        p0?.let { text = it }
+    }
+
+    override fun setText(p0: CharSequence?, p1: Int, p2: Int) {
+        p0?.let { text = it.subSequence(p1, p2) }
+    }
+
+    override fun setTextLines(p0: IntArray?, p1: IntArray?) {
+        textLines = p0
+    }
+
+    override fun setTransformation(p0: Matrix?) {
+        transformation = p0
+    }
+
+    override fun setVisibility(p0: Int) {
+        visibility = p0
+    }
+
+    override fun setWebDomain(p0: String?) {
+        webDomain = p0
+    }
+
+    // Unimplemented methods.
+    override fun asyncCommit() {
+        TODO("not implemented")
+    }
+
+    override fun asyncNewChild(p0: Int): ViewStructure {
+        TODO("not implemented")
+    }
+
+    override fun getTextSelectionEnd(): Int {
+        TODO("not implemented")
+    }
+
+    override fun getTextSelectionStart(): Int {
+        TODO("not implemented")
+    }
+
+    override fun newHtmlInfoBuilder(p0: String): HtmlInfo.Builder {
+        TODO("not implemented")
+    }
+
+    override fun setAutofillId(p0: AutofillId) {
+        TODO("not implemented")
+    }
+
+    override fun setChildCount(p0: Int) {
+        TODO("not implemented")
+    }
+
+    override fun setLocaleList(p0: LocaleList?) {
+        TODO("not implemented")
+    }
+
+    override fun setTextStyle(p0: Float, p1: Int, p2: Int, p3: Int) {
+        TODO("not implemented")
+    }
+}
+
+private fun Rect?.contentEquals(other: Rect?) =
+    when {
+        (other == null && this == null) -> true
+        (other == null || this == null) -> false
+        else ->
+            other.left == left && other.right == right && other.bottom == bottom && other.top == top
+    }
+
+/** Obtains a parcel and then recycles it correctly whether an exception is thrown or not. */
+private fun useParcel(block: (Parcel) -> Unit) {
+    var parcel: Parcel? = null
+    try {
+        parcel = Parcel.obtain()
+        block(parcel)
+    } finally {
+        parcel?.recycle()
+    }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt
new file mode 100644
index 0000000..56e3231
--- /dev/null
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/PopulateAutofillStructBenchmark.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.benchmark.autofill
+
+import android.view.View
+import androidx.benchmark.ExperimentalBenchmarkConfigApi
+import androidx.benchmark.MicrobenchmarkConfig
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.ComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.doFramesUntilNoChangesPending
+import androidx.compose.ui.ComposeUiFlags.isSemanticAutofillEnabled
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalView
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@SdkSuppress(minSdkVersion = 26)
+@RunWith(Parameterized::class)
+class PopulateAutofillStructBenchmark(private val isAutofillEnabled: Boolean) {
+    @OptIn(ExperimentalComposeUiApi::class)
+    private val previousFlagValue = isSemanticAutofillEnabled
+    private lateinit var ownerView: View
+
+    @OptIn(ExperimentalBenchmarkConfigApi::class)
+    @get:Rule
+    val benchmarkRule = ComposeBenchmarkRule(MicrobenchmarkConfig(traceAppTagEnabled = true))
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "Autofill enabled = {0}")
+        fun data(): Collection<Array<Any>> {
+            val testCases = mutableListOf<Array<Any>>()
+            // Add a `false` parameter here and run locally to compare Autofill off vs on
+            for (isAutofillEnabled in listOf(true)) {
+                testCases.add(arrayOf(isAutofillEnabled))
+            }
+            return testCases
+        }
+    }
+
+    @Test
+    fun populateViewStructureBenchmark_textScreen() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        isSemanticAutofillEnabled = isAutofillEnabled
+        measurePopulateViewStructureRepeatedOnUiThread { AutofillTextScreen() }
+        @OptIn(ExperimentalComposeUiApi::class)
+        isSemanticAutofillEnabled = previousFlagValue
+    }
+
+    @Test
+    fun populateViewStructureBenchmark_autofillScreen() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        isSemanticAutofillEnabled = isAutofillEnabled
+        measurePopulateViewStructureRepeatedOnUiThread { AutofillScreen() }
+        @OptIn(ExperimentalComposeUiApi::class)
+        isSemanticAutofillEnabled = previousFlagValue
+    }
+
+    private fun measurePopulateViewStructureRepeatedOnUiThread(content: @Composable () -> Unit) {
+        benchmarkRule.runBenchmarkFor(
+            givenTestCase = {
+                object : ComposeTestCase {
+                    @Composable
+                    override fun Content() {
+                        ownerView = LocalView.current
+                        content()
+                    }
+                }
+            }
+        ) {
+            benchmarkRule.runOnUiThread { doFramesUntilNoChangesPending() }
+            benchmarkRule.measureRepeatedOnUiThread {
+                ownerView.onProvideAutofillVirtualStructure(FakeViewStructure(), 0)
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
index 898c1b2..3ec6174 100644
--- a/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
+++ b/compose/ui/ui/benchmark/src/androidTest/java/androidx/compose/ui/benchmark/autofill/SemanticAutofillBenchmarkUtils.kt
@@ -18,10 +18,18 @@
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Button
+import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
 import androidx.compose.material.TextField
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.autofill.ContentDataType
 import androidx.compose.ui.autofill.ContentType
@@ -29,6 +37,7 @@
 import androidx.compose.ui.semantics.contentType
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
 
 @Composable
 internal fun RemovableAutofillText(state: MutableState<Boolean>) {
@@ -180,6 +189,76 @@
     }
 }
 
+@Composable
+internal fun AutofillTextScreen() {
+    Column {
+        TextField(
+            value = data.firstName,
+            onValueChange = {},
+            label = { Text("Enter first name here: ") },
+            modifier =
+                Modifier.semantics {
+                    contentType = ContentType.PersonFirstName
+                    contentDataType = ContentDataType.Text
+                }
+        )
+        TextField(
+            value = data.lastName,
+            onValueChange = {},
+            label = { Text("Enter last name here: ") },
+            modifier =
+                Modifier.semantics {
+                    contentType = ContentType.PersonLastName
+                    contentDataType = ContentDataType.Text
+                }
+        )
+        TextField(
+            value = data.firstName,
+            onValueChange = {},
+            label = { Text("Enter first name here: ") },
+            modifier =
+                Modifier.semantics {
+                    contentType = ContentType.PersonFirstName
+                    contentDataType = ContentDataType.Text
+                }
+        )
+        TextField(
+            value = data.lastName,
+            onValueChange = {},
+            label = { Text("Enter last name here: ") },
+            modifier =
+                Modifier.semantics {
+                    contentType = ContentType.PersonLastName
+                    contentDataType = ContentDataType.Text
+                }
+        )
+    }
+}
+
+@Composable
+internal fun AutofillScreen() {
+    Scaffold(
+        content = { padding ->
+            Column(modifier = Modifier.fillMaxSize().padding(padding)) {
+                // Navigation Button backwards
+                Button(onClick = {}, modifier = Modifier.align(Alignment.Start)) {
+                    Spacer(modifier = Modifier.width(8.dp))
+                    Text("Go to route.")
+                }
+
+                // Navigation Button forwards
+                Button(onClick = {}, modifier = Modifier.align(Alignment.Start)) {
+                    Spacer(modifier = Modifier.width(8.dp))
+                    Text("Go to next screen")
+                }
+
+                Spacer(modifier = Modifier.height(16.dp))
+                AutofillTextScreen()
+            }
+        }
+    )
+}
+
 internal data class PersonData(
     var title: String = "",
     var firstName: String = "",
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt
new file mode 100644
index 0000000..e1be2f7
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/focus/FocusListenerTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2025 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.focus
+
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.requireSemanticsInfo
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FocusListenerTest {
+    @get:Rule val rule = createComposeRule()
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    private val previousFlagValue = ComposeUiFlags.isSemanticAutofillEnabled
+
+    // When we clear focus on Pre P devices, request focus is called even when we are
+    // in touch mode.
+    // https://developer.android.com/about/versions/pie/android-9.0-changes-28#focus
+    private val initialFocusAfterClearFocus = SDK_INT < Build.VERSION_CODES.P
+
+    @Before
+    fun enableAutofill() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isSemanticAutofillEnabled = true
+    }
+
+    @After
+    fun disableAutofill() {
+        @OptIn(ExperimentalComposeUiApi::class)
+        ComposeUiFlags.isSemanticAutofillEnabled = previousFlagValue
+    }
+
+    @Test
+    fun nothingFocused() {
+        // Arrange.
+        val listener = TestFocusListener()
+        rule.setContent(listener) { Box(Modifier.size(10.dp).focusable()) }
+
+        // Assert.
+        rule.runOnIdle { assertThat(listener.events).isEmpty() }
+    }
+
+    @Test
+    fun firstItemFocused() {
+        // Arrange.
+        val listener = TestFocusListener()
+        rule.setContent(listener) { Box(Modifier.size(10.dp).testTag("item").focusable()) }
+        val itemId = rule.onNodeWithTag("item").semanticsId()
+
+        // Act.
+        rule.onNodeWithTag("item").requestFocus()
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(listener).isEqualTo(TestFocusListener(mutableListOf(Pair(null, itemId))))
+        }
+    }
+
+    @Test
+    fun firstItemUnFocused() {
+        // Arrange.
+        val listener = TestFocusListener()
+        lateinit var focusManager: FocusManager
+        rule.setContent(listener) {
+            focusManager = LocalFocusManager.current
+            Box(Modifier.size(10.dp).testTag("item").focusable())
+        }
+        val itemId = rule.onNodeWithTag("item").semanticsId()
+        rule.onNodeWithTag("item").requestFocus()
+        rule.runOnIdle { listener.reset() }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(listener)
+                .isEqualTo(
+                    TestFocusListener(
+                        if (initialFocusAfterClearFocus) {
+                            mutableListOf(Pair(itemId, null), Pair(null, itemId))
+                        } else {
+                            mutableListOf(Pair(itemId, null))
+                        }
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun secondItemFocused() {
+        // Arrange.
+        val listener = TestFocusListener()
+        rule.setContent(listener) {
+            Column {
+                Box(Modifier.size(10.dp).testTag("item1").focusable())
+                Box(Modifier.size(10.dp).testTag("item2").focusable())
+            }
+        }
+        val item1Id = rule.onNodeWithTag("item1").semanticsId()
+        val item2Id = rule.onNodeWithTag("item2").semanticsId()
+        rule.onNodeWithTag("item1").requestFocus()
+        rule.runOnIdle { listener.reset() }
+
+        // Act.
+        rule.onNodeWithTag("item2").requestFocus()
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(listener)
+                .isEqualTo(
+                    TestFocusListener(mutableListOf(Pair(item1Id, null), Pair(null, item2Id)))
+                )
+        }
+    }
+
+    @Test
+    fun secondItemUnFocused() {
+        // Arrange.
+        val listener = TestFocusListener()
+        lateinit var focusManager: FocusManager
+        rule.setContent(listener) {
+            focusManager = LocalFocusManager.current
+            Column {
+                Box(Modifier.size(10.dp).testTag("item1").focusable())
+                Box(Modifier.size(10.dp).testTag("item2").focusable())
+            }
+        }
+        val item1Id = rule.onNodeWithTag("item1").semanticsId()
+        val item2Id = rule.onNodeWithTag("item2").semanticsId()
+        rule.onNodeWithTag("item1").requestFocus()
+        rule.onNodeWithTag("item2").requestFocus()
+        rule.runOnIdle { listener.reset() }
+
+        // Act.
+        rule.runOnIdle { focusManager.clearFocus() }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(listener)
+                .isEqualTo(
+                    TestFocusListener(
+                        if (initialFocusAfterClearFocus) {
+                            mutableListOf(Pair(item2Id, null), Pair(null, item1Id))
+                        } else {
+                            mutableListOf(Pair(item2Id, null))
+                        }
+                    )
+                )
+        }
+    }
+
+    private data class TestFocusListener(
+        val events: MutableList<Pair<Int?, Int?>> = mutableListOf<Pair<Int?, Int?>>()
+    ) : FocusListener {
+        override fun onFocusChanged(
+            previous: FocusTargetModifierNode?,
+            current: FocusTargetModifierNode?
+        ) {
+            events +=
+                Pair(
+                    previous?.requireSemanticsInfo()?.semanticsId,
+                    current?.requireSemanticsInfo()?.semanticsId
+                )
+        }
+
+        fun reset() {
+            events.clear()
+        }
+    }
+
+    private fun ComposeContentTestRule.setContent(
+        focusListener: FocusListener,
+        content: @Composable (() -> Unit)
+    ) {
+        setContent {
+            val focusOwner = LocalFocusManager.current as FocusOwner
+            DisposableEffect(focusOwner, focusListener) {
+                focusOwner.listeners += focusListener
+                onDispose { focusOwner.listeners -= focusListener }
+            }
+            content()
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
index f6a524e..69f31cc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/autofill/AndroidAutofillManager.android.kt
@@ -27,7 +27,12 @@
 import androidx.annotation.RequiresApi
 import androidx.collection.MutableIntSet
 import androidx.collection.mutableObjectListOf
+import androidx.compose.ui.ComposeUiFlags
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.focus.FocusListener
+import androidx.compose.ui.focus.FocusTargetModifierNode
 import androidx.compose.ui.internal.checkPreconditionNotNull
+import androidx.compose.ui.node.requireSemanticsInfo
 import androidx.compose.ui.platform.coreshims.ViewCompatShims
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsConfiguration
@@ -54,7 +59,7 @@
     private val view: View,
     private val rectManager: RectManager,
     private val packageName: String,
-) : AutofillManager(), SemanticsListener {
+) : AutofillManager(), SemanticsListener, FocusListener {
     private var reusableRect = Rect()
     private var rootAutofillId: AutofillId
 
@@ -85,6 +90,26 @@
         }
     }
 
+    override fun onFocusChanged(
+        previous: FocusTargetModifierNode?,
+        current: FocusTargetModifierNode?
+    ) {
+        previous?.requireSemanticsInfo()?.let {
+            if (it.semanticsConfiguration?.isAutofillable() == true) {
+                platformAutofillManager.notifyViewExited(view, it.semanticsId)
+            }
+        }
+        current?.requireSemanticsInfo()?.let {
+            if (it.semanticsConfiguration?.isAutofillable() == true) {
+                val semanticsId = it.semanticsId
+                rectManager.rects.withRect(semanticsId) { l, t, r, b ->
+                    platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
+                }
+                previouslyFocusedId = semanticsId
+            }
+        }
+    }
+
     /** Send events to the autofill service in response to semantics changes. */
     override fun onSemanticsChanged(
         semanticsInfo: SemanticsInfo,
@@ -109,22 +134,18 @@
         }
 
         // Check Focus.
-        // TODO: Instead of saving the focused item here, add some internal API to focusManager
-        //  so that this could be more efficient.
-        val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
-        val currFocus = config?.getOrNull(SemanticsProperties.Focused)
-        if (previousFocus != true && currFocus == true && config.isAutofillable()) {
-            previouslyFocusedId = semanticsId
-            rectManager.rects.withRect(semanticsId) { left, top, right, bottom ->
-                platformAutofillManager.notifyViewEntered(
-                    view,
-                    semanticsId,
-                    Rect(left, top, right, bottom)
-                )
+        if (@OptIn(ExperimentalComposeUiApi::class) !ComposeUiFlags.isTrackFocusEnabled) {
+            val previousFocus = prevConfig?.getOrNull(SemanticsProperties.Focused)
+            val currFocus = config?.getOrNull(SemanticsProperties.Focused)
+            if (previousFocus != true && currFocus == true && config.isAutofillable()) {
+                previouslyFocusedId = semanticsId
+                rectManager.rects.withRect(semanticsId) { l, t, r, b ->
+                    platformAutofillManager.notifyViewEntered(view, semanticsId, Rect(l, t, r, b))
+                }
             }
-        }
-        if (previousFocus == true && currFocus != true && prevConfig.isAutofillable()) {
-            platformAutofillManager.notifyViewExited(view, semanticsId)
+            if (previousFocus == true && currFocus != true && prevConfig.isAutofillable()) {
+                platformAutofillManager.notifyViewExited(view, semanticsId)
+            }
         }
 
         // Update currentlyDisplayedIDs if relevance to Autocommit has changed.
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 15f5213..c059f5d 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
@@ -2037,7 +2037,10 @@
         viewTreeObserver.addOnTouchModeChangeListener(touchModeChangeListener)
 
         if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.setViewTranslationCallback(this)
-        if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners += it }
+        _autofillManager?.let {
+            focusOwner.listeners += it
+            semanticsOwner.listeners += it
+        }
     }
 
     override fun onDetachedFromWindow() {
@@ -2062,7 +2065,10 @@
         viewTreeObserver.removeOnTouchModeChangeListener(touchModeChangeListener)
 
         if (SDK_INT >= S) AndroidComposeViewTranslationCallbackS.clearViewTranslationCallback(this)
-        if (autofillSupported()) _autofillManager?.let { semanticsOwner.listeners -= it }
+        _autofillManager?.let {
+            semanticsOwner.listeners -= it
+            focusOwner.listeners -= it
+        }
     }
 
     override fun onProvideAutofillVirtualStructure(structure: ViewStructure?, flags: Int) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt
new file mode 100644
index 0000000..139f0db
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusListener.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 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.focus
+
+/** Listener that can be used to observe focus changes. */
+internal interface FocusListener {
+    /**
+     * Called when the focus changes.
+     *
+     * @param previous The previous focus target node.
+     * @param current The new focus target node.
+     */
+    fun onFocusChanged(previous: FocusTargetModifierNode?, current: FocusTargetModifierNode?)
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index ae474794..ae030e0 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.focus
 
+import androidx.collection.MutableObjectList
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.input.key.KeyEvent
@@ -147,6 +148,9 @@
     /** Schedule the owner to be invalidated after onApplyChanges. */
     fun scheduleInvalidationForOwner()
 
+    /** Listeners that will be notified when the active item changes. */
+    val listeners: MutableObjectList<FocusListener>
+
     /** The focus state of the root focus node. */
     val rootState: FocusState
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 07a8cdf..01e0582 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.focus
 
 import androidx.collection.MutableLongSet
+import androidx.collection.MutableObjectList
 import androidx.compose.ui.ComposeUiFlags
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -465,15 +466,19 @@
     override val rootState: FocusState
         get() = rootFocusNode.focusState
 
+    override val listeners: MutableObjectList<FocusListener> = MutableObjectList(1)
+
     override var activeFocusTargetNode: FocusTargetNode? = null
         set(value) {
             val previousValue = field
             field = value
             if (value == null || previousValue !== value) isFocusCaptured = false
+            if (@OptIn(ExperimentalComposeUiApi::class) ComposeUiFlags.isSemanticAutofillEnabled) {
+                listeners.forEach { it.onFocusChanged(previousValue, value) }
+            }
         }
 
     override var isFocusCaptured: Boolean = false
-        get() = field
         set(value) {
             requirePrecondition(!value || activeFocusTargetNode != null) {
                 "Cannot capture focus when the active focus target node is unset"
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index d0ada4b..08505aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.internal.checkPrecondition
 import androidx.compose.ui.internal.checkPreconditionNotNull
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.semantics.SemanticsInfo
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 
@@ -325,6 +326,8 @@
         }
         .layoutNode
 
+internal fun DelegatableNode.requireSemanticsInfo(): SemanticsInfo = requireLayoutNode()
+
 internal fun DelegatableNode.requireOwner(): Owner =
     checkPreconditionNotNull(requireLayoutNode().owner) { "This node does not have an owner." }
 
diff --git a/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt
new file mode 100644
index 0000000..3026d4b
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/1.3.0-beta01.txt
@@ -0,0 +1,101 @@
+// Signature format: 4.0
+package androidx.coordinatorlayout.widget {
+
+  public class CoordinatorLayout extends android.view.ViewGroup implements androidx.core.view.NestedScrollingParent2 androidx.core.view.NestedScrollingParent3 {
+    ctor public CoordinatorLayout(android.content.Context);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?, @AttrRes int);
+    method public void dispatchDependentViewsChanged(android.view.View);
+    method public boolean doViewsOverlap(android.view.View, android.view.View);
+    method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateDefaultLayoutParams();
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+    method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
+    method public java.util.List<android.view.View!> getDependencies(android.view.View);
+    method public java.util.List<android.view.View!> getDependents(android.view.View);
+    method public android.graphics.drawable.Drawable? getStatusBarBackground();
+    method public boolean isPointInChildBounds(android.view.View, int, int);
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void onDraw(android.graphics.Canvas);
+    method public void onLayoutChild(android.view.View, int);
+    method public void onMeasureChild(android.view.View, int, int, int, int);
+    method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+    method public void onNestedScroll(android.view.View, int, int, int, int, int);
+    method public void onNestedScroll(android.view.View, int, int, int, int, int, int[]);
+    method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+    method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+    method public void onStopNestedScroll(android.view.View, int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+    method public void setStatusBarBackgroundColor(@ColorInt int);
+    method public void setStatusBarBackgroundResource(@DrawableRes int);
+  }
+
+  public static interface CoordinatorLayout.AttachedBehavior {
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior getBehavior();
+  }
+
+  public abstract static class CoordinatorLayout.Behavior<V extends android.view.View> {
+    ctor public CoordinatorLayout.Behavior();
+    ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet?);
+    method public boolean blocksInteractionBelow(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method public boolean getInsetDodgeRect(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect);
+    method @ColorInt public int getScrimColor(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method @FloatRange(from=0, to=1) public float getScrimOpacity(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method public static Object? getTag(android.view.View);
+    method public boolean layoutDependsOn(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public androidx.core.view.WindowInsetsCompat onApplyWindowInsets(androidx.coordinatorlayout.widget.CoordinatorLayout, V, androidx.core.view.WindowInsetsCompat);
+    method public void onAttachedToLayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+    method public boolean onDependentViewChanged(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDependentViewRemoved(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDetachedFromLayoutParams();
+    method public boolean onInterceptTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public boolean onLayoutChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int);
+    method public boolean onMeasureChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int, int, int, int);
+    method public boolean onNestedFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+    method public boolean onNestedPreFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float);
+    method @Deprecated public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+    method public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[], int);
+    method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+    method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int);
+    method public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, int, int[]);
+    method @Deprecated public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+    method public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+    method public boolean onRequestChildRectangleOnScreen(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+    method public void onRestoreInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.os.Parcelable);
+    method public android.os.Parcelable? onSaveInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method @Deprecated public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int);
+    method public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, int, int);
+    method @Deprecated public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int);
+    method public boolean onTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public static void setTag(android.view.View, Object?);
+  }
+
+  @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface CoordinatorLayout.DefaultBehavior {
+    method @Deprecated public abstract Class<? extends androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior!> value();
+  }
+
+  public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(int, int);
+    method @IdRes public int getAnchorId();
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior? getBehavior();
+    method public void setAnchorId(@IdRes int);
+    method public void setBehavior(androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior?);
+    field public int anchorGravity;
+    field public int dodgeInsetEdges;
+    field public int gravity;
+    field public int insetEdge;
+    field public int keyline;
+  }
+
+  protected static class CoordinatorLayout.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public CoordinatorLayout.SavedState(android.os.Parcel!, ClassLoader!);
+    ctor public CoordinatorLayout.SavedState(android.os.Parcelable!);
+    field public static final android.os.Parcelable.Creator<androidx.coordinatorlayout.widget.CoordinatorLayout.SavedState!>! CREATOR;
+  }
+
+}
+
diff --git a/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt
new file mode 100644
index 0000000..6af58e16
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/res-1.3.0-beta01.txt
@@ -0,0 +1,9 @@
+attr keylines
+attr layout_anchor
+attr layout_anchorGravity
+attr layout_behavior
+attr layout_dodgeInsetEdges
+attr layout_insetEdge
+attr layout_keyline
+attr statusBarBackground
+style Widget_Support_CoordinatorLayout
diff --git a/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt b/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt
new file mode 100644
index 0000000..22ecfc8
--- /dev/null
+++ b/coordinatorlayout/coordinatorlayout/api/restricted_1.3.0-beta01.txt
@@ -0,0 +1,105 @@
+// Signature format: 4.0
+package androidx.coordinatorlayout.widget {
+
+  public class CoordinatorLayout extends android.view.ViewGroup implements androidx.core.view.NestedScrollingParent2 androidx.core.view.NestedScrollingParent3 {
+    ctor public CoordinatorLayout(android.content.Context);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?);
+    ctor public CoordinatorLayout(android.content.Context, android.util.AttributeSet?, @AttrRes int);
+    method public void dispatchDependentViewsChanged(android.view.View);
+    method public boolean doViewsOverlap(android.view.View, android.view.View);
+    method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateDefaultLayoutParams();
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.util.AttributeSet!);
+    method protected androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams! generateLayoutParams(android.view.ViewGroup.LayoutParams!);
+    method public java.util.List<android.view.View!> getDependencies(android.view.View);
+    method public java.util.List<android.view.View!> getDependents(android.view.View);
+    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final androidx.core.view.WindowInsetsCompat? getLastWindowInsets();
+    method public android.graphics.drawable.Drawable? getStatusBarBackground();
+    method public boolean isPointInChildBounds(android.view.View, int, int);
+    method public void onAttachedToWindow();
+    method public void onDetachedFromWindow();
+    method public void onDraw(android.graphics.Canvas);
+    method public void onLayoutChild(android.view.View, int);
+    method public void onMeasureChild(android.view.View, int, int, int, int);
+    method public void onNestedPreScroll(android.view.View, int, int, int[], int);
+    method public void onNestedScroll(android.view.View, int, int, int, int, int);
+    method public void onNestedScroll(android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int, int[]);
+    method public void onNestedScrollAccepted(android.view.View, android.view.View, int, int);
+    method public boolean onStartNestedScroll(android.view.View, android.view.View, int, int);
+    method public void onStopNestedScroll(android.view.View, int);
+    method public void setStatusBarBackground(android.graphics.drawable.Drawable?);
+    method public void setStatusBarBackgroundColor(@ColorInt int);
+    method public void setStatusBarBackgroundResource(@DrawableRes int);
+  }
+
+  public static interface CoordinatorLayout.AttachedBehavior {
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior getBehavior();
+  }
+
+  public abstract static class CoordinatorLayout.Behavior<V extends android.view.View> {
+    ctor public CoordinatorLayout.Behavior();
+    ctor public CoordinatorLayout.Behavior(android.content.Context, android.util.AttributeSet?);
+    method public boolean blocksInteractionBelow(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method public boolean getInsetDodgeRect(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect);
+    method @ColorInt public int getScrimColor(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method @FloatRange(from=0, to=1) public float getScrimOpacity(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method public static Object? getTag(android.view.View);
+    method public boolean layoutDependsOn(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public androidx.core.view.WindowInsetsCompat onApplyWindowInsets(androidx.coordinatorlayout.widget.CoordinatorLayout, V, androidx.core.view.WindowInsetsCompat);
+    method public void onAttachedToLayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+    method public boolean onDependentViewChanged(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDependentViewRemoved(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onDetachedFromLayoutParams();
+    method public boolean onInterceptTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public boolean onLayoutChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int);
+    method public boolean onMeasureChild(androidx.coordinatorlayout.widget.CoordinatorLayout, V, int, int, int, int);
+    method public boolean onNestedFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float, boolean);
+    method public boolean onNestedPreFling(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, float, float);
+    method @Deprecated public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[]);
+    method public void onNestedPreScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int[], @androidx.core.view.ViewCompat.NestedScrollType int);
+    method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int);
+    method @Deprecated public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int);
+    method public void onNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, int, int, int, int, @androidx.core.view.ViewCompat.NestedScrollType int, int[]);
+    method @Deprecated public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int);
+    method public void onNestedScrollAccepted(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int, @androidx.core.view.ViewCompat.NestedScrollType int);
+    method public boolean onRequestChildRectangleOnScreen(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.graphics.Rect, boolean);
+    method public void onRestoreInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.os.Parcelable);
+    method public android.os.Parcelable? onSaveInstanceState(androidx.coordinatorlayout.widget.CoordinatorLayout, V);
+    method @Deprecated public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int);
+    method public boolean onStartNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, android.view.View, @androidx.core.view.ViewCompat.ScrollAxis int, @androidx.core.view.ViewCompat.NestedScrollType int);
+    method @Deprecated public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View);
+    method public void onStopNestedScroll(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.View, @androidx.core.view.ViewCompat.NestedScrollType int);
+    method public boolean onTouchEvent(androidx.coordinatorlayout.widget.CoordinatorLayout, V, android.view.MotionEvent);
+    method public static void setTag(android.view.View, Object?);
+  }
+
+  @Deprecated @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) public static @interface CoordinatorLayout.DefaultBehavior {
+    method @Deprecated public abstract Class<? extends androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior!> value();
+  }
+
+  @IntDef({0x0, 0x1, 0x2}) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) public static @interface CoordinatorLayout.DispatchChangeEvent {
+  }
+
+  public static class CoordinatorLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(android.view.ViewGroup.MarginLayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(androidx.coordinatorlayout.widget.CoordinatorLayout.LayoutParams);
+    ctor public CoordinatorLayout.LayoutParams(int, int);
+    method @IdRes public int getAnchorId();
+    method public androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior? getBehavior();
+    method public void setAnchorId(@IdRes int);
+    method public void setBehavior(androidx.coordinatorlayout.widget.CoordinatorLayout.Behavior?);
+    field public int anchorGravity;
+    field public int dodgeInsetEdges;
+    field public int gravity;
+    field public int insetEdge;
+    field public int keyline;
+  }
+
+  protected static class CoordinatorLayout.SavedState extends androidx.customview.view.AbsSavedState {
+    ctor public CoordinatorLayout.SavedState(android.os.Parcel!, ClassLoader!);
+    ctor public CoordinatorLayout.SavedState(android.os.Parcelable!);
+    field public static final android.os.Parcelable.Creator<androidx.coordinatorlayout.widget.CoordinatorLayout.SavedState!>! CREATOR;
+  }
+
+}
+
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
index cf59415..e9af1a6 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/platform/aggregate/ResultGroupByPeriodAggregatorTest.kt
@@ -395,7 +395,7 @@
     }
 
     @Test
-    fun getResult_recordNeededForCalculation_returnsMapWithResult() {
+    fun getResult_recordContributingToAggregation_returnsListWithResult() {
         val aggregator =
             ResultGroupedByPeriodAggregator(
                 createLocalTimeRange(
@@ -423,7 +423,34 @@
     }
 
     @Test
-    fun getResult_recordOutOfBounds_returnsEmptyMap() {
+    fun getResult_recordNotContributingToAggregation_returnsEmptyList() {
+        val aggregator =
+            ResultGroupedByPeriodAggregator(
+                createLocalTimeRange(
+                    TimeRangeFilter.after(
+                        Instant.ofEpochMilli(100).toLocalTimeWithDefaultZoneFallback(ZoneOffset.UTC)
+                    )
+                ),
+                bucketPeriod = Period.ofDays(1)
+            ) {
+                TransFatTotalAggregationProcessor(it)
+            }
+
+        aggregator.filterAndAggregate(
+            NutritionRecord(
+                startTime = Instant.ofEpochMilli(100),
+                endTime = Instant.ofEpochMilli(1000),
+                startZoneOffset = ZoneOffset.UTC,
+                endZoneOffset = ZoneOffset.UTC,
+                metadata = Metadata(dataOrigin = DataOrigin("some.package")),
+            )
+        )
+
+        assertThat(aggregator.getResult()).isEmpty()
+    }
+
+    @Test
+    fun getResult_recordOutOfBounds_returnsEmptyList() {
         val aggregator =
             ResultGroupedByPeriodAggregator(
                 LocalTimeRange(
diff --git a/libraryversions.toml b/libraryversions.toml
index 8988755..11546e0 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -31,7 +31,7 @@
 CONSTRAINTLAYOUT_COMPOSE = "1.2.0-alpha01"
 CONSTRAINTLAYOUT_CORE = "1.2.0-alpha01"
 CONTENTPAGER = "1.1.0-alpha01"
-COORDINATORLAYOUT = "1.3.0-alpha03"
+COORDINATORLAYOUT = "1.3.0-beta01"
 CORE = "1.16.0-alpha02"
 CORE_ANIMATION = "1.0.0"
 CORE_ANIMATION_TESTING = "1.0.0"
diff --git a/navigation3/navigation3/api/current.txt b/navigation3/navigation3/api/current.txt
index bbb7d86..ec7e433 100644
--- a/navigation3/navigation3/api/current.txt
+++ b/navigation3/navigation3/api/current.txt
@@ -1,16 +1,6 @@
 // Signature format: 4.0
 package androidx.navigation3 {
 
-  public final class AnimatedNavDisplay {
-    method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
-    method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
-    field public static final androidx.navigation3.AnimatedNavDisplay INSTANCE;
-  }
-
-  public final class AnimatedNavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void AnimatedNavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
-  }
-
   public interface NavContentWrapper {
     method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
     method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
@@ -18,11 +8,12 @@
 
   public final class NavDisplay {
     method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
+    method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
     field public static final androidx.navigation3.NavDisplay INSTANCE;
   }
 
   public final class NavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
   }
 
   public final class NavRecord<T> {
@@ -36,7 +27,8 @@
   }
 
   public final class NavWrapperManager {
-    ctor public NavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+    ctor public NavWrapperManager();
+    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
     method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
     method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
   }
diff --git a/navigation3/navigation3/api/restricted_current.txt b/navigation3/navigation3/api/restricted_current.txt
index bbb7d86..ec7e433 100644
--- a/navigation3/navigation3/api/restricted_current.txt
+++ b/navigation3/navigation3/api/restricted_current.txt
@@ -1,16 +1,6 @@
 // Signature format: 4.0
 package androidx.navigation3 {
 
-  public final class AnimatedNavDisplay {
-    method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
-    method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
-    field public static final androidx.navigation3.AnimatedNavDisplay INSTANCE;
-  }
-
-  public final class AnimatedNavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void AnimatedNavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
-  }
-
   public interface NavContentWrapper {
     method @androidx.compose.runtime.Composable public default void WrapBackStack(java.util.List<?> backStack);
     method @androidx.compose.runtime.Composable public <T> void WrapContent(androidx.navigation3.NavRecord<T> record);
@@ -18,11 +8,12 @@
 
   public final class NavDisplay {
     method public java.util.Map<java.lang.String,java.lang.Object> isDialog(boolean boolean);
+    method public java.util.Map<java.lang.String,java.lang.Object> transition(androidx.compose.animation.EnterTransition? enter, androidx.compose.animation.ExitTransition? exit);
     field public static final androidx.navigation3.NavDisplay INSTANCE;
   }
 
   public final class NavDisplay_androidKt {
-    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
+    method @androidx.compose.runtime.Composable public static <T> void NavDisplay(java.util.List<? extends T> backstack, optional androidx.compose.ui.Modifier modifier, optional androidx.navigation3.NavWrapperManager wrapperManager, optional androidx.compose.ui.Alignment contentAlignment, optional androidx.compose.animation.SizeTransform? sizeTransform, optional androidx.compose.animation.EnterTransition enterTransition, optional androidx.compose.animation.ExitTransition exitTransition, optional kotlin.jvm.functions.Function0<kotlin.Unit> onBack, kotlin.jvm.functions.Function1<? super T,? extends androidx.navigation3.NavRecord<? extends T>> recordProvider);
   }
 
   public final class NavRecord<T> {
@@ -36,7 +27,8 @@
   }
 
   public final class NavWrapperManager {
-    ctor public NavWrapperManager(java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
+    ctor public NavWrapperManager();
+    ctor public NavWrapperManager(optional java.util.List<? extends androidx.navigation3.NavContentWrapper> navContentWrappers);
     method @androidx.compose.runtime.Composable public <T> void ContentForRecord(androidx.navigation3.NavRecord<T> record);
     method @androidx.compose.runtime.Composable public void PrepareBackStack(java.util.List<?> backStack);
   }
diff --git a/navigation3/navigation3/build.gradle b/navigation3/navigation3/build.gradle
index 796ed34..74aa38e 100644
--- a/navigation3/navigation3/build.gradle
+++ b/navigation3/navigation3/build.gradle
@@ -67,11 +67,10 @@
         androidMain {
             dependsOn(jvmMain)
             dependencies {
+                api("androidx.compose.animation:animation:1.7.5")
+                api("androidx.compose.ui:ui:1.7.5")
                 implementation("androidx.activity:activity-compose:1.9.3")
                 implementation("androidx.annotation:annotation:1.8.0")
-                implementation("androidx.compose.animation:animation:1.7.5")
-                implementation("androidx.compose.foundation:foundation:1.7.5")
-                implementation("androidx.compose.ui:ui:1.7.5")
             }
         }
 
diff --git a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
index bc706ddb..fa6ff47 100644
--- a/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
+++ b/navigation3/navigation3/samples/src/main/kotlin/androidx/navigation3/samples/NavDisplaySamples.kt
@@ -24,7 +24,6 @@
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewmodel.compose.viewModel
 import androidx.lifecycle.viewmodel.navigation3.ViewModelStoreNavContentWrapper
-import androidx.navigation3.AnimatedNavDisplay
 import androidx.navigation3.NavDisplay
 import androidx.navigation3.NavRecord
 import androidx.navigation3.SavedStateNavContentWrapper
@@ -32,9 +31,13 @@
 import androidx.navigation3.recordProvider
 import androidx.navigation3.rememberNavWrapperManager
 
+class ProfileViewModel : ViewModel() {
+    val name = "no user"
+}
+
 @Sampled
 @Composable
-fun BasicNav() {
+fun BaseNav() {
     val backStack = rememberMutableStateListOf(Profile)
     val manager =
         rememberNavWrapperManager(
@@ -46,54 +49,14 @@
         onBack = { backStack.removeLast() },
         recordProvider =
             recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
-                record<Profile> {
-                    val viewModel = viewModel<ProfileViewModel>()
-                    Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
-                }
-                record<Scrollable> { Scrollable({ backStack.add(it) }) { backStack.removeLast() } }
-                record<Dialog>(featureMap = NavDisplay.isDialog(true)) {
-                    DialogContent { backStack.removeLast() }
-                }
-                record<Dashboard> { dashboardArgs ->
-                    val userId = dashboardArgs.userId
-                    Dashboard(userId, onBack = { backStack.removeLast() })
-                }
-            }
-    )
-}
-
-class ProfileViewModel : ViewModel() {
-    val name = "no user"
-}
-
-@Sampled
-@Composable
-fun AnimatedNav() {
-    val backStack = rememberMutableStateListOf(Profile)
-    val manager =
-        rememberNavWrapperManager(
-            listOf(SavedStateNavContentWrapper, ViewModelStoreNavContentWrapper)
-        )
-    AnimatedNavDisplay(
-        backstack = backStack,
-        wrapperManager = manager,
-        onBack = { backStack.removeLast() },
-        recordProvider =
-            recordProvider({ NavRecord(Unit) { Text(text = "Invalid Key") } }) {
                 record<Profile>(
-                    AnimatedNavDisplay.transition(
-                        slideInHorizontally { it },
-                        slideOutHorizontally { it }
-                    )
+                    NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) {
                     val viewModel = viewModel<ProfileViewModel>()
                     Profile(viewModel, { backStack.add(it) }) { backStack.removeLast() }
                 }
                 record<Scrollable>(
-                    AnimatedNavDisplay.transition(
-                        slideInHorizontally { it },
-                        slideOutHorizontally { it }
-                    )
+                    NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) {
                     Scrollable({ backStack.add(it) }) { backStack.removeLast() }
                 }
@@ -101,10 +64,7 @@
                     DialogContent { backStack.removeLast() }
                 }
                 record<Dashboard>(
-                    AnimatedNavDisplay.transition(
-                        slideInHorizontally { it },
-                        slideOutHorizontally { it }
-                    )
+                    NavDisplay.transition(slideInHorizontally { it }, slideOutHorizontally { it })
                 ) { dashboardArgs ->
                     val userId = dashboardArgs.userId
                     Dashboard(userId, onBack = { backStack.removeLast() })
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
similarity index 91%
rename from navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt
rename to navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
index 1ff756b..d7da926 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedNavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/AnimatedTest.kt
@@ -27,7 +27,7 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithText
 import androidx.kruth.assertThat
-import androidx.navigation3.AnimatedNavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
+import androidx.navigation3.NavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import kotlin.test.Test
@@ -36,7 +36,7 @@
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
-class AnimatedNavDisplayTest {
+class AnimatedTest {
     @get:Rule val composeTestRule = createComposeRule()
 
     @Test
@@ -47,8 +47,7 @@
 
         composeTestRule.setContent {
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            AnimatedNavDisplay(backstack, wrapperManager = manager) {
+            NavDisplay(backstack) {
                 when (it) {
                     first -> NavRecord(first) { Text(first) }
                     second -> NavRecord(second) { Text(second) }
@@ -91,8 +90,7 @@
 
         composeTestRule.setContent {
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            AnimatedNavDisplay(backstack, wrapperManager = manager) {
+            NavDisplay(backstack) {
                 when (it) {
                     first ->
                         NavRecord(
@@ -104,7 +102,7 @@
                         NavRecord(
                             second,
                             featureMap =
-                                AnimatedNavDisplay.transition(
+                                NavDisplay.transition(
                                     enter = fadeIn(tween(customDuration)),
                                     exit = fadeOut(tween(customDuration))
                                 )
diff --git a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
index a36372a8..fd5749a 100644
--- a/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
+++ b/navigation3/navigation3/src/androidInstrumentedTest/kotlin/androidx/navigation3/NavDisplayTest.kt
@@ -45,10 +45,7 @@
     @Test
     fun testContentShown() {
         composeTestRule.setContent {
-            val manager = rememberNavWrapperManager(emptyList())
-            NavDisplay(backstack = mutableStateListOf(first), wrapperManager = manager) {
-                NavRecord(first) { Text(first) }
-            }
+            NavDisplay(backstack = mutableStateListOf(first)) { NavRecord(first) { Text(first) } }
         }
 
         assertThat(composeTestRule.onNodeWithText(first).isDisplayed()).isTrue()
@@ -59,8 +56,7 @@
         lateinit var backstack: MutableList<Any>
         composeTestRule.setContent {
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            NavDisplay(backstack = backstack, wrapperManager = manager) {
+            NavDisplay(backstack = backstack) {
                 when (it) {
                     first -> NavRecord(first) { Text(first) }
                     second -> NavRecord(second) { Text(second) }
@@ -82,15 +78,7 @@
         lateinit var backstack: MutableList<Any>
         composeTestRule.setContent {
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            NavDisplay(
-                backstack = backstack,
-                wrapperManager = manager,
-                onBack = {
-                    // removeLast requires API 35
-                    backstack.removeAt(backstack.size - 1)
-                }
-            ) {
+            NavDisplay(backstack = backstack) {
                 when (it) {
                     first -> NavRecord(first) { Text(first) }
                     second -> NavRecord(second, NavDisplay.isDialog(true)) { Text(second) }
@@ -115,15 +103,7 @@
         composeTestRule.setContent {
             onBackDispatcher = LocalOnBackPressedDispatcherOwner.current!!.onBackPressedDispatcher
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            NavDisplay(
-                backstack = backstack,
-                wrapperManager = manager,
-                onBack = {
-                    // removeLast requires API 35
-                    backstack.removeAt(backstack.size - 1)
-                }
-            ) {
+            NavDisplay(backstack = backstack) {
                 when (it) {
                     first -> NavRecord(first) { Text(first) }
                     second -> NavRecord(second) { Text(second) }
@@ -150,8 +130,7 @@
         lateinit var backstack: MutableList<Any>
         composeTestRule.setContent {
             backstack = remember { mutableStateListOf(first) }
-            val manager = rememberNavWrapperManager(emptyList())
-            NavDisplay(backstack = backstack, wrapperManager = manager) {
+            NavDisplay(backstack = backstack) {
                 when (it) {
                     first -> NavRecord(first) { numberOnScreen1 = rememberSaveable { increment++ } }
                     second -> NavRecord(second) {}
@@ -226,7 +205,6 @@
             backStack2 = remember { mutableStateListOf(second) }
             backStack3 = remember { mutableStateListOf(third) }
             state = remember { mutableStateOf(1) }
-            val manager = rememberNavWrapperManager(emptyList())
             NavDisplay(
                 backstack =
                     when (state.value) {
@@ -234,7 +212,6 @@
                         2 -> backStack2
                         else -> backStack3
                     },
-                wrapperManager = manager,
                 recordProvider =
                     recordProvider {
                         record(first) { Text(first) }
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt
deleted file mode 100644
index c1f4817..0000000
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/AnimatedNavDisplay.android.kt
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * 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.navigation3
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.ContentTransform
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.SizeTransform
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.window.Dialog
-import androidx.navigation3.AnimatedNavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
-
-/** Object that indicates the features that can be handled by the [AnimatedNavDisplay] */
-public object AnimatedNavDisplay {
-    /**
-     * Function to be called on the [NavRecord.featureMap] to notify the [AnimatedNavDisplay] that
-     * the content should be animated using the provided transitions.
-     */
-    public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
-        if (enter == null || exit == null) emptyMap()
-        else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
-
-    /**
-     * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
-     * content should be displayed inside of a [Dialog]
-     */
-    public fun isDialog(boolean: Boolean): Map<String, Any> =
-        if (!boolean) emptyMap() else mapOf(DIALOG_KEY to true)
-
-    internal const val ENTER_TRANSITION_KEY = "enterTransition"
-    internal const val EXIT_TRANSITION_KEY = "exitTransition"
-    internal const val DIALOG_KEY = "dialog"
-    internal const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700
-}
-
-/**
- * Display for Composable content that displays a single pane of content at a time, but can move
- * that content in and out with customized transitions.
- *
- * The AnimatedNavDisplay displays the content associated with the last key on the back stack in
- * most circumstances. If that content wants to be displayed as a dialog, as communicated by adding
- * [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
- * the second to last key is a displayed in the background.
- *
- * @param backstack the collection of keys that represents the state that needs to be handled
- * @param wrapperManager the manager that combines all of the [NavContentWrapper]s
- * @param modifier the modifier to be applied to the layout.
- * @param contentAlignment The [Alignment] of the [AnimatedContent]
- * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
- *   individually for each [NavRecord] by passing in the record's transitions through
- *   [NavRecord.featureMap].
- * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
- *   individually for each [NavRecord] by passing in the record's transitions through
- *   [NavRecord.featureMap].
- * @param onBack a callback for handling system back presses
- * @param recordProvider lambda used to construct each possible [NavRecord]
- * @sample androidx.navigation3.samples.AnimatedNav
- */
-@Composable
-public fun <T : Any> AnimatedNavDisplay(
-    backstack: List<T>,
-    wrapperManager: NavWrapperManager,
-    modifier: Modifier = Modifier,
-    contentAlignment: Alignment = Alignment.TopStart,
-    sizeTransform: SizeTransform? = null,
-    enterTransition: EnterTransition =
-        fadeIn(
-            animationSpec =
-                tween(
-                    DEFAULT_TRANSITION_DURATION_MILLISECOND,
-                )
-        ),
-    exitTransition: ExitTransition =
-        fadeOut(
-            animationSpec =
-                tween(
-                    DEFAULT_TRANSITION_DURATION_MILLISECOND,
-                )
-        ),
-    onBack: () -> Unit = {},
-    recordProvider: (key: T) -> NavRecord<out T>
-) {
-    BackHandler(backstack.size > 1, onBack)
-    wrapperManager.PrepareBackStack(backStack = backstack)
-    val key = backstack.last()
-    val record = recordProvider.invoke(key)
-
-    // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
-    val finalEnterTransition =
-        record.featureMap[AnimatedNavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition
-            ?: enterTransition
-    val finalExitTransition =
-        record.featureMap[AnimatedNavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition
-            ?: exitTransition
-
-    // if there is a dialog, we should create a transition with the next to last entry instead.
-    val transition =
-        if (record.featureMap[AnimatedNavDisplay.DIALOG_KEY] == true) {
-            if (backstack.size > 1) {
-                val previousKey = backstack[backstack.size - 2]
-                updateTransition(targetState = previousKey, label = previousKey.toString())
-            } else {
-                null
-            }
-        } else {
-            updateTransition(targetState = key, label = key.toString())
-        }
-
-    transition?.AnimatedContent(
-        modifier = modifier,
-        transitionSpec = {
-            ContentTransform(
-                targetContentEnter = finalEnterTransition,
-                initialContentExit = finalExitTransition,
-                sizeTransform = sizeTransform
-            )
-        },
-        contentAlignment = contentAlignment
-    ) { innerKey ->
-        wrapperManager.ContentForRecord(recordProvider.invoke(innerKey))
-    }
-
-    if (record.featureMap[AnimatedNavDisplay.DIALOG_KEY] == true) {
-        Dialog(onBack) { wrapperManager.ContentForRecord(record) }
-    }
-}
diff --git a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
index ae0ce29..cdcff77 100644
--- a/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
+++ b/navigation3/navigation3/src/androidMain/kotlin/androidx/navigation3/NavDisplay.android.kt
@@ -13,62 +13,138 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.navigation3
 
 import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.layout.Box
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.SizeTransform
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.window.Dialog
+import androidx.navigation3.NavDisplay.DEFAULT_TRANSITION_DURATION_MILLISECOND
 
 /** Object that indicates the features that can be handled by the [NavDisplay] */
 public object NavDisplay {
     /**
      * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
+     * content should be animated using the provided transitions.
+     */
+    public fun transition(enter: EnterTransition?, exit: ExitTransition?): Map<String, Any> =
+        if (enter == null || exit == null) emptyMap()
+        else mapOf(ENTER_TRANSITION_KEY to enter, EXIT_TRANSITION_KEY to exit)
+
+    /**
+     * Function to be called on the [NavRecord.featureMap] to notify the [NavDisplay] that the
      * content should be displayed inside of a [Dialog]
      */
     public fun isDialog(boolean: Boolean): Map<String, Any> =
         if (!boolean) emptyMap() else mapOf(DIALOG_KEY to true)
 
+    internal const val ENTER_TRANSITION_KEY = "enterTransition"
+    internal const val EXIT_TRANSITION_KEY = "exitTransition"
     internal const val DIALOG_KEY = "dialog"
+    internal const val DEFAULT_TRANSITION_DURATION_MILLISECOND = 700
 }
 
 /**
- * Simple display for Composable content that displays a single pane of content at a time.
+ * Display for Composable content that displays a single pane of content at a time, but can move
+ * that content in and out with customized transitions.
  *
  * The NavDisplay displays the content associated with the last key on the back stack in most
  * circumstances. If that content wants to be displayed as a dialog, as communicated by adding
  * [NavDisplay.isDialog] to a [NavRecord.featureMap], then the last key's content is a dialog and
  * the second to last key is a displayed in the background.
  *
- * @param modifier the modifier to be applied to the layout.
  * @param backstack the collection of keys that represents the state that needs to be handled
  * @param wrapperManager the manager that combines all of the [NavContentWrapper]s
+ * @param modifier the modifier to be applied to the layout.
+ * @param contentAlignment The [Alignment] of the [AnimatedContent]
+ *     * @param enterTransition Default [EnterTransition] for all [NavRecord]s. Can be overridden
+ *     * individually for each [NavRecord] by passing in the record's transitions through
+ *     * [NavRecord.featureMap].
+ *     * @param exitTransition Default [ExitTransition] for all [NavRecord]s. Can be overridden
+ *     * individually for each [NavRecord] by passing in the record's transitions through
+ *     * [NavRecord.featureMap].
+ *
  * @param onBack a callback for handling system back presses
  * @param recordProvider lambda used to construct each possible [NavRecord]
- * @sample androidx.navigation3.samples.BasicNav
+ * @sample androidx.navigation3.samples.BaseNav
  */
 @Composable
 public fun <T : Any> NavDisplay(
     backstack: List<T>,
-    wrapperManager: NavWrapperManager,
     modifier: Modifier = Modifier,
-    onBack: () -> Unit = {},
+    wrapperManager: NavWrapperManager = rememberNavWrapperManager(emptyList()),
+    contentAlignment: Alignment = Alignment.TopStart,
+    sizeTransform: SizeTransform? = null,
+    enterTransition: EnterTransition =
+        fadeIn(
+            animationSpec =
+                tween(
+                    DEFAULT_TRANSITION_DURATION_MILLISECOND,
+                )
+        ),
+    exitTransition: ExitTransition =
+        fadeOut(
+            animationSpec =
+                tween(
+                    DEFAULT_TRANSITION_DURATION_MILLISECOND,
+                )
+        ),
+    onBack: () -> Unit = { if (backstack is MutableList) backstack.removeAt(backstack.size - 1) },
     recordProvider: (key: T) -> NavRecord<out T>
 ) {
     BackHandler(backstack.size > 1, onBack)
     wrapperManager.PrepareBackStack(backStack = backstack)
     val key = backstack.last()
     val record = recordProvider.invoke(key)
-    if (record.featureMap[NavDisplay.DIALOG_KEY] == true) {
-        if (backstack.size > 1) {
-            val previousKey = backstack[backstack.size - 2]
-            val lastRecord = recordProvider.invoke(previousKey)
-            Box(modifier = modifier) { wrapperManager.ContentForRecord(lastRecord) }
+
+    // Incoming record defines transitions, otherwise it uses default transitions from NavDisplay
+    val finalEnterTransition =
+        record.featureMap[NavDisplay.ENTER_TRANSITION_KEY] as? EnterTransition ?: enterTransition
+    val finalExitTransition =
+        record.featureMap[NavDisplay.EXIT_TRANSITION_KEY] as? ExitTransition ?: exitTransition
+
+    val isDialog = record.featureMap[NavDisplay.DIALOG_KEY] == true
+
+    // if there is a dialog, we should create a transition with the next to last entry instead.
+    val transition =
+        if (isDialog) {
+            if (backstack.size > 1) {
+                val previousKey = backstack[backstack.size - 2]
+                val previousRecord = recordProvider.invoke(previousKey)
+                updateTransition(targetState = previousRecord, label = previousKey.toString())
+            } else {
+                null
+            }
+        } else {
+            updateTransition(targetState = record, label = key.toString())
         }
+
+    transition?.AnimatedContent(
+        modifier = modifier,
+        transitionSpec = {
+            ContentTransform(
+                targetContentEnter = finalEnterTransition,
+                initialContentExit = finalExitTransition,
+                sizeTransform = sizeTransform
+            )
+        },
+        contentAlignment = contentAlignment,
+        contentKey = { it.key }
+    ) { innerRecord ->
+        wrapperManager.ContentForRecord(innerRecord)
+    }
+
+    if (isDialog) {
         Dialog(onBack) { wrapperManager.ContentForRecord(record) }
-    } else {
-        Box(modifier = modifier) { wrapperManager.ContentForRecord(record) }
     }
 }
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
index 114ac13..4a137df 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/NavWrapperManager.kt
@@ -36,7 +36,7 @@
  *
  * @param navContentWrappers the [NavContentWrapper]s that are providing data to the content
  */
-public class NavWrapperManager(navContentWrappers: List<NavContentWrapper>) {
+public class NavWrapperManager(navContentWrappers: List<NavContentWrapper> = emptyList()) {
     /**
      * Final list of wrappers. This always adds a [SaveableStateNavContentWrapper] by default, as it
      * is required. It then filters out any duplicates to ensure there is always one instance of any
diff --git a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
index cab7204..9c6443f 100644
--- a/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
+++ b/navigation3/navigation3/src/commonMain/kotlin/androidx/navigation3/SaveableStateNavContentWrapper.kt
@@ -72,7 +72,12 @@
         DisposableEffect(key1 = key) {
             refCount[key] = refCount.getOrDefault(key, 0).plus(1)
             onDispose {
-                if (refCount[key] == 0) {
+                // We need to check to make sure that the refcount has been cleared here because
+                // when we are using animations, if the entire back stack is changed, we will
+                // execute the onDispose above that clears all of the counts before we finish the
+                // transition and run this onDispose so our count will already be gone and we
+                // should just remove the state.
+                if (!refCount.contains(key) || refCount[key] == 0) {
                     savedStateHolder?.removeState(key)
                 } else {
                     refCount[key] =
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
index 27466007..0c1973b 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/PdfView.kt
@@ -179,9 +179,20 @@
     /** The currently selected PDF content, as [Selection] */
     public val currentSelection: Selection?
         get() {
-            return selectionStateManager?.selectionModel?.selection
+            return selectionStateManager?.selectionModel?.value?.selection
         }
 
+    /** Listener interface to receive updates when the [currentSelection] changes */
+    public interface OnSelectionChangedListener {
+        /** Called when the [Selection] has changed */
+        public fun onSelectionChanged(
+            previousSelection: Selection?,
+            newSelection: Selection?,
+        )
+    }
+
+    private var onSelectionChangedListeners = mutableListOf<OnSelectionChangedListener>()
+
     /**
      * The [CoroutineScope] used to make suspending calls to [PdfDocument]. The size of the fixed
      * thread pool is arbitrary and subject to tuning.
@@ -192,8 +203,8 @@
     private var pageLayoutManager: PageLayoutManager? = null
     private var pageManager: PageManager? = null
     private var visiblePagesCollector: Job? = null
-    private var dimensionsCollector: Job? = null
-    private var invalidationCollector: Job? = null
+    private var layoutInfoCollector: Job? = null
+    private var pageSignalCollector: Job? = null
     private var selectionStateCollector: Job? = null
 
     private var deferredScrollPage: Int? = null
@@ -301,6 +312,33 @@
         }
     }
 
+    /**
+     * Adds the specified listener to the list of listeners that will be notified of selection
+     * change events.
+     *
+     * @param listener listener to notify when selection change events occur
+     * @see removeOnSelectionChangedListener
+     */
+    public fun addOnSelectionChangedListener(listener: OnSelectionChangedListener) {
+        onSelectionChangedListeners.add(listener)
+    }
+
+    /**
+     * Removes the specified listener from the list of listeners that will be notified of selection
+     * change events.
+     *
+     * @param listener listener to remove
+     */
+    public fun removeOnSelectionChangedListener(listener: OnSelectionChangedListener) {
+        onSelectionChangedListeners.remove(listener)
+    }
+
+    private fun dispatchSelectionChanged(old: Selection?, new: Selection?) {
+        for (listener in onSelectionChangedListeners) {
+            listener.onSelectionChanged(old, new)
+        }
+    }
+
     private fun gotoPage(pageNum: Int) {
         checkMainThread()
         val localPageLayoutManager =
@@ -386,7 +424,7 @@
         for (i in visiblePages.lower..visiblePages.upper) {
             val pageLoc = localPaginationManager.getPageLocation(i, getVisibleAreaInContentCoords())
             pageManager?.drawPage(i, canvas, pageLoc)
-            selectionModel?.let {
+            selectionModel?.value?.let {
                 selectionRenderer.drawSelectionOnPage(
                     model = it,
                     pageNum = i,
@@ -492,7 +530,7 @@
         }
         state.documentUri = pdfDocument?.uri
         state.paginationModel = pageLayoutManager?.paginationModel
-        state.selectionModel = selectionStateManager?.selectionModel
+        state.selectionModel = selectionStateManager?.selectionModel?.value
         return state
     }
 
@@ -638,32 +676,23 @@
             CoroutineScope(HandlerCompat.createAsync(handler.looper).asCoroutineDispatcher())
         pageLayoutManager?.let { manager ->
             // Don't let two copies of this run concurrently
-            val dimensionsToJoin = dimensionsCollector?.apply { cancel() }
-            dimensionsCollector =
+            val layoutInfoToJoin = layoutInfoCollector?.apply { cancel() }
+            layoutInfoCollector =
                 mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    manager.dimensions.collect {
-                        // Prevent 2 copies from running concurrently
-                        dimensionsToJoin?.join()
-                        onPageDimensionsReceived(it.first, it.second)
+                    // Prevent 2 copies from running concurrently
+                    layoutInfoToJoin?.join()
+                    launch {
+                        manager.dimensions.collect { onPageDimensionsReceived(it.first, it.second) }
                     }
-                }
-            // Don't let two copies of this run concurrently
-            val visiblePagesToJoin = visiblePagesCollector?.apply { cancel() }
-            visiblePagesCollector =
-                mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
-                    manager.visiblePages.collect {
-                        // Prevent 2 copies from running concurrently
-                        visiblePagesToJoin?.join()
-                        onVisiblePagesChanged()
-                    }
+                    launch { manager.visiblePages.collect { onVisiblePagesChanged() } }
                 }
         }
         pageManager?.let { manager ->
-            val invalidationToJoin = invalidationCollector?.apply { cancel() }
-            invalidationCollector =
+            val pageSignalsToJoin = pageSignalCollector?.apply { cancel() }
+            pageSignalCollector =
                 mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
                     // Prevent 2 copies from running concurrently
-                    invalidationToJoin?.join()
+                    pageSignalsToJoin?.join()
                     launch { manager.invalidationSignalFlow.collect { invalidate() } }
                     launch {
                         manager.pageTextReadyFlow.collect { pageNum ->
@@ -678,15 +707,22 @@
                 mainScope.launch(start = CoroutineStart.UNDISPATCHED) {
                     // Prevent 2 copies from running concurrently
                     selectionToJoin?.join()
-                    manager.selectionUiSignalBus.collect { onSelectionUiSignal(it) }
+                    launch { manager.selectionUiSignalBus.collect { onSelectionUiSignal(it) } }
+                    var prevSelection = currentSelection
+                    launch {
+                        manager.selectionModel.collect { newModel ->
+                            dispatchSelectionChanged(prevSelection, newModel?.selection)
+                            prevSelection = newModel?.selection
+                        }
+                    }
                 }
         }
     }
 
     private fun stopCollectingData() {
-        dimensionsCollector?.cancel()
+        layoutInfoCollector?.cancel()
         visiblePagesCollector?.cancel()
-        invalidationCollector?.cancel()
+        pageSignalCollector?.cancel()
         selectionStateCollector?.cancel()
     }
 
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
index 0ca14c12..ee3f374 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionModel.kt
@@ -62,6 +62,23 @@
         endBoundary.writeToParcel(dest, flags)
     }
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || other !is SelectionModel) return false
+
+        if (other.selection != selection) return false
+        if (other.startBoundary != startBoundary) return false
+        if (other.endBoundary != endBoundary) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = selection.hashCode()
+        result = 31 * result + startBoundary.hashCode()
+        result = 31 * result + endBoundary.hashCode()
+        return result
+    }
+
     companion object {
         /** Produces a [SelectionModel] from a single [PageSelection] on a single page */
         // TODO(b/386398335) Add support for creating a SelectionModel from selections on 2 pages
@@ -135,6 +152,21 @@
         dest.writeBoolean(isRtl)
     }
 
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || other !is UiSelectionBoundary) return false
+
+        if (other.location != this.location) return false
+        if (other.isRtl != this.isRtl) return false
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = location.hashCode()
+        result = 31 * result + isRtl.hashCode()
+        return result
+    }
+
     companion object CREATOR : Parcelable.Creator<UiSelectionBoundary> {
         override fun createFromParcel(parcel: Parcel): UiSelectionBoundary {
             return UiSelectionBoundary(parcel)
diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
index 72ec107..3f6cdd7 100644
--- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
+++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/view/SelectionStateManager.kt
@@ -28,7 +28,10 @@
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancelAndJoin
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 
 /** Owns and updates all mutable state related to content selection in [PdfView] */
@@ -39,8 +42,10 @@
     initialSelection: SelectionModel? = null,
 ) {
     /** The current [Selection] */
-    var selectionModel: SelectionModel? = initialSelection
-        @VisibleForTesting internal set
+    @VisibleForTesting val _selectionModel = MutableStateFlow<SelectionModel?>(initialSelection)
+
+    val selectionModel: StateFlow<SelectionModel?>
+        get() = _selectionModel
 
     /** Replay at few values in case of an UI signal issued while [PdfView] is not collecting */
     private val _selectionUiSignalBus = MutableSharedFlow<SelectionUiSignal>(replay = 3)
@@ -95,11 +100,12 @@
         setSelectionJob = null
         _selectionUiSignalBus.tryEmit(SelectionUiSignal.ToggleActionMode(show = false))
         _selectionUiSignalBus.tryEmit(SelectionUiSignal.Invalidate)
-        selectionModel = null
+        // tryEmit will always succeed for StateFlow
+        _selectionModel.tryEmit(null)
     }
 
     fun maybeShowActionMode() {
-        if (selectionModel != null) {
+        if (selectionModel.value != null) {
             _selectionUiSignalBus.tryEmit(SelectionUiSignal.ToggleActionMode(show = true))
         }
     }
@@ -124,7 +130,7 @@
     }
 
     private fun maybeHandleActionDown(location: PdfPoint, currentZoom: Float): Boolean {
-        val currentSelection = selectionModel ?: return false
+        val currentSelection = selectionModel.value ?: return false
         val start = currentSelection.startBoundary.location
         val end = currentSelection.endBoundary.location
         val touchTargetContentSize = handleTouchTargetSizePx / currentZoom
@@ -234,7 +240,9 @@
                             end.pagePoint
                         )
                     if (newSelection != null && newSelection.hasBounds) {
-                        selectionModel = SelectionModel.fromSinglePageSelection(newSelection)
+                        _selectionModel.update {
+                            SelectionModel.fromSinglePageSelection(newSelection)
+                        }
                         _selectionUiSignalBus.tryEmit(SelectionUiSignal.Invalidate)
                         // Show the action mode if the user is not actively dragging the handles
                         if (draggingState == null) {
diff --git a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
index b1f4dd3..7980a55 100644
--- a/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
+++ b/pdf/pdf-viewer/src/test/kotlin/androidx/pdf/view/SelectionStateManagerTest.kt
@@ -29,6 +29,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
@@ -99,7 +100,7 @@
         selectionStateManager.maybeSelectWordAtPoint(selectionPoint)
         testDispatcher.scheduler.runCurrent()
 
-        val selectionModel = selectionStateManager.selectionModel
+        val selectionModel = selectionStateManager.selectionModel.value
         assertThat(selectionModel).isNotNull()
         assertThat(selectionModel?.selection).isInstanceOf(TextSelection::class.java)
         val selection = requireNotNull(selectionModel?.selection as TextSelection)
@@ -146,7 +147,7 @@
         selectionStateManager.maybeSelectWordAtPoint(selectionPoint2)
         testDispatcher.scheduler.runCurrent()
 
-        val selectionModel = selectionStateManager.selectionModel
+        val selectionModel = selectionStateManager.selectionModel.value
         assertThat(selectionModel).isNotNull()
         assertThat(selectionModel?.selection).isInstanceOf(TextSelection::class.java)
         val selection = requireNotNull(selectionModel?.selection as TextSelection)
@@ -184,7 +185,7 @@
         assertThat(selectionStateManager.selectionModel).isNotNull()
         selectionStateManager.clearSelection()
 
-        assertThat(selectionStateManager.selectionModel).isNull()
+        assertThat(selectionStateManager.selectionModel.value).isNull()
         // We only care about the final 2 signals that should occur as a result of cancellation
         // hide action mode
         assertThat(uiSignals[uiSignals.size - 2])
@@ -201,18 +202,18 @@
 
         // Start a selection and don't finish it (i.e. no runCurrent)
         selectionStateManager.maybeSelectWordAtPoint(selectionPoint)
-        assertThat(selectionStateManager.selectionModel).isNull()
+        assertThat(selectionStateManager.selectionModel.value).isNull()
 
         // Clear selection, flush the scheduler, and make sure selection remains null (i.e. the work
         // enqueued by our initial selection doesn't finish and supersede the cleared state)
         selectionStateManager.clearSelection()
         testDispatcher.scheduler.runCurrent()
-        assertThat(selectionStateManager.selectionModel).isNull()
+        assertThat(selectionStateManager.selectionModel.value).isNull()
     }
 
     @Test
     fun maybeDragHandle_actionDownOutsideHandle_returnFalse() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
 
         assertThat(
                 selectionStateManager.maybeDragSelectionHandle(
@@ -226,7 +227,7 @@
 
     @Test
     fun maybeDragHandle_actionDownInsideStartHandle_returnTrue() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // Chose a point inside the start handle touch target (below and behind the start position)
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -246,7 +247,7 @@
 
     @Test
     fun maybeDragHandle_actionDownInsideEndHandle_returnTrue() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // Chose a point inside the end handle touch target (below and ahead the end position)
         val insideEndHandle =
             PointF(initialSelectionForDragging.endBoundary.location.pagePoint).apply {
@@ -266,7 +267,7 @@
 
     @Test
     fun maybeDragHandle_actionMove_updateSelection() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // "Grab" the start handle
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -295,7 +296,7 @@
 
         // Make sure the selection is updated appropriately
         testDispatcher.scheduler.runCurrent()
-        val selection = selectionStateManager.selectionModel?.selection
+        val selection = selectionStateManager.selectionModel.value?.selection
         assertThat(selection).isInstanceOf(TextSelection::class.java)
         val expectedStartLoc = initialSelectionForDragging.endBoundary.location.pagePoint
         val expectedEndLoc =
@@ -308,7 +309,7 @@
 
     @Test
     fun maybeDragHandle_actionMoveOutsidePage_returnTrue() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // "Grab" the start handle
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -337,7 +338,7 @@
 
     @Test
     fun maybeDragHandle_actionMoveWithoutActionDown_returnFalse() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // Chose a point inside the start handle touch target (below and behind the start position)
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -358,7 +359,7 @@
 
     @Test
     fun maybeDragHandle_actionUpWithoutActionDown_returnFalse() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // Chose a point inside the start handle touch target (below and behind the start position)
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
@@ -379,7 +380,7 @@
 
     @Test
     fun maybeDragHandle_actionUp_returnTrueAndStopHandlingEvents() {
-        selectionStateManager.selectionModel = initialSelectionForDragging
+        selectionStateManager._selectionModel.update { initialSelectionForDragging }
         // Chose a point inside the start handle touch target (below and behind the start position)
         val insideStartHandle =
             PointF(initialSelectionForDragging.startBoundary.location.pagePoint).apply {
diff --git a/preference/preference/src/main/res/layout/preference_list_fragment.xml b/preference/preference/src/main/res/layout/preference_list_fragment.xml
index ad03481..c2ae115 100644
--- a/preference/preference/src/main/res/layout/preference_list_fragment.xml
+++ b/preference/preference/src/main/res/layout/preference_list_fragment.xml
@@ -19,7 +19,6 @@
     xmlns:tools="http://schemas.android.com/tools"
     tools:ignore="NewApi"
     android:orientation="vertical"
-    android:fitsSystemWindows="true"
     android:layout_height="match_parent"
     android:layout_width="match_parent" >
 
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
index 37efc13..2460822 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/MultiTypedPagingSourceTest.kt
@@ -142,7 +142,7 @@
                 .containsExactlyElementsIn(
                     items.createExpected(
                         // Paging 3 implementation loads starting from initial key
-                        fromIndex = 98,
+                        fromIndex = 91,
                         toIndex = 100
                     )
                 )
@@ -579,7 +579,7 @@
                 .containsExactlyElementsIn(
                     items.createExpected(
                         // Paging 3 implementation loads starting from initial key
-                        fromIndex = 98,
+                        fromIndex = 91,
                         toIndex = 100
                     )
                 )
diff --git a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
index 0cd7d50..e72e6c8 100644
--- a/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
+++ b/room/room-paging/src/androidInstrumentedTest/kotlin/androidx/room/paging/LimitOffsetPagingSourceTest.kt
@@ -247,8 +247,9 @@
             dao.addAllItems(ITEMS_LIST)
             val result = pager.refresh(initialKey = 40) as LoadResult.Page
 
-            // initial loadSize = 15, but limited by id < 50, should only load items 40 - 50
-            assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(40, 50))
+            // initial loadSize = 15, but limited by id < 50, should treat 50 as end and
+            // load items 35 - 50
+            assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(35, 50))
             // should have 50 items fulfilling condition of id < 50 (TestItem id 0 - 49)
             assertThat(pagingSource.itemCount).isEqualTo(50)
         }
@@ -575,6 +576,36 @@
         assertThat(result.nextKey).isEqualTo(null)
     }
 
+    @Test
+    fun load_refreshKeyOnLastPage() = runPagingSourceTest { pager, _ ->
+        dao.addAllItems(ITEMS_LIST)
+        pager.refresh(initialKey = 70)
+        dao.deleteTestItems(80, 100)
+
+        // assume user was viewing last item of the refresh load with anchorPosition = 85,
+        // initialLoadSize = 15. This mimics how getRefreshKey() calculates refresh key.
+        val refreshKey = 85 - (15 / 2)
+
+        val pagingSource2 = LimitOffsetPagingSourceImpl(database)
+        val pager2 = TestPager(CONFIG, pagingSource2)
+        val result = pager2.refresh(initialKey = refreshKey) as LoadResult.Page
+
+        // database should only have 80 items left. Refresh key should be moved back at this point
+        // to ensure a full load. (greater than item count - loadSize after deletion)
+        assertThat(pagingSource2.itemCount).isEqualTo(80)
+        // ensure that paging source can handle invalid refresh key properly
+        // should load last page with items 65 - 80
+        assertThat(result.data).containsExactlyElementsIn(ITEMS_LIST.subList(65, 80))
+
+        // should account for updated item count to return correct itemsBefore, itemsAfter,
+        // prevKey, nextKey
+        assertThat(result.itemsBefore).isEqualTo(65)
+        assertThat(result.itemsAfter).isEqualTo(0)
+        // no append can be triggered
+        assertThat(result.prevKey).isEqualTo(65)
+        assertThat(result.nextKey).isEqualTo(null)
+    }
+
     /**
      * Tests the behavior if user was viewing items in the top of the database and those items were
      * deleted.
diff --git a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
index d08a210..d700b6c 100644
--- a/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
+++ b/room/room-paging/src/commonMain/kotlin/androidx/room/paging/util/RoomPagingUtil.kt
@@ -83,7 +83,7 @@
             }
         is Append -> key
         is Refresh ->
-            if (key >= itemCount) {
+            if (key >= itemCount - params.loadSize) {
                 maxOf(0, itemCount - params.loadSize)
             } else {
                 key
diff --git a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
index 108bd57..455fe21 100644
--- a/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
+++ b/security/security-state/src/main/java/androidx/security/state/SecurityPatchState.kt
@@ -95,9 +95,6 @@
         /** Kernel component providing kernel version as VersionedSpl. */
         public const val COMPONENT_KERNEL: String = "KERNEL"
 
-        /** WebView component providing default WebView provider version as VersionedSpl. */
-        internal const val COMPONENT_WEBVIEW: String = "WEBVIEW"
-
         /**
          * Vendor component providing ro.vendor.build.security_patch property value as DateBasedSpl.
          */
@@ -117,7 +114,6 @@
                 COMPONENT_SYSTEM_MODULES,
                 COMPONENT_KERNEL,
                 COMPONENT_VENDOR,
-                COMPONENT_WEBVIEW,
             ]
     )
     internal annotation class Component
@@ -572,9 +568,6 @@
 
                 DateBasedSecurityPatchLevel.fromString(vendorSpl)
             }
-
-            // TODO(musashi): Add support for webview package
-            COMPONENT_WEBVIEW -> TODO()
             else -> throw IllegalArgumentException("Unknown component: $component")
         }
     }
@@ -614,9 +607,6 @@
                 )
             }
             COMPONENT_KERNEL -> getPublishedKernelVersions()
-
-            // TODO(musashi): Add support for webview package
-            COMPONENT_WEBVIEW -> TODO()
             else -> throw IllegalArgumentException("Unknown component: $component")
         }
     }
@@ -740,8 +730,7 @@
                 // These components are expected to use DateBasedSpl
                 DateBasedSecurityPatchLevel.fromString(securityPatchLevel)
             }
-            COMPONENT_KERNEL,
-            COMPONENT_WEBVIEW -> {
+            COMPONENT_KERNEL -> {
                 // These components are expected to use VersionedSpl
                 VersionedSecurityPatchLevel.fromString(securityPatchLevel)
             }
@@ -767,13 +756,10 @@
                 COMPONENT_SYSTEM_MODULES,
                 COMPONENT_VENDOR,
                 COMPONENT_KERNEL,
-                COMPONENT_WEBVIEW
             )
 
         components.forEach { component ->
             if (component == COMPONENT_VENDOR && !USE_VENDOR_SPL) return@forEach
-            // TODO(musashi): Unblock once support for WebView is present.
-            if (component == COMPONENT_WEBVIEW) return@forEach
             val deviceSpl =
                 try {
                     getDeviceSecurityPatchLevel(component)
diff --git a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
index 28a6576..440fd0f6 100644
--- a/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
+++ b/security/security-state/src/test/java/androidx/security/state/SecurityPatchStateTest.kt
@@ -622,7 +622,7 @@
     fun testGetPatchedCves_ThrowsExceptionForInvalidComponent() {
         val spl = SecurityPatchState.DateBasedSecurityPatchLevel.fromString("2023-01-01")
 
-        securityState.getPatchedCves(SecurityPatchState.COMPONENT_WEBVIEW, spl)
+        securityState.getPatchedCves(SecurityPatchState.COMPONENT_KERNEL, spl)
     }
 
     @Test
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
index 0c304d1..a80f168 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/DatePickerDemo.kt
@@ -34,7 +34,7 @@
 import androidx.wear.compose.material3.DatePickerType
 import androidx.wear.compose.material3.Icon
 import androidx.wear.compose.material3.Text
-import androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+import androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
 import androidx.wear.compose.material3.samples.DatePickerSample
 import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
 import java.time.LocalDate
@@ -47,7 +47,7 @@
         ComposableDemo("Date Month-Day-Year") { DatePickerDemo(DatePickerType.MonthDayYear) },
         ComposableDemo("Date Day-Month-Year") { DatePickerDemo(DatePickerType.DayMonthYear) },
         ComposableDemo("Date System date format") { DatePickerSample() },
-        ComposableDemo("Date Range") { DatePickerMinDateMaxDateSample() },
+        ComposableDemo("Future only") { DatePickerFutureOnlySample() },
         ComposableDemo("Past only") { DatePickerPastOnlyDemo() },
     )
 
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
index a82e444..20ff2d3 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/EdgeButtonDemo.kt
@@ -40,7 +40,9 @@
 import androidx.compose.ui.unit.dp
 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
+import androidx.wear.compose.foundation.lazy.TransformingLazyColumn
 import androidx.wear.compose.foundation.lazy.rememberScalingLazyListState
+import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState
 import androidx.wear.compose.integration.demos.common.AdaptiveScreen
 import androidx.wear.compose.material3.ButtonDefaults
 import androidx.wear.compose.material3.Card
@@ -54,18 +56,21 @@
 import androidx.wear.compose.material3.TextButtonDefaults
 import androidx.wear.compose.material3.samples.icons.CheckIcon
 
+fun listOfLabels(): List<String> {
+    return listOf(
+        "Hi",
+        "Hello World",
+        "Hello world again?",
+        "More content as we add stuff",
+        "I don't know if this will fit now, testing",
+        "Really long text that it's going to take multiple lines",
+        "And now we are really pushing it because the screen is really small",
+    )
+}
+
 @Composable
 fun EdgeButtonBelowLazyColumnDemo() {
-    val labels =
-        listOf(
-            "Hi",
-            "Hello World",
-            "Hello world again?",
-            "More content as we add stuff",
-            "I don't know if this will fit now, testing",
-            "Really long text that it's going to take multiple lines",
-            "And now we are really pushing it because the screen is really small",
-        )
+    val labels = listOfLabels()
     val selectedLabel = remember { mutableIntStateOf(0) }
     AdaptiveScreen {
         val state = rememberLazyListState()
@@ -104,16 +109,7 @@
 
 @Composable
 fun EdgeButtonBelowScalingLazyColumnDemo() {
-    val labels =
-        listOf(
-            "Hi",
-            "Hello World",
-            "Hello world again?",
-            "More content as we add stuff",
-            "I don't know if this will fit now, testing",
-            "Really long text that it's going to take multiple lines",
-            "And now we are really pushing it because the screen is really small",
-        )
+    val labels = listOfLabels()
     val selectedLabel = remember { mutableIntStateOf(0) }
 
     AdaptiveScreen {
@@ -151,6 +147,45 @@
     }
 }
 
+@Composable
+fun EdgeButtonBelowTransformingLazyColumnDemo() {
+    val labels = listOfLabels()
+    val selectedLabel = remember { mutableIntStateOf(0) }
+    AdaptiveScreen {
+        val state = rememberTransformingLazyColumnState()
+        ScreenScaffold(
+            scrollState = state,
+            contentPadding = PaddingValues(horizontal = 10.dp, vertical = 20.dp),
+            edgeButton = {
+                EdgeButton(
+                    onClick = {},
+                    buttonSize = EdgeButtonSize.Large,
+                    colors = ButtonDefaults.buttonColors(containerColor = Color.DarkGray)
+                ) {
+                    Text(labels[selectedLabel.intValue], color = Color.White)
+                }
+            }
+        ) { contentPadding ->
+            TransformingLazyColumn(
+                state = state,
+                modifier = Modifier.fillMaxSize(),
+                verticalArrangement = Arrangement.spacedBy(4.dp),
+                contentPadding = contentPadding,
+                horizontalAlignment = Alignment.CenterHorizontally
+            ) {
+                items(labels.size) {
+                    Card(
+                        onClick = { selectedLabel.intValue = it },
+                        modifier = Modifier.fillMaxWidth(0.9f)
+                    ) {
+                        Text(labels[it])
+                    }
+                }
+            }
+        }
+    }
+}
+
 @Suppress("PrimitiveInCollection")
 @Composable
 fun EdgeButtonMultiDemo() {
diff --git a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
index b4828a7..8a93dff 100644
--- a/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
+++ b/wear/compose/compose-material3/integration-tests/src/main/java/androidx/wear/compose/material3/demos/WearMaterial3Demos.kt
@@ -104,6 +104,9 @@
                         ComposableDemo("Edge Button Below SLC") {
                             EdgeButtonBelowScalingLazyColumnDemo()
                         },
+                        ComposableDemo("Edge Button Below TLC") {
+                            EdgeButtonBelowTransformingLazyColumnDemo()
+                        },
                     )
                 ),
                 Material3DemoCategory(
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
index a412d5a..5cda3e5 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/ButtonSample.kt
@@ -17,13 +17,21 @@
 package androidx.wear.compose.material3.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
 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.res.painterResource
 import androidx.compose.ui.text.style.TextAlign
@@ -280,18 +288,39 @@
 @Sampled
 @Composable
 fun OutlinedCompactButtonSample(modifier: Modifier = Modifier) {
-    CompactButton(
-        onClick = { /* Do something */ },
-        colors = ButtonDefaults.outlinedButtonColors(),
-        border = ButtonDefaults.outlinedButtonBorder(enabled = true),
-        modifier = modifier,
-    ) {
-        Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
-        Spacer(Modifier.width(ButtonDefaults.IconSpacing))
-        Icon(
-            Icons.Filled.ArrowDropDown,
-            contentDescription = "Expand",
-            modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
-        )
+    Column(horizontalAlignment = Alignment.CenterHorizontally) {
+        var expanded by remember { mutableStateOf(false) }
+        if (expanded) {
+            Text("A multiline string showing two lines")
+        } else {
+            Text("One line text")
+        }
+        Spacer(Modifier.height(ButtonDefaults.IconSpacing))
+        CompactButton(
+            onClick = { expanded = !expanded },
+            colors = ButtonDefaults.outlinedButtonColors(),
+            border = ButtonDefaults.outlinedButtonBorder(enabled = true),
+            modifier = modifier,
+        ) {
+            if (expanded) {
+                Text("Show Less", maxLines = 1, overflow = TextOverflow.Ellipsis)
+            } else {
+                Text("Show More", maxLines = 1, overflow = TextOverflow.Ellipsis)
+            }
+            Spacer(Modifier.width(ButtonDefaults.IconSpacing))
+            if (expanded) {
+                Icon(
+                    Icons.Filled.KeyboardArrowUp,
+                    contentDescription = "Collapse",
+                    modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
+                )
+            } else {
+                Icon(
+                    Icons.Filled.KeyboardArrowDown,
+                    contentDescription = "Expand",
+                    modifier = Modifier.size(ButtonDefaults.ExtraSmallIconSize)
+                )
+            }
+        }
     }
 }
diff --git a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
index 4c2ae26..6557f67 100644
--- a/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
+++ b/wear/compose/compose-material3/samples/src/main/java/androidx/wear/compose/material3/samples/DatePickerSample.kt
@@ -17,12 +17,8 @@
 package androidx.wear.compose.material3.samples
 
 import androidx.annotation.Sampled
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.runtime.Composable
@@ -33,7 +29,6 @@
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.unit.dp
 import androidx.wear.compose.material3.Button
 import androidx.wear.compose.material3.DatePicker
 import androidx.wear.compose.material3.DatePickerType
@@ -106,14 +101,13 @@
 
 @Sampled
 @Composable
-fun DatePickerMinDateMaxDateSample() {
-    var showDatePicker by remember { mutableStateOf(false) }
-    var datePickerDate by remember { mutableStateOf(LocalDate.of(2024, 9, 2)) }
+fun DatePickerFutureOnlySample() {
+    val currentDate = LocalDate.now()
+    var showDatePicker by remember { mutableStateOf(true) }
+    var datePickerDate by remember { mutableStateOf(LocalDate.now()) }
     val formatter =
         DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
             .withLocale(LocalConfiguration.current.locales[0])
-    val minDate = LocalDate.of(2022, 10, 30)
-    val maxDate = LocalDate.of(2025, 2, 4)
     if (showDatePicker) {
         DatePicker(
             initialDate = datePickerDate, // Initialize with last picked date on reopen
@@ -121,18 +115,14 @@
                 datePickerDate = it
                 showDatePicker = false
             },
-            minValidDate = minDate,
-            maxValidDate = maxDate,
-            datePickerType = DatePickerType.YearMonthDay
+            datePickerType = DatePickerType.YearMonthDay,
+            minValidDate = currentDate
         )
     } else {
-        Column(
+        Box(
             modifier = Modifier.fillMaxSize(),
-            verticalArrangement = Arrangement.Center,
-            horizontalAlignment = Alignment.CenterHorizontally,
+            contentAlignment = Alignment.Center,
         ) {
-            Text(text = "${minDate.format(formatter)} ~ ${maxDate.format(formatter)}")
-            Spacer(modifier = Modifier.height(6.dp))
             Button(
                 onClick = { showDatePicker = true },
                 label = { Text("Selected Date") },
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
index 0f074a7..e92bce3 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/DatePickerTest.kt
@@ -35,7 +35,7 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.wear.compose.material3.internal.Strings
-import androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+import androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
 import androidx.wear.compose.material3.samples.DatePickerSample
 import androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
 import com.google.common.truth.Truth.assertThat
@@ -70,7 +70,7 @@
         rule.setContentWithTheme {
             DatePickerSample()
             DatePickerYearMonthDaySample()
-            DatePickerMinDateMaxDateSample()
+            DatePickerFutureOnlySample()
         }
     }
 
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
index 5a3f11e..b3a9562 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Button.kt
@@ -915,7 +915,8 @@
  * @sample androidx.wear.compose.material3.samples.FilledTonalCompactButtonSample
  *
  * Example of a [CompactButton] with an icon and label and with
- * [ButtonDefaults.outlinedButtonBorder] and [ButtonDefaults.outlinedButtonColors]
+ * [ButtonDefaults.outlinedButtonBorder] and [ButtonDefaults.outlinedButtonColors]. The example
+ * includes a [Text] that expands and collapses when the [CompactButton] is clicked.
  *
  * @sample androidx.wear.compose.material3.samples.OutlinedCompactButtonSample
  *
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
index af2130d..2879d30 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/DatePicker.kt
@@ -84,9 +84,9 @@
  *
  * @sample androidx.wear.compose.material3.samples.DatePickerYearMonthDaySample
  *
- * Example of a [DatePicker] with minDate and maxDate:
+ * Example of a [DatePicker] with a minDate:
  *
- * @sample androidx.wear.compose.material3.samples.DatePickerMinDateMaxDateSample
+ * @sample androidx.wear.compose.material3.samples.DatePickerFutureOnlySample
  * @param initialDate The initial value to be displayed in the DatePicker.
  * @param onDatePicked The callback that is called when the user confirms the date selection. It
  *   provides the selected date as [LocalDate]
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index a38ede8..3298f37 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -26,8 +26,8 @@
     defaultConfig {
         applicationId = "androidx.wear.compose.integration.demos"
         minSdk = 30
-        versionCode = 62
-        versionName = "1.62"
+        versionCode = 63
+        versionName = "1.63"
     }
 
     buildTypes {
diff --git a/wear/protolayout/protolayout-material3/api/current.txt b/wear/protolayout/protolayout-material3/api/current.txt
index 4db7d12..8942afc 100644
--- a/wear/protolayout/protolayout-material3/api/current.txt
+++ b/wear/protolayout/protolayout-material3/api/current.txt
@@ -224,10 +224,6 @@
     method public androidx.wear.protolayout.material3.GraphicDataCardStyle largeGraphicDataCardStyle();
   }
 
-  public final class HelpersKt {
-    method public static androidx.wear.protolayout.types.LayoutColor withOpacity(androidx.wear.protolayout.types.LayoutColor, @FloatRange(from=0.0, to=1.0) float ratio);
-  }
-
   public final class IconButtonStyle {
     field public static final androidx.wear.protolayout.material3.IconButtonStyle.Companion Companion;
   }
diff --git a/wear/protolayout/protolayout-material3/api/restricted_current.txt b/wear/protolayout/protolayout-material3/api/restricted_current.txt
index 4db7d12..8942afc 100644
--- a/wear/protolayout/protolayout-material3/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-material3/api/restricted_current.txt
@@ -224,10 +224,6 @@
     method public androidx.wear.protolayout.material3.GraphicDataCardStyle largeGraphicDataCardStyle();
   }
 
-  public final class HelpersKt {
-    method public static androidx.wear.protolayout.types.LayoutColor withOpacity(androidx.wear.protolayout.types.LayoutColor, @FloatRange(from=0.0, to=1.0) float ratio);
-  }
-
   public final class IconButtonStyle {
     field public static final androidx.wear.protolayout.material3.IconButtonStyle.Companion Companion;
   }
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
index c5fce5f..951a192 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Card.kt
@@ -62,7 +62,7 @@
  * @param time An optional slot for displaying the time relevant to the contents of the card,
  *   expected to be a short piece of text. Uses [CardColors.timeColor] color by default.
  * @param height The height of this card. It's highly recommended to set this to [expand] or
- *   [weight]
+ *   [weight].
  * @param shape Defines the card's shape, in other words the corner radius for this card.
  * @param colors The colors to be used for a background and inner content of this card. If the
  *   background image is also specified, the image will be laid out on top of the background color.
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
index a95093c..8f080b0 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/Helpers.kt
@@ -116,7 +116,7 @@
  * Note that this only looks at the static value of the [LayoutColor], any dynamic value will be
  * ignored.
  */
-public fun LayoutColor.withOpacity(@FloatRange(from = 0.0, to = 1.0) ratio: Float): LayoutColor {
+internal fun LayoutColor.withOpacity(@FloatRange(from = 0.0, to = 1.0) ratio: Float): LayoutColor {
     // From androidx.core.graphics.ColorUtils
     require(!(ratio < 0 || ratio > 1)) { "setOpacityForColor ratio must be between 0 and 1." }
     val fullyOpaque = 255
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
index d35b071..d77f5e3 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/modifiers/Semantics.kt
@@ -26,7 +26,7 @@
 import java.util.Objects
 
 /**
- * Adds content description to be read by Talkback.
+ * Adds content description to be read by accessibility services.
  *
  * @param staticValue The static content description. This value will be used if [dynamicValue] is
  *   null, or if can't be resolved.
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
index 1eb7d18..0bfb95e 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewProviderAdapter.java
@@ -18,13 +18,11 @@
 
 import android.annotation.SuppressLint;
 import android.net.Uri;
-import android.webkit.ValueCallback;
 import android.webkit.WebChromeClient;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
 import androidx.core.os.CancellationSignal;
-import androidx.webkit.PrerenderException;
 import androidx.webkit.PrerenderOperationCallback;
 import androidx.webkit.Profile;
 import androidx.webkit.SpeculativeLoadingParameters;
@@ -203,19 +201,6 @@
             @NonNull String url,
             @Nullable CancellationSignal cancellationSignal,
             @NonNull PrerenderOperationCallback callback) {
-
-        ValueCallback<Void> activationCallback = (value) -> {
-            // value will always be null.
-            callback.onPrerenderActivated();
-        };
-        ValueCallback<Throwable> errorCallback = (throwable) -> {
-            callback.onError(new PrerenderException("Prerender operation failed", throwable));
-        };
-        mImpl.prerenderUrl(
-                url,
-                cancellationSignal,
-                activationCallback,
-                errorCallback);
     }
 
     /**
@@ -228,22 +213,5 @@
             @Nullable CancellationSignal cancellationSignal,
             @NonNull SpeculativeLoadingParameters params,
             @NonNull PrerenderOperationCallback callback) {
-
-        InvocationHandler paramsBoundaryInterface =
-                BoundaryInterfaceReflectionUtil.createInvocationHandlerFor(
-                        new SpeculativeLoadingParametersAdapter(params));
-        ValueCallback<Void> activationCallback = (value) -> {
-            // value will always be null.
-            callback.onPrerenderActivated();
-        };
-        ValueCallback<Throwable> errorCallback = (throwable) -> {
-            callback.onError(new PrerenderException("Prerender operation failed", throwable));
-        };
-        mImpl.prerenderUrl(
-                url,
-                cancellationSignal,
-                paramsBoundaryInterface,
-                activationCallback,
-                errorCallback);
     }
 }
diff --git a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
index 1dcfeee..d17ea2d 100644
--- a/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
+++ b/window/window/src/androidTest/java/androidx/window/area/WindowAreaControllerImplTest.kt
@@ -319,14 +319,47 @@
             }
         }
 
-    /**
-     * Tests the presentation flow on to a rear facing display works as expected. Similar to
-     * [testPresentRearDisplayArea], but starts the presentation with a new instance of
-     * [WindowAreaControllerImpl].
-     */
     @RequiresApi(Build.VERSION_CODES.Q)
     @Test
-    fun testPresentRearDisplayAreaWithNewController(): Unit =
+    fun testRearDisplayPresentationModeSessionEndedError(): Unit =
+        testScope.runTest {
+            assumeAtLeastVendorApiLevel(minVendorApiLevel)
+            val extensionComponent = FakeWindowAreaComponent()
+            val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
+
+            extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
+            extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
+            val windowAreaInfo: WindowAreaInfo? =
+                async {
+                        return@async controller.windowAreaInfos.first().firstOrNull {
+                            it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
+                        }
+                    }
+                    .await()
+
+            assertNotNull(windowAreaInfo)
+            assertTrue {
+                windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA).status ==
+                    WINDOW_AREA_STATUS_UNAVAILABLE
+            }
+
+            val callback = TestWindowAreaPresentationSessionCallback()
+            activityScenario.scenario.onActivity { testActivity ->
+                controller.presentContentOnWindowArea(
+                    windowAreaInfo.token,
+                    testActivity,
+                    Runnable::run,
+                    callback
+                )
+                assert(!callback.sessionActive)
+                assert(callback.sessionError != null)
+                assert(callback.sessionError is IllegalStateException)
+            }
+        }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
+    @Test
+    fun testPresentContentWithNewControllerThrowsException(): Unit =
         testScope.runTest {
             assumeAtLeastVendorApiLevel(minVendorApiLevel)
             val extensions = FakeWindowAreaComponent()
@@ -360,117 +393,7 @@
                     Runnable::run,
                     callback
                 )
-                assert(callback.sessionActive)
-                assert(!callback.contentVisible)
 
-                callback.presentation?.setContentView(TextView(testActivity))
-                assert(callback.contentVisible)
-                assert(callback.sessionActive)
-
-                callback.presentation?.close()
-                assert(!callback.contentVisible)
-                assert(!callback.sessionActive)
-            }
-        }
-
-    /**
-     * Tests the presentation flow on to a rear facing display works as expected. Similar to
-     * [testTransferToRearFacingWindowArea], but starts the presentation with a new instance of
-     * [WindowAreaControllerImpl].
-     */
-    @RequiresApi(Build.VERSION_CODES.Q)
-    @Test
-    fun testTransferToRearDisplayAreaWithNewController(): Unit =
-        testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
-            val extensions = FakeWindowAreaComponent()
-            val controller = WindowAreaControllerImpl(windowAreaComponent = extensions)
-            extensions.currentRearDisplayStatus = STATUS_AVAILABLE
-            val callback = TestWindowAreaSessionCallback()
-            val windowAreaInfo =
-                async {
-                        return@async controller.windowAreaInfos.first().firstOrNull {
-                            it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
-                        }
-                    }
-                    .await()
-
-            assertNotNull(windowAreaInfo)
-            assertEquals(
-                windowAreaInfo.getCapability(OPERATION_TRANSFER_ACTIVITY_TO_AREA).status,
-                WINDOW_AREA_STATUS_AVAILABLE
-            )
-
-            activityScenario.scenario.onActivity { testActivity ->
-                testActivity.resetLayoutCounter()
-                testActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-                testActivity.waitForLayout()
-            }
-
-            // Create a new controller to start the transfer.
-            val controller2 = WindowAreaControllerImpl(windowAreaComponent = extensions)
-
-            activityScenario.scenario.onActivity { testActivity ->
-                assert(
-                    testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-                )
-                testActivity.resetLayoutCounter()
-                controller2.transferActivityToWindowArea(
-                    windowAreaInfo.token,
-                    testActivity,
-                    Runnable::run,
-                    callback
-                )
-            }
-
-            activityScenario.scenario.onActivity { testActivity ->
-                assert(
-                    testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
-                )
-                assert(callback.currentSession != null)
-                testActivity.resetLayoutCounter()
-                callback.endSession()
-            }
-            activityScenario.scenario.onActivity { testActivity ->
-                assert(
-                    testActivity.requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
-                )
-                assert(callback.currentSession == null)
-            }
-        }
-
-    @RequiresApi(Build.VERSION_CODES.Q)
-    @Test
-    fun testRearDisplayPresentationModeSessionEndedError(): Unit =
-        testScope.runTest {
-            assumeAtLeastVendorApiLevel(minVendorApiLevel)
-            val extensionComponent = FakeWindowAreaComponent()
-            val controller = WindowAreaControllerImpl(windowAreaComponent = extensionComponent)
-
-            extensionComponent.updateRearDisplayStatusListeners(STATUS_AVAILABLE)
-            extensionComponent.updateRearDisplayPresentationStatusListeners(STATUS_UNAVAILABLE)
-            val windowAreaInfo: WindowAreaInfo? =
-                async {
-                        return@async controller.windowAreaInfos.first().firstOrNull {
-                            it.type == WindowAreaInfo.Type.TYPE_REAR_FACING
-                        }
-                    }
-                    .await()
-
-            assertNotNull(windowAreaInfo)
-            assertTrue {
-                windowAreaInfo.getCapability(OPERATION_PRESENT_ON_AREA).status ==
-                    WINDOW_AREA_STATUS_UNAVAILABLE
-            }
-
-            val callback = TestWindowAreaPresentationSessionCallback()
-            activityScenario.scenario.onActivity { testActivity ->
-                controller.presentContentOnWindowArea(
-                    windowAreaInfo.token,
-                    testActivity,
-                    Runnable::run,
-                    callback
-                )
                 assert(!callback.sessionActive)
                 assert(callback.sessionError != null)
                 assert(callback.sessionError is IllegalStateException)
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
index 3b88357..04864cc 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaController.kt
@@ -130,37 +130,44 @@
         private val TAG = WindowAreaController::class.simpleName
 
         private var decorator: WindowAreaControllerDecorator = EmptyDecorator
+        private var windowAreaController: WindowAreaController? = null
 
         /** Provides an instance of [WindowAreaController]. */
         @JvmName("getOrCreate")
         @JvmStatic
         fun getOrCreate(): WindowAreaController {
-            val windowAreaComponentExtensions =
-                try {
-                    this::class.java.classLoader?.let {
-                        SafeWindowAreaComponentProvider(it).windowAreaComponent
-                    }
-                } catch (t: Throwable) {
-                    if (BuildConfig.verificationMode == VerificationMode.LOG) {
-                        Log.d(TAG, "Failed to load WindowExtensions")
-                    }
-                    null
-                }
+            return if (windowAreaController != null) windowAreaController!!
+            else {
 
-            val deviceSupported =
-                Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
-                    windowAreaComponentExtensions != null &&
-                    ExtensionsUtil.safeVendorApiLevel >= 3
+                val windowAreaComponentExtensions =
+                    try {
+                        this::class.java.classLoader?.let {
+                            SafeWindowAreaComponentProvider(it).windowAreaComponent
+                        }
+                    } catch (t: Throwable) {
+                        if (BuildConfig.verificationMode == VerificationMode.LOG) {
+                            Log.d(TAG, "Failed to load WindowExtensions")
+                        }
+                        null
+                    }
 
-            val controller =
-                if (deviceSupported) {
-                    WindowAreaControllerImpl(
-                        windowAreaComponent = windowAreaComponentExtensions!!,
-                    )
-                } else {
-                    EmptyWindowAreaControllerImpl()
-                }
-            return decorator.decorate(controller)
+                val deviceSupported =
+                    Build.VERSION.SDK_INT > Build.VERSION_CODES.Q &&
+                        windowAreaComponentExtensions != null &&
+                        ExtensionsUtil.safeVendorApiLevel >= 3
+
+                val controller =
+                    if (deviceSupported) {
+                        WindowAreaControllerImpl(
+                            windowAreaComponent = windowAreaComponentExtensions!!,
+                        )
+                    } else {
+                        EmptyWindowAreaControllerImpl()
+                    }
+                val decoratedController = decorator.decorate(controller)
+                windowAreaController = decoratedController
+                decoratedController
+            }
         }
 
         @JvmStatic
diff --git a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
index 445cb5d..5e97ca3 100644
--- a/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
+++ b/window/window/src/main/java/androidx/window/area/WindowAreaControllerImpl.kt
@@ -41,13 +41,9 @@
 import androidx.window.layout.WindowMetricsCalculator
 import androidx.window.reflection.Consumer2
 import java.util.concurrent.Executor
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
 
 /**
  * Implementation of WindowAreaController for devices that do implement the WindowAreaComponent on
@@ -208,18 +204,7 @@
             return
         }
 
-        if (currentRearDisplayModeStatus == WINDOW_AREA_STATUS_UNKNOWN) {
-            Log.d(TAG, "Force updating currentRearDisplayModeStatus")
-            // currentRearDisplayModeStatus may be null if the client has not queried
-            // WindowAreaController.windowAreaInfos using this instance. In this case, we query
-            // it for a single value to force update currentRearDisplayModeStatus.
-            CoroutineScope(executor.asCoroutineDispatcher()).launch {
-                windowAreaInfos.first()
-                startRearDisplayMode(activity, executor, windowAreaSessionCallback)
-            }
-        } else {
-            startRearDisplayMode(activity, executor, windowAreaSessionCallback)
-        }
+        startRearDisplayMode(activity, executor, windowAreaSessionCallback)
     }
 
     override fun presentContentOnWindowArea(
@@ -237,26 +222,7 @@
             return
         }
 
-        if (currentRearDisplayPresentationStatus == WINDOW_AREA_STATUS_UNKNOWN) {
-            Log.d(TAG, "Force updating currentRearDisplayPresentationStatus")
-            // currentRearDisplayModeStatus may be null if the client has not queried
-            // WindowAreaController.windowAreaInfos using this instance. In this case, we query
-            // it for a single value to force update currentRearDisplayPresentationStatus.
-            CoroutineScope(executor.asCoroutineDispatcher()).launch {
-                windowAreaInfos.first()
-                startRearDisplayPresentationMode(
-                    activity,
-                    executor,
-                    windowAreaPresentationSessionCallback
-                )
-            }
-        } else {
-            startRearDisplayPresentationMode(
-                activity,
-                executor,
-                windowAreaPresentationSessionCallback
-            )
-        }
+        startRearDisplayPresentationMode(activity, executor, windowAreaPresentationSessionCallback)
     }
 
     private fun startRearDisplayMode(
diff --git a/xr/compose/material3/material3/api/current.txt b/xr/compose/material3/material3/api/current.txt
index 98fb6c4..0dc4412 100644
--- a/xr/compose/material3/material3/api/current.txt
+++ b/xr/compose/material3/material3/api/current.txt
@@ -39,6 +39,10 @@
     property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.compose.material3.VerticalOrbiterProperties?> LocalNavigationRailOrbiterProperties;
   }
 
+  public final class ThreePaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void ThreePaneScaffold(androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder, kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane, optional kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane, kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public final class VerticalOrbiterProperties {
     ctor public VerticalOrbiterProperties(androidx.xr.compose.spatial.EdgeOffset offset, int position, androidx.compose.ui.Alignment.Vertical alignment, androidx.xr.compose.spatial.OrbiterSettings settings, androidx.xr.compose.subspace.layout.SpatialShape shape);
     method public androidx.xr.compose.material3.VerticalOrbiterProperties copy(optional androidx.xr.compose.spatial.EdgeOffset? offset, optional androidx.xr.compose.spatial.OrbiterEdge.Vertical? position, optional androidx.compose.ui.Alignment.Vertical? alignment, optional androidx.xr.compose.spatial.OrbiterSettings? settings, optional androidx.xr.compose.subspace.layout.SpatialShape? shape);
diff --git a/xr/compose/material3/material3/api/restricted_current.txt b/xr/compose/material3/material3/api/restricted_current.txt
index 98fb6c4..0dc4412 100644
--- a/xr/compose/material3/material3/api/restricted_current.txt
+++ b/xr/compose/material3/material3/api/restricted_current.txt
@@ -39,6 +39,10 @@
     property @SuppressCompatibility @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.xr.compose.material3.VerticalOrbiterProperties?> LocalNavigationRailOrbiterProperties;
   }
 
+  public final class ThreePaneScaffoldKt {
+    method @SuppressCompatibility @androidx.compose.runtime.Composable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public static void ThreePaneScaffold(androidx.xr.compose.subspace.layout.SubspaceModifier modifier, androidx.compose.material3.adaptive.layout.PaneScaffoldDirective scaffoldDirective, androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder paneOrder, kotlin.jvm.functions.Function0<kotlin.Unit> secondaryPane, optional kotlin.jvm.functions.Function0<kotlin.Unit>? tertiaryPane, kotlin.jvm.functions.Function0<kotlin.Unit> primaryPane);
+  }
+
   @SuppressCompatibility @androidx.compose.runtime.Immutable @androidx.xr.compose.material3.ExperimentalMaterial3XrApi public final class VerticalOrbiterProperties {
     ctor public VerticalOrbiterProperties(androidx.xr.compose.spatial.EdgeOffset offset, int position, androidx.compose.ui.Alignment.Vertical alignment, androidx.xr.compose.spatial.OrbiterSettings settings, androidx.xr.compose.subspace.layout.SpatialShape shape);
     method public androidx.xr.compose.material3.VerticalOrbiterProperties copy(optional androidx.xr.compose.spatial.EdgeOffset? offset, optional androidx.xr.compose.spatial.OrbiterEdge.Vertical? position, optional androidx.compose.ui.Alignment.Vertical? alignment, optional androidx.xr.compose.spatial.OrbiterSettings? settings, optional androidx.xr.compose.subspace.layout.SpatialShape? shape);
diff --git a/xr/compose/material3/material3/build.gradle b/xr/compose/material3/material3/build.gradle
index abb0d4c..0cad526 100644
--- a/xr/compose/material3/material3/build.gradle
+++ b/xr/compose/material3/material3/build.gradle
@@ -36,6 +36,7 @@
 dependencies {
     api(libs.kotlinStdlib)
     // Add dependencies here
+    implementation(project(":compose:ui:ui-unit"))
     implementation(project(":compose:material3:adaptive:adaptive"))
     implementation(project(":compose:material3:adaptive:adaptive-layout"))
     implementation(project(":compose:material3:material3"))
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
index 6862955..4411de4 100644
--- a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/EnableXrComponentOverrides.kt
@@ -21,6 +21,7 @@
 import androidx.compose.material3.LocalNavigationRailComponentOverride
 import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
 import androidx.compose.material3.adaptive.layout.LocalAnimatedPaneOverride
+import androidx.compose.material3.adaptive.layout.LocalThreePaneScaffoldOverride
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ProvidedValue
@@ -61,6 +62,7 @@
                     )
                 }
                 if (context.shouldOverrideComponent(XrComponentOverride.ThreePaneScaffold)) {
+                    add(LocalThreePaneScaffoldOverride provides XrThreePaneScaffoldOverride)
                     add(LocalAnimatedPaneOverride provides XrAnimatedPaneOverride)
                 }
             }
@@ -127,5 +129,10 @@
     @Composable
     override fun XrComponentOverrideEnablerContext.shouldOverrideComponent(
         component: XrComponentOverride
-    ): Boolean = isSpatializationEnabled
+    ): Boolean =
+        when (component) {
+            // TODO(b/388825260): Allow enabling ThreePaneScaffold once all edge-cases are fixed
+            XrComponentOverride.ThreePaneScaffold -> false
+            else -> isSpatializationEnabled
+        }
 }
diff --git a/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
new file mode 100644
index 0000000..ca1ae92
--- /dev/null
+++ b/xr/compose/material3/material3/src/main/java/androidx/xr/compose/material3/ThreePaneScaffold.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.xr.compose.material3
+
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveComponentOverrideApi
+import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective
+import androidx.compose.material3.adaptive.layout.PaneScaffoldParentData
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldHorizontalOrder
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverride
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldOverrideContext
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.xr.compose.spatial.Subspace
+import androidx.xr.compose.subspace.SpatialLayoutSpacer
+import androidx.xr.compose.subspace.SpatialPanel
+import androidx.xr.compose.subspace.SpatialRow
+import androidx.xr.compose.subspace.layout.SubspaceModifier
+import androidx.xr.compose.subspace.layout.fillMaxHeight
+import androidx.xr.compose.subspace.layout.height
+import androidx.xr.compose.subspace.layout.width
+import kotlin.math.roundToInt
+
+/**
+ * A pane scaffold composable that can display up to three panes in the order that
+ * [ThreePaneScaffoldHorizontalOrder] specifies, and allocate margins and spacers according to
+ * [PaneScaffoldDirective].
+ *
+ * [ThreePaneScaffold] is the base composable functions of adaptive programming. Developers can
+ * freely pipeline the relevant adaptive signals and use them as input of the scaffold function to
+ * render the final adaptive layout.
+ *
+ * @param modifier The modifier to be applied to the layout.
+ * @param scaffoldDirective The top-level directives about how the scaffold should arrange its
+ *   panes.
+ * @param paneOrder The horizontal order of the panes from start to end in the scaffold.
+ * @param secondaryPane The content of the secondary pane that has a priority lower then the primary
+ *   pane but higher than the tertiary pane.
+ * @param tertiaryPane The content of the tertiary pane that has the lowest priority.
+ * @param primaryPane The content of the primary pane that has the highest priority.
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@ExperimentalMaterial3XrApi
+@Composable
+public fun ThreePaneScaffold(
+    modifier: SubspaceModifier,
+    scaffoldDirective: PaneScaffoldDirective,
+    paneOrder: ThreePaneScaffoldHorizontalOrder,
+    secondaryPane: @Composable () -> Unit,
+    tertiaryPane: (@Composable () -> Unit)? = null,
+    primaryPane: @Composable () -> Unit
+) {
+    Subspace {
+        SpatialRow(modifier = modifier.height(XrThreePaneScaffoldTokens.PanelHeight)) {
+            var drawSpacer = false // Only draws spacers after the first pane is drawn
+            paneOrder.each { role ->
+                when (role) {
+                    ThreePaneScaffoldRole.Primary -> {
+                        Panel(
+                            scaffoldDirective,
+                            XrThreePaneScaffoldTokens.PrimaryPanePanelWidth,
+                            drawSpacer,
+                            primaryPane
+                        )
+                        drawSpacer = true
+                    }
+                    ThreePaneScaffoldRole.Secondary -> {
+                        Panel(
+                            scaffoldDirective,
+                            XrThreePaneScaffoldTokens.SecondaryPanePanelWidth,
+                            drawSpacer,
+                            secondaryPane
+                        )
+                        drawSpacer = true
+                    }
+                    ThreePaneScaffoldRole.Tertiary ->
+                        if (tertiaryPane != null) {
+                            Panel(
+                                scaffoldDirective,
+                                XrThreePaneScaffoldTokens.TertiaryPanePanelWidth,
+                                drawSpacer,
+                                tertiaryPane
+                            )
+                            drawSpacer = true
+                        }
+                }
+            }
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+@Composable
+private fun Panel(
+    scaffoldDirective: PaneScaffoldDirective,
+    defaultPreferredWidth: Dp,
+    drawSpacer: Boolean,
+    content: @Composable () -> Unit
+) {
+    if (drawSpacer) {
+        SpatialLayoutSpacer(SubspaceModifier.width(scaffoldDirective.horizontalPartitionSpacerSize))
+    }
+
+    SpatialPanel(SubspaceModifier.width(defaultPreferredWidth).fillMaxHeight()) {
+        Layout(content) { measurables, constraints ->
+            val measurable = measurables[0]
+            val parentData = measurable.parentData as? PaneScaffoldParentData
+            val widthFloat = parentData?.preferredWidth ?: defaultPreferredWidth
+            val width = widthFloat.toPx().roundToInt()
+            val height = constraints.maxHeight
+            return@Layout layout(width, height) {
+                measurable.measure(Constraints.fixed(width, height)).place(0, 0)
+            }
+        }
+    }
+}
+
+/**
+ * [ThreePaneScaffoldOverride] that uses the XR-specific [ThreePaneScaffold].
+ *
+ * Note that when using this override, any madifiers passed in to the 2D composable are ignored.
+ */
+@ExperimentalMaterial3XrApi
+@OptIn(
+    ExperimentalMaterial3AdaptiveApi::class,
+    ExperimentalMaterial3AdaptiveComponentOverrideApi::class
+)
+internal object XrThreePaneScaffoldOverride : ThreePaneScaffoldOverride {
+    @Composable
+    override fun ThreePaneScaffoldOverrideContext.ThreePaneScaffold() {
+        ThreePaneScaffold(
+            modifier = SubspaceModifier,
+            scaffoldDirective = scaffoldDirective.copy(maxHorizontalPartitions = 3),
+            paneOrder = paneOrder,
+            primaryPane = primaryPane,
+            secondaryPane = secondaryPane,
+            tertiaryPane = tertiaryPane,
+        )
+    }
+}
+
+// TODO(conradchen): Confirm the values with design
+private object XrThreePaneScaffoldTokens {
+    val PanelHeight = 1024.dp
+    val PrimaryPanePanelWidth = 800.dp
+    val SecondaryPanePanelWidth = 412.dp
+    val TertiaryPanePanelWidth = 412.dp
+}
+
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private inline fun ThreePaneScaffoldHorizontalOrder.each(action: (ThreePaneScaffoldRole) -> Unit) {
+    action(get(0))
+    action(get(1))
+    action(get(2))
+}