Merge "Migrate additional samples to new resolution method" into androidx-main
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
index c82ead6..d184d2f 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/Constants.kt
@@ -20,7 +20,7 @@
 
 // Minimum AGP version required
 internal val MIN_AGP_VERSION_REQUIRED_INCLUSIVE = AndroidPluginVersion(8, 0, 0)
-internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 5, 0).alpha(1)
+internal val MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE = AndroidPluginVersion(8, 6, 0).alpha(1)
 
 // Prefix for the build type baseline profile
 internal const val BUILD_TYPE_BASELINE_PROFILE_PREFIX = "nonMinified"
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
index 0c71b29..65b2efc 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/BenchmarkStateConfigTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.benchmark
 
-import android.os.Build
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SmallTest
@@ -39,14 +38,11 @@
         val state = BenchmarkState(config)
         var count = 0
         while (state.keepRunning()) {
-            if (Build.VERSION.SDK_INT < 21) {
-                // This spin loop works around an issue where on Mako API 17, nanoTime is only
-                // precise to 30us. A more ideal fix might introduce an automatic divisor to
-                // WarmupManager when the duration values it sees are 0, but this is simple.
-                val start = System.nanoTime()
-                @Suppress("ControlFlowWithEmptyBody")
-                while (System.nanoTime() == start) {}
-            }
+            // This spin loop works around an issue where nanoTime is only precise to 30us on some
+            // devices. This was reproduced on api 17 and emulators api 33. (b/331226761)
+            val start = System.nanoTime()
+            @Suppress("ControlFlowWithEmptyBody")
+            while (System.nanoTime() == start) {}
             count++
         }
 
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
index f68f14d..ac0aee1 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/MetricResultTest.kt
@@ -18,15 +18,25 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
-public class MetricResultTest {
+class MetricResultTest {
     @Test
-    public fun repeat() {
+    fun constructorThrowsIfEmpty() {
+        val exception = assertFailsWith<IllegalArgumentException> {
+            MetricResult("test", emptyList())
+        }
+
+        assertEquals("At least one result is necessary, 0 found for test.", exception.message!!)
+    }
+
+    @Test
+    fun repeat() {
         val metricResult = MetricResult("test", listOf(10.0, 10.0, 10.0, 10.0))
         assertEquals(10.0, metricResult.min, 0.0)
         assertEquals(10.0, metricResult.max, 0.0)
@@ -39,7 +49,7 @@
     }
 
     @Test
-    public fun one() {
+    fun one() {
         val metricResult = MetricResult("test", listOf(10.0))
         assertEquals(10.0, metricResult.min, 0.0)
         assertEquals(10.0, metricResult.max, 0.0)
@@ -52,7 +62,7 @@
     }
 
     @Test
-    public fun simple() {
+    fun simple() {
         val metricResult = MetricResult("test", (0..100).map { it.toDouble() })
         assertEquals(50.0, metricResult.median, 0.0)
         assertEquals(100.0, metricResult.max, 0.0)
@@ -65,7 +75,7 @@
     }
 
     @Test
-    public fun lerp() {
+    fun lerp() {
         assertEquals(MetricResult.lerp(0.0, 1000.0, 0.5), 500.0, 0.0)
         assertEquals(MetricResult.lerp(0.0, 1000.0, 0.75), 750.0, 0.0)
         assertEquals(MetricResult.lerp(0.0, 1000.0, 0.25), 250.0, 0.0)
@@ -73,7 +83,7 @@
     }
 
     @Test
-    public fun getPercentile() {
+    fun getPercentile() {
         (0..100).forEach {
             assertEquals(
                 it.toDouble(),
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
index 3af2d32..357557a 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/MetricResult.kt
@@ -47,7 +47,7 @@
     init {
         val values = data.sorted()
         val size = values.size
-        require(size >= 1) { "At least one result is necessary." }
+        require(size >= 1) { "At least one result is necessary, $size found for $name." }
 
         val mean: Double = data.average()
         min = values.first()
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index 50b1662..01c4831 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -17,8 +17,10 @@
 package androidx.benchmark.macro
 
 import android.annotation.SuppressLint
+import android.content.Intent
 import androidx.annotation.RequiresApi
 import androidx.benchmark.DeviceInfo
+import androidx.benchmark.json.BenchmarkData
 import androidx.benchmark.perfetto.PerfettoConfig
 import androidx.benchmark.perfetto.PerfettoHelper
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -85,6 +87,38 @@
         assertTrue(exception.message!!.contains("Require iterations > 0"))
     }
 
+    @Test
+    fun macrobenchmarkWithStartupMode_noMethodTrace() {
+        val result = macrobenchmarkWithStartupMode(
+                uniqueName = "uniqueName", // ignored, uniqueness not important
+                className = "className",
+                testName = "testName",
+                packageName = Packages.TARGET,
+                metrics = listOf(StartupTimingMetric()),
+                compilationMode = CompilationMode.Ignore(),
+                iterations = 1,
+                startupMode = StartupMode.COLD,
+                perfettoConfig = null,
+                setupBlock = {},
+                measureBlock = {
+                    startActivityAndWait(
+                        Intent(
+                            "androidx.benchmark.integration.macrobenchmark.target" +
+                                ".TRIVIAL_STARTUP_ACTIVITY"
+                        )
+                    )
+                }
+            )
+        assertEquals(
+            1,
+            result.profilerOutputs!!.size
+        )
+        assertEquals(
+            result.profilerOutputs!!.single().type,
+            BenchmarkData.TestResult.ProfilerOutput.Type.PerfettoTrace
+        )
+    }
+
     enum class Block { Setup, Measure }
 
     @RequiresApi(29)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
index 65ada92..e15e059 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/Packages.kt
@@ -30,4 +30,11 @@
      * Preferably use this app/package if not killing/compiling target.
      */
     const val TEST = "androidx.benchmark.macro.test"
+
+    /**
+     * Package not present on device.
+     *
+     * Used to validate behavior when package can't be found.
+     */
+    const val MISSING = "not.real.fake.package"
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index 055ccfd..b299dde 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -17,43 +17,82 @@
 package androidx.benchmark.macro
 
 import android.os.Build
-import androidx.benchmark.junit4.PerfettoTraceRule
-import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import kotlin.test.assertContains
 import kotlin.test.assertNull
-import org.junit.Rule
+import org.junit.Assert.assertNotNull
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ProfileInstallBroadcastTest {
-    @OptIn(ExperimentalPerfettoCaptureApi::class)
-    @get:Rule
-    val perfettoTraceRule = PerfettoTraceRule()
-
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
     @Test
     fun installProfile() {
         assertNull(ProfileInstallBroadcast.installProfile(Packages.TARGET))
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun installProfile_missing() {
+        val errorString = ProfileInstallBroadcast.installProfile(Packages.MISSING)
+        assertNotNull(errorString)
+        assertContains(
+            errorString!!,
+            "The baseline profile install broadcast was not received"
+        )
+    }
+
     @Test
     fun skipFileOperation() {
         assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "WRITE_SKIP_FILE"))
         assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "DELETE_SKIP_FILE"))
     }
 
+    @Test
+    fun skipFileOperation_missing() {
+        ProfileInstallBroadcast.skipFileOperation(Packages.MISSING, "WRITE_SKIP_FILE").apply {
+            assertNotNull(this)
+            assertContains(this!!, "The baseline profile skip file broadcast was not received")
+        }
+        ProfileInstallBroadcast.skipFileOperation(Packages.MISSING, "DELETE_SKIP_FILE").apply {
+            assertNotNull(this)
+            assertContains(this!!, "The baseline profile skip file broadcast was not received")
+        }
+    }
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
     @Test
     fun saveProfile() {
         assertNull(ProfileInstallBroadcast.saveProfile(Packages.TARGET))
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun saveProfile_missing() {
+        val errorString = ProfileInstallBroadcast.saveProfile(Packages.MISSING)
+        assertNotNull(errorString)
+        assertContains(errorString!!, "The save profile broadcast event was not received")
+    }
+
     @Test
     fun dropShaderCache() {
         assertNull(ProfileInstallBroadcast.dropShaderCache(Packages.TARGET))
     }
+
+    @Test
+    fun dropShaderCache_missing() {
+        val errorString = ProfileInstallBroadcast.dropShaderCache(Packages.MISSING)
+        assertNotNull(errorString)
+        assertContains(errorString!!, "The DROP_SHADER_CACHE broadcast was not received")
+
+        // validate extra instructions
+        assertContains(
+            errorString,
+            "verify: 1) androidx.profileinstaller.ProfileInstallReceiver appears unobfuscated"
+        )
+    }
 }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index ec88304..a16bfd5 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -230,10 +230,6 @@
     // Capture if the app being benchmarked is a system app.
     scope.isSystemApp = applicationInfo.isSystemApp()
 
-    if (requestMethodTracing) {
-        scope.startMethodTracing()
-    }
-
     // Ensure the device is awake
     scope.device.wakeUp()
 
@@ -309,7 +305,9 @@
                                 it.start()
                             }
                         }
-                        scope.startMethodTracing()
+                        if (requestMethodTracing) {
+                            scope.startMethodTracing()
+                        }
                         trace("measureBlock") {
                             measureBlock(scope)
                         }
@@ -318,7 +316,9 @@
                             metrics.forEach {
                                 it.stop()
                             }
-                            methodTracingResultFiles += scope.stopMethodTracing()
+                            if (requestMethodTracing) {
+                                methodTracingResultFiles += scope.stopMethodTracing()
+                            }
                         }
                     }
                 }!!
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index bd622f0..e51872e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -187,10 +187,8 @@
         // Use an explicit broadcast given the app was force-stopped.
         val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
         val operationKey = "EXTRA_BENCHMARK_OPERATION"
-        val result = Shell.amBroadcast(
-            "-a $action -e $operationKey $operation $packageName/$receiverName"
-        )
-        return when (result) {
+        val broadcastArguments = "-a $action -e $operationKey $operation $packageName/$receiverName"
+        return when (val result = Shell.amBroadcast(broadcastArguments)) {
             null, 0, 16 /* BENCHMARK_OPERATION_UNKNOWN */ -> {
                 // 0 is returned by the platform by default, and also if no broadcast receiver
                 // receives the broadcast.
@@ -201,7 +199,12 @@
                     "This most likely means that the `androidx.profileinstaller` library " +
                     "used by the target apk is old. Please use `1.3.0-alpha02` or newer. " +
                     "For more information refer to the release notes at " +
-                    "https://developer.android.com/jetpack/androidx/releases/profileinstaller."
+                    "https://developer.android.com/jetpack/androidx/releases/profileinstaller. " +
+                    "If you are already using androidx.profileinstaller library and still seeing " +
+                    "error, verify: 1) androidx.profileinstaller.ProfileInstallReceiver appears " +
+                    "unobfuscated in your APK's AndroidManifest and dex, and 2) the following " +
+                    "command executes successfully (should print 14): " +
+                    "adb shell am broadcast $broadcastArguments"
             }
             15 -> { // RESULT_BENCHMARK_OPERATION_FAILURE
                 "The $operation broadcast failed."
diff --git a/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt
new file mode 100644
index 0000000..9852727
--- /dev/null
+++ b/binarycompatibilityvalidator/binarycompatibilityvalidator/src/main/java/androidx/binarycompatibilityvalidator/BinaryCompatibilityChecker.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalLibraryAbiReader::class)
+
+package androidx.binarycompatibilityvalidator
+
+import org.jetbrains.kotlin.library.abi.ExperimentalLibraryAbiReader
+import org.jetbrains.kotlin.library.abi.LibraryAbi
+
+@Suppress("UNUSED_PARAMETER")
+@OptIn(ExperimentalLibraryAbiReader::class)
+class BinaryCompatibilityChecker(
+    private val newLibraryAbi: LibraryAbi,
+    private val oldLibraryAbi: LibraryAbi
+) {
+    fun checkBinariesAreCompatible() {
+        TODO()
+    }
+
+    companion object {
+        fun checkAllBinariesAreCompatible(
+            newLibraryAbis: Map<String, LibraryAbi>,
+            oldLibraryAbis: Map<String, LibraryAbi>
+        ) {
+            TODO()
+        }
+    }
+}
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
index f34133e..330676e 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/BundleInsideHelper.kt
@@ -21,7 +21,6 @@
 import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.Project
-import org.gradle.api.Task
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.attributes.Usage
 import org.gradle.api.file.FileTreeElement
@@ -29,12 +28,8 @@
 import org.gradle.api.tasks.Optional
 import org.gradle.api.tasks.TaskProvider
 import org.gradle.jvm.tasks.Jar
-import org.gradle.kotlin.dsl.findByType
 import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.named
-import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
-import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
 
 /** Allow java and Android libraries to bundle other projects inside the project jar/aar. */
 object BundleInsideHelper {
@@ -90,101 +85,6 @@
     }
 
     /**
-     * Creates a configuration for the users to use that will be used bundle these dependency jars
-     * inside of this project's jar.
-     *
-     * ```
-     * dependencies {
-     *   bundleInside(project(":foo"))
-     *   debugBundleInside(project(path: ":bar", configuration: "someDebugConfiguration"))
-     *   releaseBundleInside(project(path: ":bar", configuration: "someReleaseConfiguration"))
-     * }
-     * ```
-     *
-     * @param from specifies from which package the rename should happen
-     * @param to specifies to which package to put the renamed classes
-     * @param dropResourcesWithSuffix used to drop Java resources if they match this suffix, null
-     *   means no filtering
-     * @receiver the project that should bundle jars specified by these configurations
-     */
-    @JvmStatic
-    fun Project.forInsideJar(from: String, to: String, dropResourcesWithSuffix: String?) {
-        val bundle = createBundleConfiguration()
-        val repackage =
-            configureRepackageTaskForType(
-                relocations = listOf(Relocation(from, to)),
-                configuration = bundle,
-                dropResourcesWithSuffix = dropResourcesWithSuffix
-            )
-        dependencies.add("compileOnly", files(repackage.flatMap { it.archiveFile }))
-        dependencies.add("testImplementation", files(repackage.flatMap { it.archiveFile }))
-
-        val jarTask = tasks.named("jar")
-        jarTask.configure {
-            it as Jar
-            it.from(repackage.map { files(zipTree(it.archiveFile.get().asFile)) })
-        }
-        addArchivesToConfiguration("apiElements", jarTask)
-        addArchivesToConfiguration("runtimeElements", jarTask)
-    }
-
-    private fun Project.addArchivesToConfiguration(
-        configName: String,
-        jarTask: TaskProvider<Task>
-    ) {
-        configurations.getByName(configName) {
-            it.outgoing.artifacts.clear()
-            it.outgoing.artifact(
-                jarTask.flatMap { jarTask ->
-                    jarTask as Jar
-                    jarTask.archiveFile
-                }
-            )
-        }
-    }
-
-    /**
-     * KMP Version of [Project.forInsideJar]. See those docs for details.
-     *
-     * @param dropResourcesWithSuffix used to drop Java resources if they match this suffix,
-     *     * null means no filtering
-     *
-     * TODO(b/237104605): bundleInside is a global configuration. Should figure out how to make it
-     *   work properly with kmp and source sets so it can reside inside a sourceSet dependency.
-     */
-    @JvmStatic
-    fun Project.forInsideJarKmp(from: String, to: String, dropResourcesWithSuffix: String?) {
-        val kmpExtension =
-            extensions.findByType<KotlinMultiplatformExtension>() ?: error("kmp only")
-        val bundle = createBundleConfiguration()
-        val repackage =
-            configureRepackageTaskForType(
-                relocations = listOf(Relocation(from, to)),
-                configuration = bundle,
-                dropResourcesWithSuffix = dropResourcesWithSuffix
-            )
-
-        // To account for KMP structure we need to find the jvm specific target
-        // and add the repackaged archive files to only their compilations.
-        val jvmTarget =
-            kmpExtension.targets.firstOrNull { it.platformType == KotlinPlatformType.jvm }
-                as? KotlinJvmTarget ?: error("cannot find jvm target")
-        jvmTarget.compilations["main"].defaultSourceSet {
-            dependencies { compileOnly(files(repackage.flatMap { it.archiveFile })) }
-        }
-        jvmTarget.compilations["test"].defaultSourceSet {
-            dependencies { implementation(files(repackage.flatMap { it.archiveFile })) }
-        }
-        val jarTask = tasks.named(jvmTarget.artifactsTaskName)
-        jarTask.configure {
-            it as Jar
-            it.from(repackage.map { files(zipTree(it.archiveFile.get().asFile)) })
-        }
-        addArchivesToConfiguration("jvmApiElements", jarTask)
-        addArchivesToConfiguration("jvmRuntimeElements", jarTask)
-    }
-
-    /**
      * Creates a configuration for users to use that will bundle the dependency jars
      * inside of this lint check's jar. This is required because lintPublish does not currently
      * support dependencies, so instead we need to bundle any dependencies with the lint jar
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java
new file mode 100644
index 0000000..cb72a9f
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/StreamSharingForceEnabledEffect.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.testing.impl;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraEffect;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.SurfaceOutput;
+import androidx.camera.core.SurfaceProcessor;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.UseCaseGroup;
+import androidx.lifecycle.LifecycleOwner;
+
+/**
+ * An effect that is used to simulate the stream sharing is enabled automatically.
+ *
+ * <p>To simulate stream sharing is enabled automatically, create and add the effect to
+ * {@link UseCaseGroup.Builder#addEffect(CameraEffect)} and then bind UseCases via
+ * {@linkplain androidx.camera.lifecycle.ProcessCameraProvider#bindToLifecycle(
+ * LifecycleOwner, CameraSelector, UseCaseGroup)}.
+ *
+ * <p>To test stream sharing with real effects, use {@link CameraEffect} API instead.
+ */
+public class StreamSharingForceEnabledEffect extends CameraEffect {
+
+    public StreamSharingForceEnabledEffect() {
+        super(PREVIEW | VIDEO_CAPTURE, TRANSFORMATION_PASSTHROUGH, command -> {
+        }, new SurfaceProcessor() {
+            @Override
+            public void onInputSurface(@NonNull SurfaceRequest request) {
+                request.willNotProvideSurface();
+            }
+
+            @Override
+            public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
+                surfaceOutput.close();
+            }
+        }, t -> {
+        });
+    }
+}
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
index d499582..bda76f0 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/SupportedQualitiesVerificationTest.kt
@@ -42,12 +42,16 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.DynamicRange
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.utils.TransformUtils.rotateSize
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.impl.AndroidUtil.isEmulator
 import androidx.camera.testing.impl.CameraPipeConfigTestRule
 import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
+import androidx.camera.testing.impl.SurfaceTextureProvider
 import androidx.camera.testing.impl.WakelockEmptyActivityRule
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
 import androidx.camera.video.internal.compat.quirk.DeviceQuirks
@@ -62,8 +66,8 @@
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import org.junit.After
-import org.junit.Assume
 import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -150,10 +154,10 @@
 
     @Before
     fun setUp() {
-        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
 
         // Skip test for b/168175357
-        Assume.assumeFalse(
+        assumeFalse(
             "Cuttlefish has MediaCodec dequeueInput/Output buffer fails issue. Unable to test.",
             Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
         )
@@ -172,7 +176,7 @@
 
         // Ignore the unsupported Quality options
         val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo)
-        Assume.assumeTrue(
+        assumeTrue(
             "Camera ${cameraSelector.lensFacing} not support $quality, skip this test item.",
             videoCapabilities.isQualitySupported(quality, dynamicRange)
         )
@@ -194,10 +198,20 @@
     fun qualityOptionCanRecordVideo_enableSurfaceProcessing() {
         assumeSuccessfulSurfaceProcessing()
 
-        testQualityOptionRecordVideo(enableSurfaceProcessing = true)
+        testQualityOptionRecordVideo(forceEnableSurfaceProcessing = true)
     }
 
-    private fun testQualityOptionRecordVideo(enableSurfaceProcessing: Boolean = false) {
+    @Test
+    fun qualityOptionCanRecordVideo_enableStreamSharing() {
+        assumeSuccessfulSurfaceProcessing()
+
+        testQualityOptionRecordVideo(forceEnableStreamSharing = true)
+    }
+
+    private fun testQualityOptionRecordVideo(
+        forceEnableSurfaceProcessing: Boolean = false,
+        forceEnableStreamSharing: Boolean = false,
+    ) {
         // Skip for b/331618729
         assumeFalse(
             "Emulator API 28 crashes running this test.",
@@ -209,10 +223,12 @@
             videoCapabilities.getProfiles(quality, dynamicRange)!!.defaultVideoProfile
         val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(quality)).build()
         val videoCapture = VideoCapture.Builder(recorder).apply {
-            if (enableSurfaceProcessing) {
+            if (forceEnableSurfaceProcessing) {
                 setSurfaceProcessingForceEnabled()
             }
         }.build()
+        val preview = Preview.Builder().build()
+        assumeTrue(camera.isUseCasesCombinationSupported(preview, videoCapture))
         val file = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
         val latchForRecordingStatus = CountDownLatch(5)
         val latchForRecordingFinalized = CountDownLatch(1)
@@ -236,17 +252,29 @@
         }
 
         instrumentation.runOnMainSync {
+            preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
+            val useCaseGroup = UseCaseGroup.Builder().apply {
+                addUseCase(preview)
+                addUseCase(videoCapture)
+                if (forceEnableStreamSharing) {
+                    addEffect(StreamSharingForceEnabledEffect())
+                }
+            }.build()
             cameraProvider.bindToLifecycle(
                 lifecycleOwner,
                 cameraSelector,
-                videoCapture,
+                useCaseGroup
             )
         }
 
-        if (enableSurfaceProcessing) {
+        if (forceEnableSurfaceProcessing) {
             // Ensure the surface processing is enabled.
             assertThat(isSurfaceProcessingEnabled(videoCapture)).isTrue()
         }
+        if (forceEnableStreamSharing) {
+            // Ensure the stream sharing is enabled.
+            assertThat(isStreamSharingEnabled(videoCapture)).isTrue()
+        }
 
         // Act.
         videoCapture.startVideoRecording(file, eventListener).use {
@@ -261,11 +289,17 @@
         // Verify resolution.
         val resolutionToVerify = Size(videoProfile.width, videoProfile.height)
         val rotationDegrees = getRotationNeeded(videoCapture, cameraInfo)
+        // Skip verification when:
+        // * The device has extra cropping quirk. UseCase surface will be configured with a fixed
+        //   resolution regardless of the preference.
+        // * The device has size can not encode quirk as the final resolution will be modified.
+        // * Flexible quality settings such as using HIGHEST and LOWEST. This is because the
+        //   surface combination will affect the final resolution.
         if (!hasExtraCroppingQuirk(implName) && !hasSizeCannotEncodeVideoQuirk(
                 resolutionToVerify,
                 rotationDegrees,
                 isSurfaceProcessingEnabled(videoCapture)
-            )
+            ) && !isFlexibleQuality(quality)
         ) {
             verifyVideoResolution(
                 context,
@@ -278,6 +312,9 @@
         file.delete()
     }
 
+    private fun isFlexibleQuality(quality: Quality) =
+        quality == Quality.HIGHEST || quality == Quality.LOWEST
+
     private fun VideoCapture<Recorder>.startVideoRecording(
         file: File,
         eventListener: Consumer<VideoRecordEvent>
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 66b945f..7f0d5d9 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
@@ -40,6 +40,8 @@
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
 import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
@@ -53,6 +55,7 @@
 import androidx.camera.testing.impl.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
 import androidx.camera.testing.impl.CameraPipeConfigTestRule
 import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.StreamSharingForceEnabledEffect
 import androidx.camera.testing.impl.SurfaceTextureProvider
 import androidx.camera.testing.impl.WakelockEmptyActivityRule
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
@@ -62,6 +65,7 @@
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
 import androidx.core.util.Consumer
+import androidx.lifecycle.LifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -69,6 +73,7 @@
 import androidx.test.rule.GrantPermissionRule
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import com.google.common.util.concurrent.ListenableFuture
 import java.io.File
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
@@ -91,7 +96,8 @@
 class VideoRecordingTest(
     private val implName: String,
     private var cameraSelector: CameraSelector,
-    private val cameraConfig: CameraXConfig
+    private val cameraConfig: CameraXConfig,
+    private val forceEnableStreamSharing: Boolean,
 ) {
 
     @get:Rule
@@ -126,22 +132,38 @@
                 arrayOf(
                     "back+" + Camera2Config::class.simpleName,
                     CameraSelector.DEFAULT_BACK_CAMERA,
-                    Camera2Config.defaultConfig()
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/false,
                 ),
                 arrayOf(
                     "front+" + Camera2Config::class.simpleName,
                     CameraSelector.DEFAULT_FRONT_CAMERA,
-                    Camera2Config.defaultConfig()
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/false,
+                ),
+                arrayOf(
+                    "back+" + Camera2Config::class.simpleName + "+streamSharing",
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    Camera2Config.defaultConfig(),
+                    /*forceEnableStreamSharing=*/true,
                 ),
                 arrayOf(
                     "back+" + CameraPipeConfig::class.simpleName,
                     CameraSelector.DEFAULT_BACK_CAMERA,
-                    CameraPipeConfig.defaultConfig()
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/false,
                 ),
                 arrayOf(
                     "front+" + CameraPipeConfig::class.simpleName,
                     CameraSelector.DEFAULT_FRONT_CAMERA,
-                    CameraPipeConfig.defaultConfig()
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/false,
+                ),
+                arrayOf(
+                    "back+" + CameraPipeConfig::class.simpleName + "+streamSharing",
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                    /*forceEnableStreamSharing=*/true,
                 ),
             )
         }
@@ -151,7 +173,7 @@
     private val context: Context = ApplicationProvider.getApplicationContext()
     // TODO(b/278168212): Only SDR is checked by now. Need to extend to HDR dynamic ranges.
     private val dynamicRange = DynamicRange.SDR
-    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var cameraProvider: ProcessCameraProviderWrapper
     private lateinit var lifecycleOwner: FakeLifecycleOwner
     private lateinit var preview: Preview
     private lateinit var cameraInfo: CameraInfo
@@ -202,7 +224,8 @@
         skipVideoRecordingTestIfNotSupportedByEmulator()
 
         ProcessCameraProvider.configureInstance(cameraConfig)
-        cameraProvider = ProcessCameraProvider.getInstance(context).get()
+        cameraProvider =
+            ProcessCameraProviderWrapper(ProcessCameraProvider.getInstance(context).get())
         lifecycleOwner = FakeLifecycleOwner()
         lifecycleOwner.startAndResume()
 
@@ -1227,6 +1250,36 @@
         assumeExtraCroppingQuirk(implName)
     }
 
+    private inner class ProcessCameraProviderWrapper(val cameraProvider: ProcessCameraProvider) {
+
+        fun bindToLifecycle(
+            lifecycleOwner: LifecycleOwner,
+            cameraSelector: CameraSelector,
+            vararg useCases: UseCase
+        ): Camera {
+            if (useCases.isEmpty()) {
+                return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, *useCases)
+            }
+            val useCaseGroup = UseCaseGroup.Builder().apply {
+                useCases.forEach { useCase -> addUseCase(useCase) }
+                if (forceEnableStreamSharing) {
+                    addEffect(StreamSharingForceEnabledEffect())
+                }
+            }.build()
+            return cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCaseGroup)
+        }
+
+        fun unbind(vararg useCases: UseCase) {
+            cameraProvider.unbind(*useCases)
+        }
+
+        fun unbindAll() {
+            cameraProvider.unbindAll()
+        }
+
+        fun shutdownAsync(): ListenableFuture<Void> = cameraProvider.shutdownAsync()
+    }
+
     private class ImageSavedCallback :
         ImageCapture.OnImageSavedCallback {
 
diff --git a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
index 34493db..fa4a65e 100644
--- a/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
+++ b/compose/foundation/foundation/integration-tests/lazy-tests/src/androidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
@@ -17,10 +17,11 @@
 package androidx.compose.foundation.lazy.staggeredgrid
 
 import androidx.compose.foundation.AutoTestFrameClock
-import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.scrollBy
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
@@ -34,11 +35,11 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
-@OptIn(ExperimentalFoundationApi::class)
 @MediumTest
 @RunWith(Parameterized::class)
 class LazyStaggeredGridScrollTest(
@@ -59,20 +60,23 @@
     private var itemSizeDp = Dp.Unspecified
     private val itemCount = 100
 
+    @Before
+    fun initSizes() {
+        itemSizeDp = with(rule.density) {
+            itemSizePx.toDp()
+        }
+    }
+
     fun setContent(
         containerSizePx: Int = itemSizePx * 5,
         afterContentPaddingPx: Int = 0
     ) {
-        itemSizeDp = with(rule.density) {
-            itemSizePx.toDp()
-        }
         rule.setContent {
             state = rememberLazyStaggeredGridState()
             with(rule.density) {
                 TestContent(containerSizePx.toDp(), afterContentPaddingPx.toDp())
             }
         }
-        rule.waitForIdle()
     }
 
     @Test
@@ -353,6 +357,41 @@
         }
     }
 
+    @Test
+    fun scrollBy_emptyItemWithSpacing() {
+        val state = LazyStaggeredGridState()
+        val spacingDp = with(rule.density) { 10.toDp() }
+        rule.setContent {
+            LazyStaggeredGrid(
+                lanes = 1,
+                state = state,
+                mainAxisSpacing = spacingDp,
+                modifier = Modifier.size(itemSizeDp),
+            ) {
+                item {
+                    Box(Modifier.size(itemSizeDp).testTag("0").debugBorder())
+                }
+                item { } // empty item surrounded by spacings
+                item {
+                    Box(Modifier.size(itemSizeDp).testTag("2").debugBorder())
+                }
+            }
+        }
+        rule.runOnIdle {
+            runBlocking(AutoTestFrameClock()) {
+                // empty item introduces two spacings 10 pixels each
+                // so after this operation item 2 is right at the edge of the viewport
+                state.scrollBy(20f)
+                // then we do some extra scrolling to make item 2 visible
+                state.scrollBy(20f)
+            }
+        }
+        rule.onNodeWithTag("2")
+            .assertMainAxisStartPositionInRootIsEqualTo(
+                itemSizeDp - with(rule.density) { 20.toDp() }
+            )
+    }
+
     @Composable
     private fun TestContent(containerSizeDp: Dp, afterContentPaddingDp: Dp) {
         // |-|-|
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
index d1097a6..4d0efc2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/AndroidExternalSurfaceTest.kt
@@ -273,6 +273,13 @@
     }
 
     @Test
+    @Ignore(
+        """Despite best efforts in screenshotToImage(), this test is too flaky currently.
+            |Since this test only tests that the `zOrder` parameter is properly passed
+            |to the underlying SurfaceView, we don't lose much by disabling it.
+            |This test should be more robust on API level 34 using the window
+            |screenshot API."""
+    )
     fun testZOrderDefault() {
         val latch = CountDownLatch(FrameCount)
 
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
index 7a75d11..a52b748 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/ClickableTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredWidth
@@ -106,6 +107,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.reflect.KClass
 import kotlin.test.assertFailsWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.junit.After
@@ -1335,26 +1338,52 @@
     private fun Modifier.dynamicPointerInputModifier(
         enabled: Boolean,
         key: Any? = Unit,
-        onPress: () -> Unit = { },
+        onEnter: () -> Unit = { },
         onMove: () -> Unit = { },
+        onPress: () -> Unit = { },
         onRelease: () -> Unit = { },
-    ) = if (enabled) {
+        onExit: () -> Unit = { },
+        ) = if (enabled) {
         pointerInput(key) {
             awaitPointerEventScope {
                 while (true) {
                     val event = awaitPointerEvent()
-                    if (event.type == PointerEventType.Press) {
-                        onPress()
-                    } else if (event.type == PointerEventType.Move) {
-                        onMove()
-                    } else if (event.type == PointerEventType.Release) {
-                        onRelease()
+                    when (event.type) {
+                        PointerEventType.Enter -> {
+                            onEnter()
+                        }
+                        PointerEventType.Press -> {
+                            onPress()
+                        }
+                        PointerEventType.Move -> {
+                            onMove()
+                        }
+                        PointerEventType.Release -> {
+                            onRelease()
+                        }
+                        PointerEventType.Exit -> {
+                            onExit()
+                        }
                     }
                 }
             }
         }
     } else this
 
+    private fun Modifier.dynamicPointerInputModifierWithDetectTapGestures(
+        enabled: Boolean,
+        key: Any? = Unit,
+        onTap: () -> Unit = { }
+    ) = if (enabled) {
+        pointerInput(key) {
+            detectTapGestures {
+                onTap()
+            }
+        }
+    } else {
+        this
+    }
+
     private fun Modifier.dynamicClickableModifier(
         enabled: Boolean,
         onClick: () -> Unit
@@ -1365,8 +1394,20 @@
         ) { onClick() }
     } else this
 
+    // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+    // The next ~20 tests test enabling/disabling dynamic input modifiers (both pointer input and
+    // clickable) using various combinations (touch vs. mouse, Unit vs. unique keys, nested UI
+    // elements vs. all modifiers on one UI element, etc.)
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicClickableModifierTouch_addsAbovePointerInputWithKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithKeyTouchEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1410,8 +1451,15 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicClickableModifierTouch_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyTouchEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1455,9 +1503,18 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
+     */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicClickableModifierMouse_addsAbovePointerInputWithKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithKeyMouseEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1509,9 +1566,18 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for the non-dynamic
+     * pointer input and clickable{} for the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
+     */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicClickableModifierMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicClickableModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
         // This is part of a dynamic modifier
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
@@ -1563,8 +1629,16 @@
         }
     }
 
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicInputModifierTouch_addsAboveClickableWithKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithKeyTouchEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1603,8 +1677,16 @@
         }
     }
 
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
     @Test
-    fun dynamicInputModifierTouch_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithUnitKeyTouchEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1642,9 +1724,22 @@
         }
     }
 
-    // Tests a dynamic pointer input AND a dynamic clickable{} above an existing pointer input.
+    /* Uses pointer input block for the non-dynamic pointer input and BOTH a clickable{} and
+     * pointer input block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer
+     * inputs (both on same Box).
+     * Both the dynamic Pointer and clickable{} are disabled to start and then enabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     * 3. Touch down
+     * 4. Assert
+     * 5. Touch move
+     * 6. Assert
+     * 7. Touch up
+     * 8. Assert
+     */
     @Test
-    fun dynamicInputModifiersInTouchStream_addsAboveClickableWithUnitKey_triggersAllModifiers() {
+    fun dynamicInputAndClickableModifier_addsAbovePointerInputWithUnitKeyTouchEventsWithMove() {
         var activeDynamicClickable by mutableStateOf(false)
         var dynamicClickableCounter by mutableStateOf(0)
 
@@ -1676,6 +1771,10 @@
                 .dynamicClickableModifier(activeDynamicClickable) {
                     dynamicClickableCounter++
                 }
+                // Note the .background() above the static pointer input block
+                // TODO (jjw): Remove once bug fixed for when a dynamic pointer input follows
+                // directly after another pointer input (both using Unit key).
+                // Workaround: add a modifier between them OR use unique keys (that is, not Unit)
                 .background(Color.Green)
                 .pointerInput(Unit) {
                     originalPointerInputLambdaExecutionCount++
@@ -1768,13 +1867,19 @@
         }
     }
 
-    /*
-     * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
-     * using a hardware device with an Android device.
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierMouse_addsAboveClickableWithKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithKeyMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1820,13 +1925,19 @@
         }
     }
 
-    /*
-     * Tests adding dynamic modifier with COMPLETE mouse events, that is, the expected events from
-     * using a hardware device with an Android device.
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Mouse "click()" (press/release)
+     * 3. Mouse exit
+     * 4. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierMouse_addsAboveClickableWithUnitKey_triggersInBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableWithUnitKeyMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1871,15 +1982,26 @@
         }
     }
 
-    /* Tests dynamically adding a pointer input DURING an event stream (specifically, Hover).
-     * Hover is the only scenario where you can add a new pointer input modifier during the event
-     * stream AND receive events in the same active stream from that new pointer input modifier.
-     * It isn't possible in the down/up scenario because you add the new modifier during the down
-     * but you don't get another down until the next event stream.
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     *
+     * Tests dynamically adding a pointer input ABOVE an existing pointer input DURING an
+     * event stream (specifically, Hover).
+     *
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Assert
+     * 3. Mouse press
+     * 4. Assert
+     * 5. Mouse release
+     * 6. Assert
+     * 7. Mouse exit
+     * 8. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierHoverMouse_addsAbovePointerInputWithUnitKey_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyMouseEvents_correctEvents() {
         var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
         var originalPointerInputEventCounter by mutableStateOf(0)
 
@@ -1959,14 +2081,17 @@
         }
     }
 
-    /* This is the same as the test above, but
-     *   1. Using clickable{}
-     *   2. It enables the dynamic pointer input and starts the hover event stream in a more
-     * hacky way (using mouse click without hover which triggers hover enter on release).
+    /* Uses clickable{} for the non-dynamic pointer input and pointer input
+     * block (awaitPointerEventScope + awaitPointerEvent) for the dynamic pointer input (both
+     * on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
      */
     @OptIn(ExperimentalTestApi::class)
     @Test
-    fun dynamicInputModifierIncompleteMouse_addsAboveClickableHackyEvents_triggersBothModifiers() {
+    fun dynamicInputModifier_addsAboveClickableIncompleteMouseEvents_correctEvents() {
         var clickableClickCounter by mutableStateOf(0)
         // Note: I'm tracking press instead of release because clickable{} consumes release
         var dynamicPressCounter by mutableStateOf(0)
@@ -1989,16 +2114,6 @@
             )
         }
 
-        // Usually, a proper event stream from hardware for mouse input would be:
-        // - enter() (hover enter)
-        // - click()
-        // - exit()
-        // However, in this case, I'm just calling click() which triggers actions:
-        // - press
-        // - release
-        // - hover enter
-        // This starts a hover event stream (in a more hacky way) and also enables the dynamic
-        // pointer input to start recording events.
         rule.onNodeWithTag("myClickable").performMouseInput {
             click()
         }
@@ -2018,6 +2133,1749 @@
         }
     }
 
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     *
+     * Tests dynamically adding a pointer input AFTER an existing pointer input DURING an
+     * event stream (specifically, Hover).
+     * Hover is the only scenario where you can add a new pointer input modifier during the event
+     * stream AND receive events in the same active stream from that new pointer input modifier.
+     * It isn't possible in the down/up scenario because you add the new modifier during the down
+     * but you don't get another down until the next event stream.
+     *
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse enter
+     * 2. Assert
+     * 3. Mouse press
+     * 4. Assert
+     * 5. Mouse release
+     * 6. Assert
+     * 7. Mouse exit
+     * 8. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEventCounter by mutableStateOf(0)
+
+        var dynamicPressCounter by mutableStateOf(0)
+        var dynamicReleaseCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .background(Color.Green)
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            awaitPointerEvent()
+                            originalPointerInputEventCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                }
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onPress = {
+                        dynamicPressCounter++
+                    },
+                    onRelease = {
+                        dynamicReleaseCounter++
+                    }
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEventCounter)
+            assertEquals(0, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            press()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            // Both the original and enabled dynamic pointer input modifiers will get the event
+            // since they are on the same Box.
+            assertEquals(2, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            assertTrue(activateDynamicPointerInput)
+            release()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(4, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+    }
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input (both on same Box).
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEventCounter by mutableStateOf(0)
+
+        var dynamicPressCounter by mutableStateOf(0)
+        var dynamicReleaseCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .background(Color.Green)
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            awaitPointerEvent()
+                            originalPointerInputEventCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                }
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onPress = {
+                        dynamicPressCounter++
+                    },
+                    onRelease = {
+                        dynamicReleaseCounter++
+                    }
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter) // Enter, Press, Release
+            assertEquals(0, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            // Because the mouse is still within the box area, Compose doesn't need to trigger an
+            // Exit. Instead, it just triggers two events (Press and Release) which is why the
+            // total is only 5.
+            assertEquals(5, originalPointerInputEventCounter) // Press, Release
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+    }
+
+    /* The next set of tests uses two nested boxes inside a box. The two nested boxes each contain
+     * their own pointer input modifier (vs. the tests above that apply two pointer input modifiers
+     * to the same box).
+     */
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input.
+     * The Dynamic Pointer is disabled to start and then enabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEventCounter by mutableStateOf(0)
+
+        var dynamicPressCounter by mutableStateOf(0)
+        var dynamicReleaseCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                awaitPointerEvent()
+                                originalPointerInputEventCounter++
+                                activateDynamicPointerInput = true
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onPress = {
+                            dynamicPressCounter++
+                        },
+                        onRelease = {
+                            dynamicReleaseCounter++
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter)
+            assertEquals(0, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+    }
+
+    /* Uses pointer input block (awaitPointerEventScope + awaitPointerEvent) for both the
+     * non-dynamic pointer input and the dynamic pointer input.
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_togglesBelowPointerInputUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEventCounter by mutableStateOf(0)
+
+        var dynamicPressCounter by mutableStateOf(0)
+        var dynamicReleaseCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                originalPointerInputEventCounter++
+
+                                // Note: We only set the activateDynamicPointerInput to true on
+                                // Release because we do not want it set on just any event.
+                                // Specifically, we do not want it set on Exit, because, in the
+                                // case of this event, the exit will be triggered around the same
+                                // time as the other dynamic pointer input receives a press (when
+                                // it is enabled) because, as soon as that gets that event, Compose
+                                // sees this box no longer the hit target (the box with the dynamic
+                                // pointer input is now), so it triggers an exit on this original
+                                // non-dynamic pointer input. If we allowed
+                                // activateDynamicPointerInput to be set during any event, it would
+                                // undo us setting activateDynamicPointerInput to false in the other
+                                // pointer input handler.
+                                if (event.type == PointerEventType.Release) {
+                                    activateDynamicPointerInput = true
+                                }
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Cyan)
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onPress = {
+                            dynamicPressCounter++
+                        },
+                        onRelease = {
+                            dynamicReleaseCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter)
+            assertEquals(0, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(3, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+    }
+
+    /* Uses Foundation's detectTapGestures{} for the non-dynamic pointer input. The dynamic pointer
+     * input uses the lower level pointer input commands.
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowWithUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEventCounter by mutableStateOf(0)
+
+        var dynamicPressCounter by mutableStateOf(0)
+        var dynamicReleaseCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        detectTapGestures {
+                            originalPointerInputEventCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onPress = {
+                            dynamicPressCounter++
+                        },
+                        onRelease = {
+                            dynamicReleaseCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEventCounter)
+            assertEquals(0, dynamicPressCounter)
+            assertEquals(0, dynamicReleaseCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            click()
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEventCounter)
+            assertEquals(1, dynamicPressCounter)
+            assertEquals(1, dynamicReleaseCounter)
+        }
+    }
+
+    /* Uses Foundation's detectTapGestures{} for both the non-dynamic pointer input and the
+     * dynamic pointer input (vs. the lower level pointer input commands).
+     * The Dynamic Pointer is disabled to start, then enabled, and finally disabled.
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowWithKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        // This command is the same as
+        // rule.onNodeWithTag("myClickable").performTouchInput { click() }
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertEquals(0, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /*
+     * The next four tests are based on the test above (nested boxes using a pointer input
+     * modifier blocks with the Foundation Gesture detectTapGestures{}).
+     *
+     * The difference is the dynamic pointer input modifier is enabled to start (while in the
+     * other tests it is disabled to start).
+     *
+     * The tests below tests out variations (mouse vs. touch and Unit keys vs. unique keys).
+     */
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIT for key)
+     * Event sequences:
+     * 1. Touch "click" (down/move/up)
+     * 2. Assert
+     */
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithUnitKeyTouchEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = Unit,
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performClick()
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses UNIQUE key)
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffWithKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("myUniqueKey1") {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = "myUniqueKey2",
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+
+    /* Dynamic Pointer enabled to start, disabled, then re-enabled (uses Unit for key)
+     * Event sequences:
+     * 1. Mouse "click" (incomplete [down/up only], does not include expected hover in/out)
+     * 2. Assert
+     */
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBoxGesture_togglesBelowOffUnitKeyIncompleteMouseEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputTapGestureCounter by mutableStateOf(0)
+
+        var dynamicTapGestureCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(true)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+
+                        detectTapGestures {
+                            originalPointerInputTapGestureCounter++
+                            activateDynamicPointerInput = true
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifierWithDetectTapGestures(
+                        enabled = activateDynamicPointerInput,
+                        key = Unit,
+                        onTap = {
+                            dynamicTapGestureCounter++
+                            activateDynamicPointerInput = false
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertTrue(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+            assertEquals(0, originalPointerInputLambdaExecutionCount)
+            assertEquals(0, originalPointerInputTapGestureCounter)
+            // Since second box is created following first box, it will get the event
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput { click() }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputTapGestureCounter)
+            assertTrue(activateDynamicPointerInput)
+            assertEquals(1, dynamicTapGestureCounter)
+        }
+    }
+    // !!!!! MOUSE & TOUCH EVENTS TESTS WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
+    // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (START) !!!!!
+    /* These tests dynamically add a pointer input BEFORE or AFTER an existing pointer input DURING
+     * an event stream (specifically, Hover). Some tests use unique keys while others use UNIT as
+     * the key. Finally, some of the tests apply the modifiers to the same Box while others use
+     * sibling blocks (read the test name for details).
+     *
+     * Test name explains the test.
+     * All tests start with the dynamic pointer disabled and enable it on the first hover enter
+     *
+     * Event sequences:
+     * 1. Hover enter
+     * 2. Assert
+     * 3. Move
+     * 4. Assert
+     * 5. Move
+     * 6. Assert
+     * 7. Hover exit
+     * 8. Assert
+     */
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsAbovePointerInputWithUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+                .background(Color.Green)
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Hover Exit event then a Hover Enter event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(2, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsAbovePointerInputWithKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .dynamicPointerInputModifier(
+                    key = "unique_key_1234",
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+                .background(Color.Green)
+                .pointerInput("unique_key_5678") {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BEFORE the original modifier, it WILL reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Hover Exit event then a Hover Enter event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(2, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(2, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputWithUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .pointerInput(Unit) {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+                .background(Color.Green)
+                .dynamicPointerInputModifier(
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Move event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputModifier_addsBelowPointerInputWithKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier
+                .size(200.dp)
+                .testTag("myClickable")
+                .pointerInput("unique_key_5678") {
+                    originalPointerInputLambdaExecutionCount++
+                    awaitPointerEventScope {
+                        while (true) {
+                            val event = awaitPointerEvent()
+                            when (event.type) {
+                                PointerEventType.Enter -> {
+                                    originalPointerInputEnterEventCounter++
+                                    activateDynamicPointerInput = true
+                                }
+                                PointerEventType.Move -> {
+                                    originalPointerInputMoveEventCounter++
+                                }
+                                PointerEventType.Exit -> {
+                                    originalPointerInputExitEventCounter++
+                                }
+                            }
+                        }
+                    }
+                }
+                .background(Color.Green)
+                .dynamicPointerInputModifier(
+                    key = "unique_key_1234",
+                    enabled = activateDynamicPointerInput,
+                    onEnter = {
+                        dynamicEnterCounter++
+                    },
+                    onMove = {
+                        dynamicMoveCounter++
+                    },
+                    onExit = {
+                        dynamicExitCounter++
+                    }
+                )
+            )
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        // Because the dynamic pointer input is added after the "enter" event (and it is part of the
+        // same modifier chain), it will receive events as well now.
+        // (Because the dynamic modifier is added BELOW the original modifier, it will not reset the
+        // event stream for the original modifier.)
+        // Original pointer input:  Move event
+        // Dynamic pointer input:   Hover enter event
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsBelowPointerInputUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsBelowPointerInputKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("unique_key_1234") {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        key = "unique_key_5678",
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(1, dynamicEnterCounter)
+            assertEquals(1, dynamicMoveCounter)
+            assertEquals(1, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsAbovePointerInputUnitKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput(Unit) {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun dynamicInputNestedBox_addsAbovePointerInputKeyHoverEvents_correctEvents() {
+        var originalPointerInputLambdaExecutionCount by mutableStateOf(0)
+        var originalPointerInputEnterEventCounter by mutableStateOf(0)
+        var originalPointerInputMoveEventCounter by mutableStateOf(0)
+        var originalPointerInputExitEventCounter by mutableStateOf(0)
+
+        var dynamicEnterCounter by mutableStateOf(0)
+        var dynamicMoveCounter by mutableStateOf(0)
+        var dynamicExitCounter by mutableStateOf(0)
+        var activateDynamicPointerInput by mutableStateOf(false)
+
+        rule.setContent {
+            Box(Modifier.size(100.dp).testTag("myClickable")) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .dynamicPointerInputModifier(
+                        key = "unique_key_5678",
+                        enabled = activateDynamicPointerInput,
+                        onEnter = {
+                            dynamicEnterCounter++
+                        },
+                        onMove = {
+                            dynamicMoveCounter++
+                        },
+                        onExit = {
+                            dynamicExitCounter++
+                        }
+                    )
+                )
+                Box(Modifier
+                    .fillMaxSize()
+                    .background(Color.Green)
+                    .pointerInput("unique_key_1234") {
+                        originalPointerInputLambdaExecutionCount++
+                        awaitPointerEventScope {
+                            while (true) {
+                                val event = awaitPointerEvent()
+                                when (event.type) {
+                                    PointerEventType.Enter -> {
+                                        originalPointerInputEnterEventCounter++
+                                        activateDynamicPointerInput = true
+                                    }
+                                    PointerEventType.Move -> {
+                                        originalPointerInputMoveEventCounter++
+                                    }
+                                    PointerEventType.Exit -> {
+                                        originalPointerInputExitEventCounter++
+                                    }
+                                }
+                            }
+                        }
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            assertFalse(activateDynamicPointerInput)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            enter()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(0, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(1, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            moveBy(Offset(1.0f, 1.0f))
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(0, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+
+        rule.onNodeWithTag("myClickable").performMouseInput {
+            exit()
+        }
+
+        rule.runOnIdle {
+            assertEquals(1, originalPointerInputLambdaExecutionCount)
+            assertEquals(1, originalPointerInputEnterEventCounter)
+            assertEquals(2, originalPointerInputMoveEventCounter)
+            assertEquals(1, originalPointerInputExitEventCounter)
+            assertEquals(0, dynamicEnterCounter)
+            assertEquals(0, dynamicMoveCounter)
+            assertEquals(0, dynamicExitCounter)
+        }
+    }
+    // !!!!! HOVER EVENTS ONLY WITH DYNAMIC MODIFIER INPUT TESTS SECTION (END) !!!!!
+
     @OptIn(ExperimentalTestApi::class)
     @Test
     @LargeTest
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
index a173669..0b3760d 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/contextmenu/ContextMenuCommon.kt
@@ -59,7 +59,7 @@
     onClick: () -> Unit = {},
 ) {
     item(
-        label = label,
+        label = { label },
         modifier = modifier,
         enabled = enabled,
         leadingIcon = leadingIcon,
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
new file mode 100644
index 0000000..d0e5b20
--- /dev/null
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/CoreTextFieldHandwritingBoundsTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text
+
+import android.view.inputmethod.CursorAnchorInfo
+import android.view.inputmethod.ExtractedText
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.setFocusableContent
+import androidx.compose.foundation.text.handwriting.HandwritingBoundsVerticalOffset
+import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.input.InputMethodInterceptor
+import androidx.compose.foundation.text.input.internal.InputMethodManager
+import androidx.compose.foundation.text.input.internal.inputMethodManagerFactory
+import androidx.compose.foundation.text.matchers.isZero
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CoreTextFieldHandwritingBoundsTest {
+    @get:Rule
+    val rule = createComposeRule()
+    private val inputMethodInterceptor = InputMethodInterceptor(rule)
+
+    private val fakeImm = object : InputMethodManager {
+        private var stylusHandwritingStartCount = 0
+
+        fun expectStylusHandwriting(started: Boolean) {
+            if (started) {
+                assertThat(stylusHandwritingStartCount).isEqualTo(1)
+                stylusHandwritingStartCount = 0
+            } else {
+                assertThat(stylusHandwritingStartCount).isZero()
+            }
+        }
+
+        override fun isActive(): Boolean = true
+
+        override fun restartInput() {}
+
+        override fun showSoftInput() {}
+
+        override fun hideSoftInput() {}
+
+        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {}
+
+        override fun updateSelection(
+            selectionStart: Int,
+            selectionEnd: Int,
+            compositionStart: Int,
+            compositionEnd: Int
+        ) {}
+
+        override fun updateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {}
+
+        override fun startStylusHandwriting() {
+            ++stylusHandwritingStartCount
+        }
+    }
+
+    @Before
+    fun setup() {
+        // Test is only meaningful when stylusHandwriting is supported.
+        assumeTrue(isStylusHandwritingSupported)
+    }
+
+    @Test
+    fun coreTextField_stylusPointerInEditorBounds_focusAndStartHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        val editorTag1 = "CoreTextField1"
+        val editorTag2 = "CoreTextField2"
+
+        setContent {
+            Column(Modifier.safeContentPadding()) {
+                EditLine(Modifier.testTag(editorTag1))
+                EditLine(Modifier.testTag(editorTag2))
+            }
+        }
+
+        rule.onNodeWithTag(editorTag1).performStylusHandwriting()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(editorTag1).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+    }
+
+    @Test
+    fun coreTextField_stylusPointerInOverlappingArea_focusedEditorStartHandwriting() {
+        inputMethodManagerFactory = { fakeImm }
+
+        val editorTag1 = "CoreTextField1"
+        val editorTag2 = "CoreTextField2"
+        val spacerTag = "Spacer"
+
+        setContent {
+            Column(Modifier.safeContentPadding()) {
+                EditLine(Modifier.testTag(editorTag1))
+                Spacer(
+                    modifier = Modifier.fillMaxWidth()
+                        .height(HandwritingBoundsVerticalOffset)
+                        .testTag(spacerTag)
+                )
+                EditLine(Modifier.testTag(editorTag2))
+            }
+        }
+
+        rule.onNodeWithTag(editorTag2).requestFocus()
+        rule.waitForIdle()
+
+        // Spacer's height equals to HandwritingBoundsVerticalPadding, both editor will receive the
+        // event.
+        rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+        rule.waitForIdle()
+
+        // Assert that focus didn't change, handwriting is started on the focused editor 2.
+        rule.onNodeWithTag(editorTag2).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+
+        rule.onNodeWithTag(editorTag1).requestFocus()
+        rule.onNodeWithTag(spacerTag).performStylusHandwriting()
+        rule.waitForIdle()
+
+        // Now handwriting is performed on the focused editor 1.
+        rule.onNodeWithTag(editorTag1).assertIsFocused()
+        fakeImm.expectStylusHandwriting(true)
+    }
+
+    @Composable
+    fun EditLine(modifier: Modifier = Modifier) {
+        var value by remember { mutableStateOf(TextFieldValue()) }
+        CoreTextField(
+            value = value,
+            onValueChange = { value = it },
+            modifier = modifier
+                .fillMaxWidth()
+                // make the size of TextFields equal to padding, so that touch bounds of editors
+                // in the same column/row are overlapping.
+                .height(HandwritingBoundsVerticalOffset)
+        )
+    }
+
+    private fun setContent(
+        extraItemForInitialFocus: Boolean = true,
+        content: @Composable () -> Unit
+    ) {
+        rule.setFocusableContent(extraItemForInitialFocus) {
+            inputMethodInterceptor.Content {
+                content()
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
index 966c702..5888282 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/HandwritingTestUtils.kt
@@ -101,11 +101,6 @@
     }
 
     private fun sendTouchEvent(action: Int) {
-        val positionInScreen = run {
-            val array = intArrayOf(0, 0)
-            root.view.getLocationOnScreen(array)
-            Offset(array[0].toFloat(), array[1].toFloat())
-        }
         val motionEvent = MotionEvent.obtain(
             /* downTime = */ downTime,
             /* eventTime = */ currentTime,
@@ -125,13 +120,13 @@
                     // test if it handles them properly (versus breaking here and we not knowing
                     // if Compose properly handles these values).
                     x = if (startOffset.isValid()) {
-                        positionInScreen.x + startOffset.x
+                        startOffset.x
                     } else {
                         Float.NaN
                     }
 
                     y = if (startOffset.isValid()) {
-                        positionInScreen.y + startOffset.y
+                        startOffset.y
                     } else {
                         Float.NaN
                     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
index 79686c2..3de2da2 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/HandwritingDetectorTest.kt
@@ -16,8 +16,12 @@
 
 package androidx.compose.foundation.text.input
 
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.foundation.text.handwriting.HandwritingBoundsVerticalOffset
 import androidx.compose.foundation.text.handwriting.handwritingDetector
 import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.performStylusClick
@@ -48,7 +52,9 @@
 
     private val imm = FakeInputMethodManager()
 
-    private val tag = "detector"
+    private val detectorTag = "detector"
+    private val insideSpacerTag = "inside"
+    private val outsideSpacerTag = "outside"
 
     private var callbackCount = 0
 
@@ -62,39 +68,72 @@
         callbackCount = 0
 
         rule.setContent {
-            Spacer(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .handwritingDetector { callbackCount++ }
-                    .testTag(tag)
-            )
+            Column(Modifier.safeContentPadding()) {
+                Spacer(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(HandwritingBoundsVerticalOffset)
+                        .handwritingDetector { callbackCount++ }
+                        .testTag(detectorTag)
+                )
+                // This spacer is within the extended handwriting bounds of the detector
+                Spacer(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(HandwritingBoundsVerticalOffset)
+                        .testTag(insideSpacerTag)
+                )
+                // This spacer is outside the extended handwriting bounds of the detector
+                Spacer(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(HandwritingBoundsVerticalOffset)
+                        .testTag(outsideSpacerTag)
+                )
+            }
         }
     }
 
     @Test
     fun detector_handwriting_preparesDelegation() {
-        rule.onNodeWithTag(tag).performStylusHandwriting()
+        rule.onNodeWithTag(detectorTag).performStylusHandwriting()
 
         assertHandwritingDelegationPrepared()
     }
 
     @Test
+    fun detector_handwritingInExtendedBounds_preparesDelegation() {
+        // This spacer is within the extended handwriting bounds of the detector
+        rule.onNodeWithTag(insideSpacerTag).performStylusHandwriting()
+
+        assertHandwritingDelegationPrepared()
+    }
+
+    @Test
+    fun detector_handwritingOutsideExtendedBounds_notPreparesDelegation() {
+        // This spacer is outside the extended handwriting bounds of the detector
+        rule.onNodeWithTag(outsideSpacerTag).performStylusHandwriting()
+
+        assertHandwritingDelegationNotPrepared()
+    }
+
+    @Test
     fun detector_click_notPreparesDelegation() {
-        rule.onNodeWithTag(tag).performStylusClick()
+        rule.onNodeWithTag(detectorTag).performStylusClick()
 
         assertHandwritingDelegationNotPrepared()
     }
 
     @Test
     fun detector_longClick_notPreparesDelegation() {
-        rule.onNodeWithTag(tag).performStylusLongClick()
+        rule.onNodeWithTag(detectorTag).performStylusLongClick()
 
         assertHandwritingDelegationNotPrepared()
     }
 
     @Test
     fun detector_longPressAndDrag_notPreparesDelegation() {
-        rule.onNodeWithTag(tag).performStylusLongPressAndDrag()
+        rule.onNodeWithTag(detectorTag).performStylusLongPressAndDrag()
 
         assertHandwritingDelegationNotPrepared()
     }
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
index 9495032..cd782ed 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldLongPressTest.kt
@@ -56,6 +56,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.junit.Rule
@@ -392,6 +393,32 @@
         assertThat(state.selection).isEqualTo(TextRange(4, 7))
     }
 
+    @Test
+    fun longPress_startingFromEndPadding_draggingUp_selectsFromLastWord_ltr() {
+        val state = TextFieldState("abc def\nghi jkl\nmno pqr")
+        rule.setTextFieldTestContent {
+            BasicTextField(
+                state = state,
+                textStyle = TextStyle(),
+                modifier = Modifier
+                    .testTag(TAG)
+                    .width(200.dp)
+            )
+        }
+
+        rule.onNodeWithTag(TAG).performTouchInput {
+            longPress(bottomRight)
+            repeat((bottomRight - topRight).y.roundToInt()) {
+                moveBy(Offset(0f, -1f))
+            }
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.selection).isEqualTo(TextRange(4, 23))
+        }
+    }
+
     //region RTL
 
     @Test
@@ -499,6 +526,34 @@
     }
 
     @Test
+    fun longPress_startingFromEndPadding_draggingUp_selectsFromLastWord_rtl() {
+        val state = TextFieldState("$rtlText2\n$rtlText2\n$rtlText2")
+        rule.setTextFieldTestContent {
+            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                BasicTextField(
+                    state = state,
+                    textStyle = TextStyle(),
+                    modifier = Modifier
+                        .testTag(TAG)
+                        .width(200.dp)
+                )
+            }
+        }
+
+        rule.onNodeWithTag(TAG).performTouchInput {
+            longPress(bottomLeft)
+            repeat((bottomLeft - topLeft).y.roundToInt()) {
+                moveBy(Offset(0f, -1f))
+            }
+            up()
+        }
+
+        rule.runOnIdle {
+            assertThat(state.selection).isEqualTo(TextRange(4, 23))
+        }
+    }
+
+    @Test
     fun longPress_startDraggingToScrollRight_startHandleDoesNotShow_ltr() {
         val state = TextFieldState("abc def ghi ".repeat(10))
         rule.setTextFieldTestContent {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
index d11fe39..56a5472 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/contextmenu/ContextMenuUi.android.kt
@@ -254,7 +254,7 @@
      * Returns whether or not the context menu should be dismissed.
      */
     fun item(
-        label: String,
+        label: @Composable () -> String,
         modifier: Modifier = Modifier,
         enabled: Boolean = true,
         /**
@@ -272,11 +272,12 @@
          */
         onClick: () -> Unit,
     ) {
-        check(label.isNotBlank()) { "Label must not be blank" }
         composables += { colors ->
+            val resolvedLabel = label()
+            check(resolvedLabel.isNotBlank()) { "Label must not be blank" }
             ContextMenuItem(
                 modifier = modifier,
-                label = label,
+                label = resolvedLabel,
                 enabled = enabled,
                 colors = colors,
                 leadingIcon = leadingIcon,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
index 0425d17..8c17b8a 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/ContextMenu.android.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.foundation.contextmenu.ContextMenuScope
 import androidx.compose.foundation.contextmenu.ContextMenuState
 import androidx.compose.foundation.contextmenu.close
 import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
@@ -88,3 +89,15 @@
     @Composable
     fun resolvedString(): String = stringResource(stringId)
 }
+
+internal inline fun ContextMenuScope.TextItem(
+    state: ContextMenuState,
+    label: TextContextMenuItems,
+    enabled: Boolean,
+    crossinline operation: () -> Unit
+) {
+    item(label = { label.resolvedString() }, enabled = enabled) {
+        operation()
+        state.close()
+    }
+}
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
index 6b69713..ed44b5e 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/handwriting/HandwritingDetector.android.kt
@@ -17,11 +17,11 @@
 package androidx.compose.foundation.text.handwriting
 
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.text.input.internal.ComposeInputMethodManager
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.PointerInputModifierNode
@@ -53,7 +53,15 @@
  * @sample androidx.compose.foundation.samples.HandwritingDetectorSample
  */
 fun Modifier.handwritingDetector(callback: () -> Unit) =
-    if (isStylusHandwritingSupported) then(HandwritingDetectorElement(callback)) else this
+    if (isStylusHandwritingSupported) {
+        then(HandwritingDetectorElement(callback))
+            .padding(
+                horizontal = HandwritingBoundsHorizontalOffset,
+                vertical = HandwritingBoundsVerticalOffset
+            )
+    } else {
+        this
+    }
 
 private class HandwritingDetectorElement(
     private val callback: () -> Unit
@@ -93,11 +101,9 @@
         pointerInputNode.onCancelPointerInput()
     }
 
-    val pointerInputNode = delegate(SuspendingPointerInputModifierNode {
-        detectStylusHandwriting {
-            callback()
-            composeImm.prepareStylusHandwritingDelegation()
-            return@detectStylusHandwriting true
-        }
+    val pointerInputNode = delegate(StylusHandwritingNodeWithNegativePadding {
+        callback()
+        composeImm.prepareStylusHandwritingDelegation()
+        return@StylusHandwritingNodeWithNegativePadding true
     })
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
index b427583..e770393 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.android.kt
@@ -18,36 +18,16 @@
 
 import androidx.compose.foundation.contextmenu.ContextMenuScope
 import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
 import androidx.compose.foundation.text.TextContextMenuItems
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.foundation.text.TextItem
 
-@ReadOnlyComposable
-@Composable
 internal fun TextFieldSelectionState.contextMenuBuilder(
     state: ContextMenuState,
-): ContextMenuScope.() -> Unit {
-    val cutString = TextContextMenuItems.Cut.resolvedString()
-    val copyString = TextContextMenuItems.Copy.resolvedString()
-    val pasteString = TextContextMenuItems.Paste.resolvedString()
-    val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
-    return {
-        item(state, label = cutString, enabled = canCut()) { cut() }
-        item(state, label = copyString, enabled = canCopy()) { copy(cancelSelection = false) }
-        item(state, label = pasteString, enabled = canPaste()) { paste() }
-        item(state, label = selectAllString, enabled = canSelectAll()) { selectAll() }
+): ContextMenuScope.() -> Unit = {
+    TextItem(state, TextContextMenuItems.Cut, enabled = canCut()) { cut() }
+    TextItem(state, TextContextMenuItems.Copy, enabled = canCopy()) {
+        copy(cancelSelection = false)
     }
-}
-
-private inline fun ContextMenuScope.item(
-    state: ContextMenuState,
-    label: String,
-    enabled: Boolean,
-    crossinline operation: () -> Unit
-) {
-    item(label, enabled = enabled) {
-        operation()
-        state.close()
-    }
+    TextItem(state, TextContextMenuItems.Paste, enabled = canPaste()) { paste() }
+    TextItem(state, TextContextMenuItems.SelectAll, enabled = canSelectAll()) { selectAll() }
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
index 4f24fb9..0e4100a6 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.android.kt
@@ -19,14 +19,12 @@
 import androidx.compose.foundation.PlatformMagnifierFactory
 import androidx.compose.foundation.contextmenu.ContextMenuScope
 import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
 import androidx.compose.foundation.isPlatformMagnifierSupported
 import androidx.compose.foundation.magnifier
 import androidx.compose.foundation.text.KeyCommand
 import androidx.compose.foundation.text.TextContextMenuItems
+import androidx.compose.foundation.text.TextItem
 import androidx.compose.foundation.text.platformDefaultKeyMapping
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -72,31 +70,19 @@
     }
 }
 
-@ReadOnlyComposable
-@Composable
 internal fun SelectionManager.contextMenuBuilder(
     state: ContextMenuState,
-): ContextMenuScope.() -> Unit {
-    val copyString = TextContextMenuItems.Copy.resolvedString()
-    val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
-    return {
-        listOf(
-            item(
-                label = copyString,
-                enabled = isNonEmptySelection(),
-                onClick = {
-                    copy()
-                    state.close()
-                },
-            ),
-            item(
-                label = selectAllString,
-                enabled = !isEntireContainerSelected(),
-                onClick = {
-                    selectAll()
-                    state.close()
-                },
-            ),
-        )
-    }
+): ContextMenuScope.() -> Unit = {
+    listOf(
+        TextItem(
+            state = state,
+            label = TextContextMenuItems.Copy,
+            enabled = isNonEmptySelection(),
+        ) { copy() },
+        TextItem(
+            state = state,
+            label = TextContextMenuItems.SelectAll,
+            enabled = !isEntireContainerSelected(),
+        ) { selectAll() },
+    )
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
index 3d69358..5aa6139 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.android.kt
@@ -19,12 +19,10 @@
 import androidx.compose.foundation.PlatformMagnifierFactory
 import androidx.compose.foundation.contextmenu.ContextMenuScope
 import androidx.compose.foundation.contextmenu.ContextMenuState
-import androidx.compose.foundation.contextmenu.close
 import androidx.compose.foundation.isPlatformMagnifierSupported
 import androidx.compose.foundation.magnifier
 import androidx.compose.foundation.text.TextContextMenuItems
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.foundation.text.TextItem
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -70,49 +68,29 @@
     }
 }
 
-@ReadOnlyComposable
-@Composable
 internal fun TextFieldSelectionManager.contextMenuBuilder(
     contextMenuState: ContextMenuState
-): ContextMenuScope.() -> Unit {
-    val cutString = TextContextMenuItems.Cut.resolvedString()
-    val copyString = TextContextMenuItems.Copy.resolvedString()
-    val pasteString = TextContextMenuItems.Paste.resolvedString()
-    val selectAllString = TextContextMenuItems.SelectAll.resolvedString()
-    return {
-        val isPassword = visualTransformation is PasswordVisualTransformation
-        val hasSelection = !value.selection.collapsed
-        item(
-            label = cutString,
-            enabled = hasSelection && editable && !isPassword,
-            onClick = {
-                cut()
-                contextMenuState.close()
-            },
-        )
-        item(
-            label = copyString,
-            enabled = hasSelection && !isPassword,
-            onClick = {
-                copy(cancelSelection = false)
-                contextMenuState.close()
-            },
-        )
-        item(
-            label = pasteString,
-            enabled = editable && clipboardManager?.hasText() == true,
-            onClick = {
-                paste()
-                contextMenuState.close()
-            },
-        )
-        item(
-            label = selectAllString,
-            enabled = value.selection.length != value.text.length,
-            onClick = {
-                selectAll()
-                contextMenuState.close()
-            },
-        )
-    }
+): ContextMenuScope.() -> Unit = {
+    val isPassword = visualTransformation is PasswordVisualTransformation
+    val hasSelection = !value.selection.collapsed
+    TextItem(
+        state = contextMenuState,
+        label = TextContextMenuItems.Cut,
+        enabled = hasSelection && editable && !isPassword,
+    ) { cut() }
+    TextItem(
+        state = contextMenuState,
+        label = TextContextMenuItems.Copy,
+        enabled = hasSelection && !isPassword,
+    ) { copy(cancelSelection = false) }
+    TextItem(
+        state = contextMenuState,
+        label = TextContextMenuItems.Paste,
+        enabled = editable && clipboardManager?.hasText() == true,
+    ) { paste() }
+    TextItem(
+        state = contextMenuState,
+        label = TextContextMenuItems.SelectAll,
+        enabled = value.selection.length != value.text.length,
+    ) { selectAll() }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
 ) : DragGestureNode(
     canDrag = AlwaysDrag,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
         state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
         update(
             enabled = enabled,
             interactionSource = interactionSource,
-            isResetPointerInputHandling = resetPointerInputHandling,
+            shouldResetPointerInputHandling = resetPointerInputHandling,
+            orientationLock = orientation
         )
     }
 
@@ -742,7 +741,8 @@
     @Deprecated(
         message = "Use the progress function to query the progress between two specified " +
             "anchors.",
-        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+    )
     @get:FloatRange(from = 0.0, to = 1.0)
     val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
         val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
     }
 }
 
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var minValue = Float.POSITIVE_INFINITY
     forEachValue { value ->
@@ -1307,7 +1307,7 @@
     return minValue
 }
 
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var maxValue = Float.NEGATIVE_INFINITY
     forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
 import kotlin.math.sign
 import kotlinx.coroutines.CancellationException
 
@@ -79,7 +79,7 @@
         pointerId,
         PointerType.Touch,
         onPointerSlopReached = onTouchSlopReached,
-        pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+        orientation = null,
     )
 }
 
@@ -170,33 +170,113 @@
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+    onDragStart = { _, offset -> onDragStart(offset) },
+    onDragEnd = { onDragEnd.invoke() },
+    onDragCancel = onDragCancel,
+    shouldAwaitTouchSlop = { true },
+    orientationLock = null,
+    onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as  the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+    onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+    onDragEnd: (change: PointerInputChange) -> Unit,
+    onDragCancel: () -> Unit,
+    shouldAwaitTouchSlop: () -> Boolean,
+    orientationLock: Orientation?,
+    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
 ) {
     awaitEachGesture {
+        val initialDown =
+            awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+        val awaitTouchSlop = shouldAwaitTouchSlop()
+
+        if (!awaitTouchSlop) {
+            initialDown.consume()
+        }
         val down = awaitFirstDown(requireUnconsumed = false)
         var drag: PointerInputChange?
         var overSlop = Offset.Zero
-        do {
-            drag = awaitPointerSlopOrCancellation(
-                down.id,
-                down.type,
-                pointerDirectionConfig = BidirectionalPointerDirectionConfig
-            ) { change, over ->
-                change.consume()
-                overSlop = over
-            }
-        } while (drag != null && !drag.isConsumed)
-        if (drag != null) {
-            onDragStart.invoke(drag.position)
-            onDrag(drag, overSlop)
-            if (
-                !drag(drag.id) {
-                    onDrag(it, it.positionChange())
-                    it.consume()
+        var initialDelta = Offset.Zero
+
+        if (awaitTouchSlop) {
+            do {
+                drag = awaitPointerSlopOrCancellation(
+                    down.id,
+                    down.type,
+                    orientation = orientationLock
+                ) { change, over ->
+                    change.consume()
+                    overSlop = over
                 }
-            ) {
+            } while (drag != null && !drag.isConsumed)
+            initialDelta = drag?.position ?: Offset.Zero
+        } else {
+            drag = initialDown
+        }
+
+        if (drag != null) {
+            onDragStart.invoke(initialDown, initialDelta)
+            onDrag(drag, overSlop)
+            val upEvent = drag(
+                pointerId = drag.id,
+                onDrag = {
+                    onDrag(it, it.positionChange())
+                },
+                orientation = orientationLock,
+                motionConsumed = {
+                    it.isConsumed
+                })
+            if (upEvent == null) {
                 onDragCancel()
             } else {
-                onDragEnd()
+                onDragEnd(upEvent)
             }
         }
     }
@@ -288,7 +368,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 /**
@@ -324,7 +404,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+    orientation = Orientation.Vertical,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -439,7 +519,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 /**
@@ -472,7 +552,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+    orientation = Orientation.Horizontal,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -563,9 +643,12 @@
 
 /**
  * Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
  *
  * @return The last pointer input event change when gesture ended with all pointers up
  * and null when the gesture was canceled.
@@ -573,7 +656,7 @@
 internal suspend inline fun AwaitPointerEventScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit,
-    hasDragged: (PointerInputChange) -> Boolean,
+    orientation: Orientation?,
     motionConsumed: (PointerInputChange) -> Boolean
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
     }
     var pointer = pointerId
     while (true) {
-        val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+        val change = awaitDragOrUp(pointer) {
+            val positionChange = it.positionChangeIgnoreConsumed()
+            val motionChange = if (orientation == null) {
+                positionChange.getDistance()
+            } else {
+                if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+            }
+            motionChange != 0.0f
+        } ?: return null
 
         if (motionConsumed(change)) {
             return null
@@ -629,16 +720,14 @@
 }
 
 /**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of  touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
  * lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
  * [awaitPointerSlopOrCancellation] is called, then `null` is returned.
  *
  * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
  * position change, pointer slop will not have been considered detected and the detection will
  * continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
  *
@@ -650,10 +739,10 @@
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
     pointerId: PointerId,
     pointerType: PointerType,
-    pointerDirectionConfig: PointerDirectionConfig,
+    orientation: Orientation?,
     onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
     }
     val touchSlop = viewConfiguration.pointerSlop(pointerType)
     var pointer: PointerId = pointerId
-    var totalPositionChange = Offset.Zero
-
+    val touchSlopDetector = TouchSlopDetector(orientation)
     while (true) {
         val event = awaitPointerEvent()
         val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
                 pointer = otherDown.id
             }
         } else {
-            val currentPosition = dragEvent.position
-            val previousPosition = dragEvent.previousPosition
-
-            val positionChange = currentPosition - previousPosition
-
-            totalPositionChange += positionChange
-
-            val inDirection = pointerDirectionConfig.calculateDeltaChange(
-                totalPositionChange
-            )
-
-            if (inDirection < touchSlop) {
-                // verify that nothing else consumed the drag event
-                awaitPointerEvent(PointerEventPass.Final)
-                if (dragEvent.isConsumed) {
-                    return null
-                }
-            } else {
-                val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
-                    totalPositionChange,
-                    touchSlop
-                )
-
+            val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+            if (postSlopOffset != null) {
                 onPointerSlopReached(
                     dragEvent,
                     postSlopOffset
@@ -707,7 +774,13 @@
                 if (dragEvent.isConsumed) {
                     return dragEvent
                 } else {
-                    totalPositionChange = Offset.Zero
+                    touchSlopDetector.reset()
+                }
+            } else {
+                // verify that nothing else consumed the drag event
+                awaitPointerEvent(PointerEventPass.Final)
+                if (dragEvent.isConsumed) {
+                    return null
                 }
             }
         }
@@ -715,70 +788,77 @@
 }
 
 /**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
  */
-internal interface PointerDirectionConfig {
-    fun calculateDeltaChange(offset: Offset): Float
-    fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
 
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+    fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+    fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
 
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
+    /**
+     * The accumulation of drag deltas in this detector.
+     */
+    private var totalPositionChange: Offset = Offset.Zero
+
+    /**
+     * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+     * slop provided by [touchSlop], this method will return the post slop offset, that is the
+     * total accumulated delta change minus the touch slop value, otherwise this should return null.
+     */
+    fun addPointerInputChange(
+        dragEvent: PointerInputChange,
         touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.x -
-            (sign(totalPositionChange.x) * touchSlop)
-        return Offset(finalMainPositionChange, totalPositionChange.y)
+    ): Offset? {
+        val currentPosition = dragEvent.position
+        val previousPosition = dragEvent.previousPosition
+        val positionChange = currentPosition - previousPosition
+        totalPositionChange += positionChange
+
+        val inDirection = if (orientation == null) {
+            totalPositionChange.getDistance()
+        } else {
+            totalPositionChange.mainAxis().absoluteValue
+        }
+
+        val hasCrossedSlop = inDirection >= touchSlop
+
+        return if (hasCrossedSlop) {
+            calculatePostSlopOffset(touchSlop)
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Resets the accumulator associated with this detector.
+     */
+    fun reset() {
+        totalPositionChange = Offset.Zero
+    }
+
+    private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+        return if (orientation == null) {
+            val touchSlopOffset =
+                totalPositionChange / totalPositionChange.getDistance() * touchSlop
+            // update postSlopOffset
+            totalPositionChange - touchSlopOffset
+        } else {
+            val finalMainAxisChange = totalPositionChange.mainAxis() -
+                (sign(totalPositionChange.mainAxis()) * touchSlop)
+            val finalCrossAxisChange = totalPositionChange.crossAxis()
+            if (orientation == Orientation.Horizontal) {
+                Offset(finalMainAxisChange, finalCrossAxisChange)
+            } else {
+                Offset(finalCrossAxisChange, finalMainAxisChange)
+            }
+        }
     }
 }
 
 /**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.y -
-            (sign(totalPositionChange.y) * touchSlop)
-        return Offset(totalPositionChange.x, finalMainPositionChange)
-    }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val touchSlopOffset =
-            totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
-        return totalPositionChange - touchSlopOffset
-    }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
-    if (this == Orientation.Vertical) VerticalPointerDirectionConfig
-    else HorizontalPointerDirectionConfig
-
-/**
  * Waits for a long press by examining [pointerId].
  *
  * If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
                         // should technically never happen as we checked it above
                         finished = true
                     }
-                // Pointer (id) stayed down.
+                    // Pointer (id) stayed down.
                 } else {
                     longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
 import kotlin.math.sign
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
         }
     }
 
-    override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@DraggableNode.onDragStarted(this, startedPosition)
 
@@ -357,6 +350,7 @@
             canDrag,
             enabled,
             interactionSource,
+            orientation,
             resetPointerInputHandling
         )
     }
@@ -372,6 +366,7 @@
     canDrag: (PointerInputChange) -> Boolean,
     enabled: Boolean,
     interactionSource: MutableInteractionSource?,
+    private var orientationLock: Orientation?
 ) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
 
     protected var canDrag = canDrag
@@ -397,13 +392,6 @@
     abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
 
     /**
-     * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
-     * important when observing the delta change for Draggable, as we want to observe the change
-     * in the main axis only.
-     */
-    abstract val pointerDirectionConfig: PointerDirectionConfig
-
-    /**
      * Passes the action needed when a drag starts. This gives the ability to pass the desired
      * behavior from other nodes implementing AbstractDraggableNode
      */
@@ -478,62 +466,63 @@
             // re-create tracker when pointer input block restarts. This lazily creates the tracker
             // only when it is need.
             val velocityTracker = VelocityTracker()
+            val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+                { startEvent, initialDelta ->
+                    if (canDrag.invoke(startEvent)) {
+                        if (!isListeningForEvents) {
+                            if (channel == null) {
+                                channel = Channel(capacity = Channel.UNLIMITED)
+                            }
+                            startListeningForEvents()
+                        }
+                        val overSlopOffset = initialDelta
+                        val xSign = sign(startEvent.position.x)
+                        val ySign = sign(startEvent.position.y)
+                        val adjustedStart = startEvent.position -
+                            Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+                        channel?.trySend(DragStarted(adjustedStart))
+                    }
+                }
+
+            val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+                velocityTracker.addPointerInputChange(upEvent)
+                val maximumVelocity = currentValueOf(LocalViewConfiguration)
+                    .maximumFlingVelocity
+                val velocity = velocityTracker.calculateVelocity(
+                    Velocity(maximumVelocity, maximumVelocity)
+                )
+                velocityTracker.resetTracking()
+                channel?.trySend(DragStopped(velocity))
+            }
+
+            val onDragCancel: () -> Unit = {
+                channel?.trySend(DragCancelled)
+            }
+
+            val shouldAwaitTouchSlop: () -> Boolean = {
+                !startDragImmediately()
+            }
+
+            val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+                { change, delta ->
+                    velocityTracker.addPointerInputChange(change)
+                    channel?.trySend(DragDelta(delta))
+                }
+
             coroutineScope {
                 try {
-                    awaitPointerEventScope {
-                        while (isActive) {
-                            awaitDownAndSlop(
-                                _canDrag,
-                                ::startDragImmediately,
-                                velocityTracker,
-                                pointerDirectionConfig
-                            )?.let {
-                                /**
-                                 * The gesture crossed the touch slop, events are now relevant
-                                 * and should be propagated
-                                 */
-                                if (!isListeningForEvents) {
-                                    if (channel == null) {
-                                        channel = Channel(capacity = Channel.UNLIMITED)
-                                    }
-                                    startListeningForEvents()
-                                }
-                                var isDragSuccessful = false
-                                try {
-                                    isDragSuccessful = awaitDrag(
-                                        it.first,
-                                        it.second,
-                                        velocityTracker,
-                                        channel
-                                    ) { event ->
-                                        pointerDirectionConfig.calculateDeltaChange(
-                                            event.positionChangeIgnoreConsumed()
-                                        ) != 0f
-                                    }
-                                } catch (cancellation: CancellationException) {
-                                    isDragSuccessful = false
-                                    if (!isActive) throw cancellation
-                                } finally {
-                                    val maximumVelocity = currentValueOf(LocalViewConfiguration)
-                                        .maximumFlingVelocity
-                                    val event = if (isDragSuccessful) {
-                                        val velocity = velocityTracker.calculateVelocity(
-                                            Velocity(maximumVelocity, maximumVelocity)
-                                        )
-                                        velocityTracker.resetTracking()
-                                        DragStopped(velocity)
-                                    } else {
-                                        DragCancelled
-                                    }
-                                    channel?.trySend(event)
-                                }
-                            }
-                        }
-                    }
-                } catch (exception: CancellationException) {
-                    if (!isActive) {
-                        throw exception
-                    }
+                    detectDragGestures(
+                        orientationLock = orientationLock,
+                        onDragStart = onDragStart,
+                        onDragEnd = onDragEnd,
+                        onDragCancel = onDragCancel,
+                        shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+                        onDrag = onDrag
+                    )
+                } catch (cancellation: CancellationException) {
+                    channel?.trySend(DragCancelled)
+                    if (!isActive) throw cancellation
                 }
             }
         }
@@ -580,9 +569,10 @@
         canDrag: (PointerInputChange) -> Boolean = this.canDrag,
         enabled: Boolean = this.enabled,
         interactionSource: MutableInteractionSource? = this.interactionSource,
-        isResetPointerInputHandling: Boolean = false
+        orientationLock: Orientation? = this.orientationLock,
+        shouldResetPointerInputHandling: Boolean = false
     ) {
-        var resetPointerInputHandling = isResetPointerInputHandling
+        var resetPointerInputHandling = shouldResetPointerInputHandling
 
         this.canDrag = canDrag
         if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
             this.interactionSource = interactionSource
         }
 
+        if (this.orientationLock != orientationLock) {
+            this.orientationLock = orientationLock
+            resetPointerInputHandling = true
+        }
+
         if (resetPointerInputHandling) {
             pointerInputNode?.resetPointerInputHandler()
         }
     }
 }
 
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
-    canDrag: (PointerInputChange) -> Boolean,
-    startDragImmediately: () -> Boolean,
-    velocityTracker: VelocityTracker,
-    pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
-    val initialDown =
-        awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
-    return if (!canDrag(initialDown)) {
-        null
-    } else if (startDragImmediately()) {
-        initialDown.consume()
-        velocityTracker.addPointerInputChange(initialDown)
-        // since we start immediately we don't wait for slop and the initial delta is 0
-        initialDown to Offset.Zero
-    } else {
-        val down = awaitFirstDown(requireUnconsumed = false)
-        velocityTracker.addPointerInputChange(down)
-        var initialDelta = Offset.Zero
-        val postPointerSlop = { event: PointerInputChange, offset: Offset ->
-            velocityTracker.addPointerInputChange(event)
-            event.consume()
-            initialDelta = offset
-        }
-
-        val afterSlopResult = awaitPointerSlopOrCancellation(
-            down.id,
-            down.type,
-            pointerDirectionConfig = pointerDirectionConfig,
-            onPointerSlopReached = postPointerSlop
-        )
-
-        if (afterSlopResult != null) afterSlopResult to initialDelta else null
-    }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
-    startEvent: PointerInputChange,
-    initialDelta: Offset,
-    velocityTracker: VelocityTracker,
-    channel: SendChannel<DragEvent>?,
-    hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
-    val overSlopOffset = initialDelta
-    val xSign = sign(startEvent.position.x)
-    val ySign = sign(startEvent.position.y)
-    val adjustedStart = startEvent.position -
-        Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-    channel?.trySend(DragStarted(adjustedStart))
-
-    channel?.trySend(DragDelta(initialDelta))
-
-    return onDragOrUp(hasDragged, startEvent.id) { event ->
-        // Velocity tracker takes all events, even UP
-        velocityTracker.addPointerInputChange(event)
-
-        // Dispatch only MOVE events
-        if (!event.changedToUpIgnoreConsumed()) {
-            val delta = event.positionChange()
-            event.consume()
-            channel?.trySend(DragDelta(delta))
-        }
-    }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
-    hasDragged: (PointerInputChange) -> Boolean,
-    pointerId: PointerId,
-    onDrag: (PointerInputChange) -> Unit
-): Boolean {
-    return drag(
-        pointerId = pointerId,
-        onDrag = onDrag,
-        hasDragged = hasDragged,
-        motionConsumed = { it.isConsumed }
-    )?.let(onDrag) != null
-}
-
 private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
 
     private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = null
 ) {
 
     override suspend fun drag(
@@ -282,8 +283,6 @@
         }
     }
 
-    override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@Draggable2DNode.onDragStarted(this, startedPosition)
 
@@ -317,10 +316,11 @@
         this.startDragImmediately = startDragImmediately
 
         update(
-            canDrag,
-            enabled,
-            interactionSource,
-            resetPointerInputHandling
+            canDrag = canDrag,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = null,
+            shouldResetPointerInputHandling = resetPointerInputHandling
         )
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
 ) : DragGestureNode(
     canDrag = CanDragCalculation,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ), ObserverModifierNode, CompositionLocalConsumerModifierNode,
     FocusPropertiesModifierNode, KeyInputModifierNode {
 
@@ -333,9 +334,6 @@
         scrollingLogic.dispatchDragEvents(forEachDelta)
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = scrollingLogic.pointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
         this.flingBehavior = flingBehavior
 
         // update DragGestureNode
-        update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+        update(
+            canDrag = CanDragCalculation,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+            shouldResetPointerInputHandling = resetPointerInputHandling
+        )
     }
 
     override fun onAttach() {
@@ -777,8 +781,6 @@
         return resetPointerInputHandling
     }
 
-    fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     fun isVertical(): Boolean = orientation == Vertical
 }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 5928a6e..886f376 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -960,10 +960,6 @@
         val mainAxisOffset = itemScrollOffsets.maxInRange(spanRange)
         val crossAxisOffset = resolvedSlots.positions[laneIndex]
 
-        if (item.placeablesCount == 0) {
-            // nothing to place, ignore spacings
-            continue
-        }
         item.position(
             mainAxis = mainAxisOffset,
             crossAxis = crossAxisOffset,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 50eccc5..8bd9033 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -27,8 +27,7 @@
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.relocation.BringIntoViewRequester
 import androidx.compose.foundation.relocation.bringIntoViewRequester
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
-import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
+import androidx.compose.foundation.text.handwriting.stylusHandwriting
 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
@@ -82,6 +81,7 @@
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.platform.LocalDensity
@@ -405,34 +405,6 @@
             textDragObserver = manager.touchSelectionObserver,
         )
         .pointerHoverIcon(textPointerIcon)
-        .then(
-            if (isStylusHandwritingSupported && writeable) {
-                Modifier.pointerInput(Unit) {
-                    detectStylusHandwriting {
-                        if (!state.hasFocus) {
-                            focusRequester.requestFocus()
-                        }
-                        // If this is a password field, we can't trigger handwriting.
-                        // The expected behavior is 1) request focus 2) show software keyboard.
-                        // Note: TextField will show software keyboard automatically when it
-                        // gain focus. 3) show a toast message telling that handwriting is not
-                        // supported for password fields. TODO(b/335294152)
-                        if (imeOptions.keyboardType != KeyboardType.Password) {
-                            // TextInputService is calling LegacyTextInputServiceAdapter under the
-                            // hood.  And because it's a public API, startStylusHandwriting is added
-                            // to legacyTextInputServiceAdapter instead.
-                            // startStylusHandwriting may be called before the actual input
-                            // session starts when the editor is not focused, this is handled
-                            // internally by the LegacyTextInputServiceAdapter.
-                            legacyTextInputServiceAdapter.startStylusHandwriting()
-                        }
-                        true
-                    }
-                }
-            } else {
-                Modifier
-            }
-        )
 
     val drawModifier = Modifier.drawBehind {
         state.layoutResult?.let { layoutResult ->
@@ -657,10 +629,32 @@
             imeAction = imeOptions.imeAction,
         )
 
+    val stylusHandwritingModifier = Modifier.stylusHandwriting(writeable) {
+        if (!state.hasFocus) {
+            focusRequester.requestFocus()
+        }
+        // If this is a password field, we can't trigger handwriting.
+        // The expected behavior is 1) request focus 2) show software keyboard.
+        // Note: TextField will show software keyboard automatically when it
+        // gain focus. 3) show a toast message telling that handwriting is not
+        // supported for password fields. TODO(b/335294152)
+        if (imeOptions.keyboardType != KeyboardType.Password) {
+            // TextInputService is calling LegacyTextInputServiceAdapter under the
+            // hood.  And because it's a public API, startStylusHandwriting is added
+            // to legacyTextInputServiceAdapter instead.
+            // startStylusHandwriting may be called before the actual input
+            // session starts when the editor is not focused, this is handled
+            // internally by the LegacyTextInputServiceAdapter.
+            legacyTextInputServiceAdapter.startStylusHandwriting()
+        }
+        true
+    }
+
     // Modifiers that should be applied to the outer text field container. Usually those include
     // gesture and semantics modifiers.
     val decorationBoxModifier = modifier
         .legacyTextInputAdapter(legacyTextInputServiceAdapter, state, manager)
+        .then(stylusHandwritingModifier)
         .then(focusModifier)
         .interceptDPadAndMoveFocus(state, focusManager)
         .previewKeyEventToDeselectOnBack(state, manager)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
index 3d4ba3b..4909b8f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/handwriting/StylusHandwriting.kt
@@ -18,69 +18,194 @@
 
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.layout.padding
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusEventModifierNode
+import androidx.compose.ui.focus.FocusState
+import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.input.pointer.PointerInputChange
-import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.offset
 import androidx.compose.ui.util.fastFirstOrNull
 
 /**
- * A utility function that detects stylus movements and calls the [onHandwritingSlopExceeded] when
+ * A modifier that detects stylus movements and calls the [onHandwritingSlopExceeded] when
  * it detects that stylus movement has exceeds the handwriting slop.
- * If [onHandwritingSlopExceeded] returns true, this method will consume the events and consider
+ * If [onHandwritingSlopExceeded] returns true, it will consume the events and consider
  * that the handwriting has successfully started. Otherwise, it'll stop monitoring the current
  * gesture.
+ * @param enabled whether this modifier is enabled, it's used for the case where the editor is
+ * readOnly or disabled.
+ * @param onHandwritingSlopExceeded the callback that's invoked when it detects stylus handwriting.
+ * The return value determines whether the handwriting is triggered or not. When it's true, this
+ * modifier will consume the pointer events.
  */
-internal suspend inline fun PointerInputScope.detectStylusHandwriting(
-    crossinline onHandwritingSlopExceeded: () -> Boolean
-) {
-    awaitEachGesture {
-        val firstDown =
-            awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+internal fun Modifier.stylusHandwriting(
+    enabled: Boolean,
+    onHandwritingSlopExceeded: () -> Boolean
+): Modifier = if (enabled && isStylusHandwritingSupported) {
+    this.then(StylusHandwritingElementWithNegativePadding(onHandwritingSlopExceeded))
+        .padding(
+            horizontal = HandwritingBoundsHorizontalOffset,
+            vertical = HandwritingBoundsVerticalOffset
+        )
+} else {
+    this
+}
 
-        val isStylus =
-            firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
-        if (!isStylus) {
-            return@awaitEachGesture
+private data class StylusHandwritingElementWithNegativePadding(
+    val onHandwritingSlopExceeded: () -> Boolean
+) : ModifierNodeElement<StylusHandwritingNodeWithNegativePadding>() {
+    override fun create(): StylusHandwritingNodeWithNegativePadding {
+        return StylusHandwritingNodeWithNegativePadding(onHandwritingSlopExceeded)
+    }
+
+    override fun update(node: StylusHandwritingNodeWithNegativePadding) {
+        node.onHandwritingSlopExceeded = onHandwritingSlopExceeded
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "stylusHandwriting"
+        properties["onHandwritingSlopExceeded"] = onHandwritingSlopExceeded
+    }
+}
+
+/**
+ * A stylus handwriting node with negative padding. This node should be  used in pair with a padding
+ * modifier. Together, they expands the touch bounds of the editor while keep its visual bounds the
+ * same.
+ * Note: this node is a temporary solution, ideally we don't need it.
+ */
+internal class StylusHandwritingNodeWithNegativePadding(
+    onHandwritingSlopExceeded: () -> Boolean
+) : StylusHandwritingNode(onHandwritingSlopExceeded), LayoutModifierNode {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val paddingVerticalPx = HandwritingBoundsVerticalOffset.roundToPx()
+        val paddingHorizontalPx = HandwritingBoundsHorizontalOffset.roundToPx()
+        val newConstraint = constraints.offset(
+            2 * paddingHorizontalPx,
+            2 * paddingVerticalPx
+        )
+        val placeable = measurable.measure(newConstraint)
+
+        val height = placeable.height - paddingVerticalPx * 2
+        val width = placeable.width - paddingHorizontalPx * 2
+        return layout(width, height) {
+            placeable.place(-paddingHorizontalPx, -paddingVerticalPx)
         }
-        // Await the touch slop before long press timeout.
-        var exceedsTouchSlop: PointerInputChange? = null
-        // The stylus move must exceeds touch slop before long press timeout.
-        while (true) {
-            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Main)
-            // The tracked pointer is consumed or lifted, stop tracking.
-            val change = pointerEvent.changes.fastFirstOrNull {
-                !it.isConsumed && it.id == firstDown.id && it.pressed
-            }
-            if (change == null) {
-                break
+    }
+
+    override fun sharePointerInputWithSiblings(): Boolean {
+        // Share events to siblings so that the expanded touch bounds won't block other elements
+        // surrounding the editor.
+        return true
+    }
+}
+
+internal open class StylusHandwritingNode(
+    var onHandwritingSlopExceeded: () -> Boolean
+) : DelegatingNode(), PointerInputModifierNode, FocusEventModifierNode {
+
+    private var focused = false
+
+    override fun onFocusEvent(focusState: FocusState) {
+        focused = focusState.isFocused
+    }
+
+    private val suspendingPointerInputModifierNode = delegate(SuspendingPointerInputModifierNode {
+        awaitEachGesture {
+            val firstDown =
+                awaitFirstDown(requireUnconsumed = true, pass = PointerEventPass.Initial)
+
+            val isStylus =
+                firstDown.type == PointerType.Stylus || firstDown.type == PointerType.Eraser
+            if (!isStylus) {
+                return@awaitEachGesture
             }
 
-            val time = change.uptimeMillis - firstDown.uptimeMillis
-            if (time >= viewConfiguration.longPressTimeoutMillis) {
-                break
+            val isInBounds = firstDown.position.x >= 0 && firstDown.position.x < size.width &&
+                firstDown.position.y >= 0 && firstDown.position.y < size.height
+
+            // If the editor is focused or the first down is within the editor's bounds, we
+            // await the initial pass. This prioritize the focused editor over unfocused
+            // editor.
+            val pass = if (focused || isInBounds) {
+                PointerEventPass.Initial
+            } else {
+                PointerEventPass.Main
             }
 
-            val offset = change.position - firstDown.position
-            if (offset.getDistance() > viewConfiguration.handwritingSlop) {
-                exceedsTouchSlop = change
-                break
+            // Await the touch slop before long press timeout.
+            var exceedsTouchSlop: PointerInputChange? = null
+            // The stylus move must exceeds touch slop before long press timeout.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass)
+                // The tracked pointer is consumed or lifted, stop tracking.
+                val change = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                }
+                if (change == null) {
+                    break
+                }
+
+                val time = change.uptimeMillis - firstDown.uptimeMillis
+                if (time >= viewConfiguration.longPressTimeoutMillis) {
+                    break
+                }
+
+                val offset = change.position - firstDown.position
+                if (offset.getDistance() > viewConfiguration.handwritingSlop) {
+                    exceedsTouchSlop = change
+                    break
+                }
+            }
+
+            if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
+                return@awaitEachGesture
+            }
+            exceedsTouchSlop.consume()
+
+            // Consume the remaining changes of this pointer.
+            while (true) {
+                val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
+                val pointerChange = pointerEvent.changes.fastFirstOrNull {
+                    !it.isConsumed && it.id == firstDown.id && it.pressed
+                } ?: return@awaitEachGesture
+                pointerChange.consume()
             }
         }
+    })
 
-        if (exceedsTouchSlop == null || !onHandwritingSlopExceeded.invoke()) {
-            return@awaitEachGesture
-        }
-        exceedsTouchSlop.consume()
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        suspendingPointerInputModifierNode.onPointerEvent(pointerEvent, pass, bounds)
+    }
 
-        // Consume the remaining changes of this pointer.
-        while (true) {
-            val pointerEvent = awaitPointerEvent(pass = PointerEventPass.Initial)
-            val pointerChange = pointerEvent.changes.fastFirstOrNull {
-                !it.isConsumed && it.id == firstDown.id && it.pressed
-            } ?: return@awaitEachGesture
-            pointerChange.consume()
-        }
+    override fun onCancelPointerInput() {
+        suspendingPointerInputModifierNode.onCancelPointerInput()
+    }
+
+    fun resetPointerInputHandler() {
+        suspendingPointerInputModifierNode.resetPointerInputHandler()
     }
 }
 
@@ -89,3 +214,9 @@
  *  and NOT for checking whether the IME supports handwriting.
  */
 internal expect val isStylusHandwritingSupported: Boolean
+
+/**
+ * The amount of the padding added to the handwriting bounds of an editor.
+ */
+internal val HandwritingBoundsVerticalOffset = 40.dp
+internal val HandwritingBoundsHorizontalOffset = 10.dp
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
index eeddf40..5d02a03f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/TextFieldDecoratorModifier.kt
@@ -29,7 +29,7 @@
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.KeyboardActionScope
 import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.text.handwriting.detectStylusHandwriting
+import androidx.compose.foundation.text.handwriting.StylusHandwritingNode
 import androidx.compose.foundation.text.handwriting.isStylusHandwritingSupported
 import androidx.compose.foundation.text.input.InputTransformation
 import androidx.compose.foundation.text.input.KeyboardActionHandler
@@ -225,41 +225,36 @@
                     detectTextFieldLongPressAndAfterDrag(requestFocus)
                 }
             }
-            // Note: when editable changes (enabled or readOnly changes) or keyboard type changes,
-            // this pointerInputModifier is reset. And we don't need to worry about cancel or launch
-            // the stylus handwriting detecting job.
-            if (isStylusHandwritingSupported && editable) {
-                 launch(start = CoroutineStart.UNDISPATCHED) {
-                    detectStylusHandwriting {
-                        if (!isFocused) {
-                            requestFocus()
-                        }
-                        // If this is a password field, we can't trigger handwriting.
-                        // The expected behavior is 1) request focus 2) show software keyboard.
-                        // Note: TextField will show software keyboard automatically when it
-                        // gain focus. 3) show a toast message telling that handwriting is not
-                        // supported for password fields. TODO(b/335294152)
-                        if (keyboardOptions.keyboardType != KeyboardType.Password) {
-                            // Send the handwriting start signal to platform.
-                            // The editor should send the signal when it is focused or is about
-                            // to gain focus, Here are more details:
-                            //   1) if the editor already has an active input session, the
-                            //   platform handwriting service should already listen to this flow
-                            //   and it'll start handwriting right away.
-                            //
-                            //   2) if the editor is not focused, but it'll be focused and
-                            //   create a new input session, one handwriting signal will be
-                            //   replayed when the platform collect this flow. And the platform
-                            //   should trigger handwriting accordingly.
-                            stylusHandwritingTrigger?.tryEmit(Unit)
-                        }
-                        return@detectStylusHandwriting true
-                    }
-                }
-            }
         }
     })
 
+    private val stylusHandwritingNode = delegate(StylusHandwritingNode {
+        if (!isFocused) {
+            requestFocus()
+        }
+
+        // If this is a password field, we can't trigger handwriting.
+        // The expected behavior is 1) request focus 2) show software keyboard.
+        // Note: TextField will show software keyboard automatically when it
+        // gain focus. 3) show a toast message telling that handwriting is not
+        // supported for password fields. TODO(b/335294152)
+        if (keyboardOptions.keyboardType != KeyboardType.Password) {
+            // Send the handwriting start signal to platform.
+            // The editor should send the signal when it is focused or is about
+            // to gain focus, Here are more details:
+            //   1) if the editor already has an active input session, the
+            //   platform handwriting service should already listen to this flow
+            //   and it'll start handwriting right away.
+            //
+            //   2) if the editor is not focused, but it'll be focused and
+            //   create a new input session, one handwriting signal will be
+            //   replayed when the platform collect this flow. And the platform
+            //   should trigger handwriting accordingly.
+            stylusHandwritingTrigger?.tryEmit(Unit)
+        }
+        return@StylusHandwritingNode true
+    })
+
     /**
      * The last enter event that was submitted to [interactionSource] from [dragAndDropNode]. We
      * need to keep a reference to this event to send a follow-up exit event.
@@ -458,6 +453,7 @@
 
         if (textFieldSelectionState != previousTextFieldSelectionState) {
             pointerInputNode.resetPointerInputHandler()
+            stylusHandwritingNode.resetPointerInputHandler()
             if (isAttached) {
                 textFieldSelectionState.receiveContentConfiguration =
                     receiveContentConfigurationProvider
@@ -466,6 +462,7 @@
 
         if (interactionSource != previousInteractionSource) {
             pointerInputNode.resetPointerInputHandler()
+            stylusHandwritingNode.resetPointerInputHandler()
         }
     }
 
@@ -604,6 +601,7 @@
             disposeInputSession()
             textFieldState.collapseSelectionToMax()
         }
+        stylusHandwritingNode.onFocusEvent(focusState)
     }
 
     override fun onAttach() {
@@ -625,10 +623,12 @@
         pass: PointerEventPass,
         bounds: IntSize
     ) {
+        stylusHandwritingNode.onPointerEvent(pointerEvent, pass, bounds)
         pointerInputNode.onPointerEvent(pointerEvent, pass, bounds)
     }
 
     override fun onCancelPointerInput() {
+        stylusHandwritingNode.onCancelPointerInput()
         pointerInputNode.onCancelPointerInput()
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
index 3d55c6a..46181b9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/input/internal/selection/TextFieldSelectionState.kt
@@ -770,6 +770,14 @@
                     allowPreviousSelectionCollapsed = false,
                 )
 
+                // When drag starts from the end padding, we eventually need to update the start
+                // point once a selection is initiated. Otherwise, startOffset is always calculated
+                // from dragBeginPosition which can refer to different positions on text if
+                // TextField starts scrolling.
+                if (dragBeginOffsetInText == -1 && !newSelection.collapsed) {
+                    dragBeginOffsetInText = newSelection.start
+                }
+
                 // Although we support reversed selection, reversing the selection after it's
                 // initiated via long press has a visual glitch that's hard to get rid of. When
                 // handles (start/end) switch places after the selection reverts, draw happens a
@@ -779,14 +787,6 @@
                     newSelection = newSelection.reverse()
                 }
 
-                // When drag starts from the end padding, we eventually need to update the start
-                // point once a selection is initiated. Otherwise, startOffset is always calculated
-                // from dragBeginPosition which can refer to different positions on text if
-                // TextField starts scrolling.
-                if (dragBeginOffsetInText == -1 && !newSelection.collapsed) {
-                    dragBeginOffsetInText = newSelection.start
-                }
-
                 // if the new selection is not equal to previous selection, consider updating the
                 // acting handle. Otherwise, acting handle should remain the same.
                 if (newSelection != prevSelection) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
index 8b8b02b..3becb0e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionContainer.kt
@@ -37,6 +37,12 @@
 /**
  * Enables text selection for its direct or indirect children.
  *
+ * Use of a lazy layout, such as [LazyRow][androidx.compose.foundation.lazy.LazyRow] or
+ * [LazyColumn][androidx.compose.foundation.lazy.LazyColumn], within a [SelectionContainer]
+ * has undefined behavior on text items that aren't composed. For example, texts that aren't
+ * composed will not be included in copy operations and select all will not expand the
+ * selection to include them.
+ *
  * @sample androidx.compose.foundation.samples.SelectionSample
  */
 @Composable
diff --git a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index cccbd34..0eadc27 100644
--- a/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/compose/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -266,6 +266,7 @@
             </intent-filter>
             <intent-filter>
                 <action android:name="androidx.compose.integration.macrobenchmark.target.CROSSFADE_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
 	</activity>
 
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index 324023b..01a08c0 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -136,3 +136,15 @@
 android {
     namespace "androidx.compose.material.icons.extended"
 }
+
+afterEvaluate {
+    // Workaround for b/337776938
+    if (!project.hasProperty("android.injected.invoked.from.ide")) {
+        tasks.named("lintAnalyzeDebugAndroidTest").configure {
+            it.dependsOn("generateTestFilesDebugAndroidTest")
+        }
+        tasks.named("generateDebugAndroidTestLintModel").configure {
+            it.dependsOn("generateTestFilesDebugAndroidTest")
+        }
+    }
+}
diff --git a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
index 0f50354..0df30f3 100644
--- a/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
+++ b/compose/material/material/src/androidInstrumentedTest/kotlin/androidx/compose/material/SnackbarTest.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.foundation.shape.CutCornerShape
 import androidx.compose.runtime.CompositionLocalProvider
@@ -95,6 +96,29 @@
     }
 
     @Test
+    fun snackbar_emptyContent() {
+        rule.setMaterialContent {
+            Snackbar {
+                // empty content should not crash
+            }
+        }
+    }
+
+    @Test
+    fun snackbar_noTextInContent() {
+        val snackbarHeight = 48.dp
+        val contentSize = 20.dp
+        rule.setMaterialContent {
+            Snackbar {
+                // non-text content should not crash
+                Box(Modifier.testTag("content").size(contentSize))
+            }
+        }
+        rule.onNodeWithTag("content")
+            .assertTopPositionInRootIsEqualTo((snackbarHeight - contentSize) / 2)
+    }
+
+    @Test
     fun snackbar_shortTextOnly_defaultSizes() {
         val snackbar = rule.setMaterialContentForSizeAssertions(
             parentMaxWidth = 300.dp
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index 29673c2..2e490ec 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -32,10 +32,12 @@
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.LastBaseline
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastFirst
+import androidx.compose.ui.util.fastForEach
 import kotlin.math.max
 
 /**
@@ -245,25 +247,42 @@
             content()
         }
     }) { measurables, constraints ->
-        require(measurables.size == 1) {
-            "text for Snackbar expected to have exactly only one child"
+        val textPlaceables = ArrayList<Placeable>(measurables.size)
+        var firstBaseline = AlignmentLine.Unspecified
+        var lastBaseline = AlignmentLine.Unspecified
+        var height = 0
+
+        measurables.fastForEach {
+            val placeable = it.measure(constraints)
+            textPlaceables.add(placeable)
+            if (placeable[FirstBaseline] != AlignmentLine.Unspecified &&
+                (firstBaseline == AlignmentLine.Unspecified ||
+                    placeable[FirstBaseline] < firstBaseline)) {
+                firstBaseline = placeable[FirstBaseline]
+            }
+            if (placeable[LastBaseline] != AlignmentLine.Unspecified &&
+                (lastBaseline == AlignmentLine.Unspecified ||
+                    placeable[LastBaseline] > lastBaseline)) {
+                lastBaseline = placeable[LastBaseline]
+            }
+            height = max(height, placeable.height)
         }
-        val textPlaceable = measurables.first().measure(constraints)
-        val firstBaseline = textPlaceable[FirstBaseline]
-        val lastBaseline = textPlaceable[LastBaseline]
-        require(firstBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
-        require(lastBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
+
+        val hasText = firstBaseline != AlignmentLine.Unspecified &&
+            lastBaseline != AlignmentLine.Unspecified
 
         val minHeight =
-            if (firstBaseline == lastBaseline) {
+            if (firstBaseline == lastBaseline || !hasText) {
                 SnackbarMinHeightOneLine
             } else {
                 SnackbarMinHeightTwoLines
             }
-        val containerHeight = max(minHeight.roundToPx(), textPlaceable.height)
+        val containerHeight = max(minHeight.roundToPx(), height)
         layout(constraints.maxWidth, containerHeight) {
-            val textPlaceY = (containerHeight - textPlaceable.height) / 2
-            textPlaceable.placeRelative(0, textPlaceY)
+            textPlaceables.fastForEach {
+                val textPlaceY = (containerHeight - it.height) / 2
+                it.placeRelative(0, textPlaceY)
+            }
         }
     }
 }
@@ -316,10 +335,10 @@
         )
 
         val firstTextBaseline = textPlaceable[FirstBaseline]
-        require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
         val lastTextBaseline = textPlaceable[LastBaseline]
-        require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
-        val isOneLine = firstTextBaseline == lastTextBaseline
+        val hasText = firstTextBaseline != AlignmentLine.Unspecified &&
+            lastTextBaseline != AlignmentLine.Unspecified
+        val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
         val buttonPlaceX = constraints.maxWidth - buttonPlaceable.width
 
         val textPlaceY: Int
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
index 9aa622d..571508f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
@@ -29,6 +29,10 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -47,6 +51,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -188,50 +193,68 @@
 
     @Test
     fun dismissibleNavigationDrawer_testOffset_customWidthLarger_whenOpen() {
-        val customWidth = NavigationDrawerWidth + 5.dp
+        val customWidth = NavigationDrawerWidth + 20.dp
+        val density = Density(0.5f)
+        lateinit var coords: LayoutCoordinates
         rule.setMaterialContent(lightColorScheme()) {
-            val drawerState = rememberDrawerState(DrawerValue.Open)
-            DismissibleNavigationDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    DismissibleDrawerSheet(Modifier.width(customWidth)) {
-                        Box(
-                            Modifier
-                                .fillMaxSize()
-                                .testTag("content")
-                        )
-                    }
-                },
-                content = {}
-            )
+            // Reduce density to ensure wide drawer fits on screen
+            CompositionLocalProvider(LocalDensity provides density) {
+                val drawerState = rememberDrawerState(DrawerValue.Open)
+                DismissibleNavigationDrawer(
+                    drawerState = drawerState,
+                    drawerContent = {
+                        DismissibleDrawerSheet(Modifier.width(customWidth)) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize()
+                                    .onGloballyPositioned {
+                                        coords = it
+                                    }
+                            )
+                        }
+                    },
+                    content = {}
+                )
+            }
         }
 
-        rule.onNodeWithTag("content")
-            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.runOnIdle {
+            assertThat(coords.positionOnScreen().x).isEqualTo(0f)
+        }
     }
 
     @Test
     fun dismissibleNavigationDrawer_testOffset_customWidthLarger_whenClosed() {
-        val customWidth = NavigationDrawerWidth + 5.dp
+        val customWidth = NavigationDrawerWidth + 20.dp
+        val density = Density(0.5f)
+        lateinit var coords: LayoutCoordinates
         rule.setMaterialContent(lightColorScheme()) {
-            val drawerState = rememberDrawerState(DrawerValue.Closed)
-            DismissibleNavigationDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    DismissibleDrawerSheet(Modifier.width(customWidth)) {
-                        Box(
-                            Modifier
-                                .fillMaxSize()
-                                .testTag("content")
-                        )
-                    }
-                },
-                content = {}
-            )
+            // Reduce density to ensure wide drawer fits on screen
+            CompositionLocalProvider(LocalDensity provides density) {
+                val drawerState = rememberDrawerState(DrawerValue.Closed)
+                DismissibleNavigationDrawer(
+                    drawerState = drawerState,
+                    drawerContent = {
+                        DismissibleDrawerSheet(Modifier.width(customWidth)) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize()
+                                    .onGloballyPositioned {
+                                        coords = it
+                                    }
+                            )
+                        }
+                    },
+                    content = {}
+                )
+            }
         }
 
-        rule.onNodeWithTag("content")
-            .assertLeftPositionInRootIsEqualTo(-customWidth)
+        rule.runOnIdle {
+            with(density) {
+                assertThat(coords.positionOnScreen().x).isWithin(1f).of(-customWidth.toPx())
+            }
+        }
     }
 
     @Test
@@ -416,12 +439,16 @@
         // When the drawer state is set to Opened
         drawerState.snapTo(DrawerValue.Open)
         // Then the drawer should be opened
-        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+        }
 
         // When the drawer state is set to Closed
         drawerState.snapTo(DrawerValue.Closed)
         // Then the drawer should be closed
-        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+        }
     }
 
     @Test
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
index 587382f..97f3407 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
@@ -29,6 +29,10 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -49,6 +53,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -167,50 +172,68 @@
 
     @Test
     fun navigationDrawer_testOffset_customWidthLarger_whenOpen() {
-        val customWidth = NavigationDrawerWidth + 5.dp
+        val customWidth = NavigationDrawerWidth + 20.dp
+        val density = Density(0.5f)
+        lateinit var coords: LayoutCoordinates
         rule.setMaterialContent(lightColorScheme()) {
-            val drawerState = rememberDrawerState(DrawerValue.Open)
-            ModalNavigationDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    ModalDrawerSheet(Modifier.width(customWidth)) {
-                        Box(
-                            Modifier
-                                .fillMaxSize()
-                                .testTag("content")
-                        )
-                    }
-                },
-                content = {}
-            )
+            // Reduce density to ensure wide drawer fits on screen
+            CompositionLocalProvider(LocalDensity provides density) {
+                val drawerState = rememberDrawerState(DrawerValue.Open)
+                ModalNavigationDrawer(
+                    drawerState = drawerState,
+                    drawerContent = {
+                        ModalDrawerSheet(Modifier.width(customWidth)) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize()
+                                    .onGloballyPositioned {
+                                        coords = it
+                                    }
+                            )
+                        }
+                    },
+                    content = {}
+                )
+            }
         }
 
-        rule.onNodeWithTag("content")
-            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.runOnIdle {
+            assertThat(coords.positionOnScreen().x).isEqualTo(0f)
+        }
     }
 
     @Test
     fun navigationDrawer_testOffset_customWidthLarger_whenClosed() {
-        val customWidth = NavigationDrawerWidth + 5.dp
+        val customWidth = NavigationDrawerWidth + 20.dp
+        val density = Density(0.5f)
+        lateinit var coords: LayoutCoordinates
         rule.setMaterialContent(lightColorScheme()) {
-            val drawerState = rememberDrawerState(DrawerValue.Closed)
-            ModalNavigationDrawer(
-                drawerState = drawerState,
-                drawerContent = {
-                    ModalDrawerSheet(Modifier.width(customWidth)) {
-                        Box(
-                            Modifier
-                                .fillMaxSize()
-                                .testTag("content")
-                        )
-                    }
-                },
-                content = {}
-            )
+            // Reduce density to ensure wide drawer fits on screen
+            CompositionLocalProvider(LocalDensity provides density) {
+                val drawerState = rememberDrawerState(DrawerValue.Closed)
+                ModalNavigationDrawer(
+                    drawerState = drawerState,
+                    drawerContent = {
+                        ModalDrawerSheet(Modifier.width(customWidth)) {
+                            Box(
+                                Modifier
+                                    .fillMaxSize()
+                                    .onGloballyPositioned {
+                                        coords = it
+                                    }
+                            )
+                        }
+                    },
+                    content = {}
+                )
+            }
         }
 
-        rule.onNodeWithTag("content")
-            .assertLeftPositionInRootIsEqualTo(-customWidth)
+        rule.runOnIdle {
+            with(density) {
+                assertThat(coords.positionOnScreen().x).isWithin(1f).of(-customWidth.toPx())
+            }
+        }
     }
 
     @Test
@@ -419,12 +442,16 @@
         // When the drawer state is set to Opened
         drawerState.snapTo(DrawerValue.Open)
         // Then the drawer should be opened
-        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Open)
+        }
 
         // When the drawer state is set to Closed
         drawerState.snapTo(DrawerValue.Closed)
         // Then the drawer should be closed
-        assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+        rule.runOnIdle {
+            assertThat(drawerState.currentValue).isEqualTo(DrawerValue.Closed)
+        }
     }
 
     @Test
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
index 50e761f..38861c1 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SnackbarTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material3
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.material3.internal.Strings
 import androidx.compose.material3.internal.getString
@@ -88,6 +89,29 @@
     }
 
     @Test
+    fun snackbar_emptyContent() {
+        rule.setMaterialContent(lightColorScheme()) {
+            Snackbar {
+                // empty content should not crash
+            }
+        }
+    }
+
+    @Test
+    fun snackbar_noTextInContent() {
+        val snackbarHeight = 48.dp
+        val contentSize = 20.dp
+        rule.setMaterialContent(lightColorScheme()) {
+            Snackbar {
+                // non-text content should not crash
+                Box(Modifier.testTag("content").size(contentSize))
+            }
+        }
+        rule.onNodeWithTag("content")
+            .assertTopPositionInRootIsEqualTo((snackbarHeight - contentSize) / 2)
+    }
+
+    @Test
     fun snackbar_withDismiss_semantics() {
         var clicked = false
         val snackbarVisuals =
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
index 644850d5..ed987ae 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt
@@ -119,29 +119,21 @@
         val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value
         CompositionLocalProvider(LocalTextStyle provides textStyle) {
             when {
-                action == null -> OneRowSnackbar(
+                actionOnNewLine && action != null -> NewLineButtonSnackbar(
                     text = content,
-                    action = null,
+                    action = action,
                     dismissAction = dismissAction,
-                    actionTextStyle,
-                    actionContentColor,
-                    dismissActionContentColor
-                )
-                actionOnNewLine -> NewLineButtonSnackbar(
-                    content,
-                    action,
-                    dismissAction,
-                    actionTextStyle,
-                    actionContentColor,
-                    dismissActionContentColor
+                    actionTextStyle = actionTextStyle,
+                    actionContentColor = actionContentColor,
+                    dismissActionContentColor = dismissActionContentColor,
                 )
                 else -> OneRowSnackbar(
                     text = content,
                     action = action,
                     dismissAction = dismissAction,
-                    actionTextStyle,
-                    actionContentColor,
-                    dismissActionContentColor
+                    actionTextStyle = actionTextStyle,
+                    actionTextColor = actionContentColor,
+                    dismissActionColor = dismissActionContentColor,
                 )
             }
         }
@@ -353,10 +345,10 @@
         )
 
         val firstTextBaseline = textPlaceable[FirstBaseline]
-        require(firstTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
         val lastTextBaseline = textPlaceable[LastBaseline]
-        require(lastTextBaseline != AlignmentLine.Unspecified) { "No baselines for text" }
-        val isOneLine = firstTextBaseline == lastTextBaseline
+        val hasText = firstTextBaseline != AlignmentLine.Unspecified &&
+            lastTextBaseline != AlignmentLine.Unspecified
+        val isOneLine = firstTextBaseline == lastTextBaseline || !hasText
         val dismissButtonPlaceX = containerWidth - dismissButtonWidth
         val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
index df5f37a..95ea8e8 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/BroadcastFrameClock.kt
@@ -82,14 +82,13 @@
     override suspend fun <R> withFrameNanos(
         onFrame: (Long) -> R
     ): R = suspendCancellableCoroutine { co ->
-        lateinit var awaiter: FrameAwaiter<R>
+        val awaiter = FrameAwaiter(onFrame, co)
         val hasNewAwaiters = synchronized(lock) {
             val cause = failureCause
             if (cause != null) {
                 co.resumeWithException(cause)
                 return@suspendCancellableCoroutine
             }
-            awaiter = FrameAwaiter(onFrame, co)
             val hadAwaiters = awaiters.isNotEmpty()
             awaiters.add(awaiter)
             if (!hadAwaiters) hasAwaitersUnlocked.set(1)
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
index 28e755c..f2833bd 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/layer/LayerManager.android.kt
@@ -20,6 +20,7 @@
 import android.media.ImageReader
 import android.os.Build
 import android.os.Looper
+import android.os.Message
 import android.view.Surface
 import androidx.annotation.RequiresApi
 import androidx.collection.ObjectList
@@ -55,7 +56,13 @@
         if (!layerList.contains(layer)) {
             layerList.add(layer)
             if (!handler.hasMessages(0)) {
-                handler.sendEmptyMessage(0)
+                // we don't run persistLayers() synchronously in order to do less work as there
+                // might be a lot of new layers created during one frame. however we also want
+                // to execute it as soon as possible to be able to persist the layers before
+                // they discard their content. it is possible that there is some other work
+                // scheduled on the main thread which is going to change what layers are drawn.
+                // we use sendMessageAtFrontOfQueue() in order to be executed before that.
+                handler.sendMessageAtFrontOfQueue(Message.obtain())
             }
         }
     }
@@ -80,7 +87,11 @@
                 1,
                 PixelFormat.RGBA_8888,
                 1
-            ).also { imageReader = it }
+            ).apply {
+                // We don't care about the result, but release the buffer back to the queue
+                // for subsequent renders to ensure the RenderThread is free as much as possible
+                setOnImageAvailableListener({ it?.acquireLatestImage()?.close() }, handler)
+            }.also { imageReader = it }
             val surface = reader.surface
             val canvas = LockHardwareCanvasHelper.lockHardwareCanvas(surface)
             // on Robolectric even this canvas is not hardware accelerated and drawing render nodes
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
index d327689..d9faada 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Canvas.kt
@@ -135,6 +135,7 @@
  * Add a rotation (in radians clockwise) to the current transform at the given pivot point.
  * The pivot coordinate remains unchanged by the rotation transformation
  *
+ * @param radians Rotation transform to apply to the [Canvas]
  * @param pivotX The x-coord for the pivot point
  * @param pivotY The y-coord for the pivot point
  */
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
index 469f415..9a77328 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Color.kt
@@ -335,6 +335,7 @@
          * 240 is blue
          * @param saturation The amount of [hue] represented in the color in the range (0..1),
          * where 0 has no color and 1 is fully saturated.
+         * @param alpha Alpha channel to apply to the computed color
          * @param value The strength of the color, where 0 is black.
          * @param colorSpace The RGB color space used to calculate the Color from the HSV values.
          */
@@ -370,6 +371,7 @@
          * where 0 has no color and 1 is fully saturated.
          * @param lightness A range of (0..1) where 0 is black, 0.5 is fully colored, and 1 is
          * white.
+         * @param alpha Alpha channel to apply to the computed color
          * @param colorSpace The RGB color space used to calculate the Color from the HSL values.
          */
         fun hsl(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
index bb9218b..70223b4 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ColorMatrix.kt
@@ -92,6 +92,7 @@
      * is represented as a 4 x 5 matrix
      * @param column Column index to query the ColorMatrix value. Range is from 0 to 4 as
      * [ColorMatrix] is represented as a 4 x 5 matrix
+     * @param v value to update at the given [row] and [column]
      */
     inline operator fun set(row: Int, column: Int, v: Float) {
         values[(row * 5) + column] = v
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
index a624bad..bffc12d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Path.kt
@@ -367,6 +367,7 @@
      *
      * @param path1 The first operand (for difference, the minuend)
      * @param path2 The second operand (for difference, the subtrahend)
+     * @param operation [PathOperation] to apply to the 2 specified paths
      *
      * @return True if operation succeeded, false otherwise and this path remains unmodified.
      */
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
index 30ebfe3..3e2848c 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/RenderEffect.kt
@@ -50,6 +50,7 @@
  * [RenderEffect] that will blur the contents of an optional input [RenderEffect]. If no
  * input [RenderEffect] is provided, the drawing commands on the [GraphicsLayerScope] this
  * [RenderEffect] is configured on will be blurred.
+ * @param renderEffect Optional input [RenderEffect] to be blurred
  * @param radiusX Blur radius in the horizontal direction
  * @param radiusY Blur radius in the vertical direction
  * @param edgeTreatment Strategy used to render pixels outside of bounds of the original input
diff --git a/compose/ui/ui-text/lint-baseline.xml b/compose/ui/ui-text/lint-baseline.xml
index e34eca2..d8802c0 100644
--- a/compose/ui/ui-text/lint-baseline.xml
+++ b/compose/ui/ui-text/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.3.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.0-beta01)" variant="all" version="8.3.0-beta01">
+<issues format="6" by="lint 8.5.0-alpha03" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.0-alpha03)" variant="all" version="8.5.0-alpha03">
 
     <issue
         id="NewApi"
@@ -12,7 +12,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -30,7 +30,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(paragraph.textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
@@ -48,7 +48,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.SrcOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
         <location
@@ -75,7 +75,7 @@
 
     <issue
         id="NewApi"
-        message="Cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
+        message="Implicit cast from `BlendMode` to `Comparable` requires API level 29 (current min is 21)"
         errorLine1="        assertThat(textPaint.blendMode).isEqualTo(BlendMode.DstOver)"
         errorLine2="                   ~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/compose/ui/ui-unit/api/current.txt b/compose/ui/ui-unit/api/current.txt
index 8af3326..3e20eda 100644
--- a/compose/ui/ui-unit/api/current.txt
+++ b/compose/ui/ui-unit/api/current.txt
@@ -132,8 +132,8 @@
     method public long copy(optional float x, optional float y);
     method public float getX();
     method public float getY();
-    method @androidx.compose.runtime.Stable public inline operator long minus(long other);
-    method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+    method @androidx.compose.runtime.Stable public operator long minus(long other);
+    method @androidx.compose.runtime.Stable public operator long plus(long other);
     property @androidx.compose.runtime.Stable public final float x;
     property @androidx.compose.runtime.Stable public final float y;
     field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -176,8 +176,8 @@
     method @androidx.compose.runtime.Stable public operator long div(int other);
     method public float getHeight();
     method public float getWidth();
-    method @androidx.compose.runtime.Stable public inline operator long minus(long other);
-    method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+    method @androidx.compose.runtime.Stable public operator long minus(long other);
+    method @androidx.compose.runtime.Stable public operator long plus(long other);
     method @androidx.compose.runtime.Stable public operator long times(float other);
     method @androidx.compose.runtime.Stable public operator long times(int other);
     property @androidx.compose.runtime.Stable public final float height;
diff --git a/compose/ui/ui-unit/api/restricted_current.txt b/compose/ui/ui-unit/api/restricted_current.txt
index d3e260a..7d41176 100644
--- a/compose/ui/ui-unit/api/restricted_current.txt
+++ b/compose/ui/ui-unit/api/restricted_current.txt
@@ -132,8 +132,8 @@
     method public long copy(optional float x, optional float y);
     method public float getX();
     method public float getY();
-    method @androidx.compose.runtime.Stable public inline operator long minus(long other);
-    method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+    method @androidx.compose.runtime.Stable public operator long minus(long other);
+    method @androidx.compose.runtime.Stable public operator long plus(long other);
     property @androidx.compose.runtime.Stable public final float x;
     property @androidx.compose.runtime.Stable public final float y;
     field public static final androidx.compose.ui.unit.DpOffset.Companion Companion;
@@ -176,8 +176,8 @@
     method @androidx.compose.runtime.Stable public operator long div(int other);
     method public float getHeight();
     method public float getWidth();
-    method @androidx.compose.runtime.Stable public inline operator long minus(long other);
-    method @androidx.compose.runtime.Stable public inline operator long plus(long other);
+    method @androidx.compose.runtime.Stable public operator long minus(long other);
+    method @androidx.compose.runtime.Stable public operator long plus(long other);
     method @androidx.compose.runtime.Stable public operator long times(float other);
     method @androidx.compose.runtime.Stable public operator long times(int other);
     property @androidx.compose.runtime.Stable public final float height;
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
index 5c5102f..e2d9797 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/Dp.kt
@@ -269,21 +269,29 @@
      * Returns a copy of this [DpOffset] instance optionally overriding the
      * x or y parameter
      */
-    fun copy(x: Dp = this.x, y: Dp = this.y): DpOffset = DpOffset(x, y)
+    fun copy(x: Dp = this.x, y: Dp = this.y): DpOffset = DpOffset(packFloats(x.value, y.value))
 
     /**
      * Subtract a [DpOffset] from another one.
      */
     @Stable
-    inline operator fun minus(other: DpOffset) =
-        DpOffset(x - other.x, y - other.y)
+    operator fun minus(other: DpOffset) = DpOffset(
+        packFloats(
+            (x - other.x).value,
+            (y - other.y).value
+        )
+    )
 
     /**
      * Add a [DpOffset] to another one.
      */
     @Stable
-    inline operator fun plus(other: DpOffset) =
-        DpOffset(x + other.x, y + other.y)
+    operator fun plus(other: DpOffset) = DpOffset(
+        packFloats(
+            (x + other.x).value,
+            (y + other.y).value
+        )
+    )
 
     @Stable
     override fun toString(): String =
@@ -297,14 +305,14 @@
         /**
          * A [DpOffset] with 0 DP [x] and 0 DP [y] values.
          */
-        val Zero = DpOffset(0.dp, 0.dp)
+        val Zero = DpOffset(0x0L)
 
         /**
          * Represents an offset whose [x] and [y] are unspecified. This is usually a replacement for
          * `null` when a primitive value is desired.
          * Access to [x] or [y] on an unspecified offset is not allowed.
          */
-        val Unspecified = DpOffset(Dp.Unspecified, Dp.Unspecified)
+        val Unspecified = DpOffset(0x7fc00000_7fc00000L)
     }
 }
 
@@ -342,7 +350,12 @@
  */
 @Stable
 fun lerp(start: DpOffset, stop: DpOffset, fraction: Float): DpOffset =
-    DpOffset(lerp(start.x, stop.x, fraction), lerp(start.y, stop.y, fraction))
+    DpOffset(
+        packFloats(
+            lerp(start.x.value, stop.x.value, fraction),
+            lerp(start.y.value, stop.y.value, fraction)
+        )
+    )
 
 /**
  * Constructs a [DpSize] from [width] and [height] [Dp] values.
@@ -372,21 +385,33 @@
      * Returns a copy of this [DpSize] instance optionally overriding the
      * width or height parameter
      */
-    fun copy(width: Dp = this.width, height: Dp = this.height): DpSize = DpSize(width, height)
+    fun copy(width: Dp = this.width, height: Dp = this.height): DpSize = DpSize(
+        packFloats(width.value, height.value)
+    )
 
     /**
      * Subtract a [DpSize] from another one.
      */
     @Stable
-    inline operator fun minus(other: DpSize) =
-        DpSize(width - other.width, height - other.height)
+    operator fun minus(other: DpSize) =
+        DpSize(
+            packFloats(
+                (width - other.width).value,
+                (height - other.height).value
+            )
+        )
 
     /**
      * Add a [DpSize] to another one.
      */
     @Stable
-    inline operator fun plus(other: DpSize) =
-        DpSize(width + other.width, height + other.height)
+    operator fun plus(other: DpSize) =
+        DpSize(
+            packFloats(
+                (width + other.width).value,
+                (height + other.height).value
+            )
+        )
 
     @Stable
     inline operator fun component1(): Dp = width
@@ -395,16 +420,36 @@
     inline operator fun component2(): Dp = height
 
     @Stable
-    operator fun times(other: Int): DpSize = DpSize(width * other, height * other)
+    operator fun times(other: Int): DpSize = DpSize(
+        packFloats(
+            (width * other).value,
+            (height * other).value
+        )
+    )
 
     @Stable
-    operator fun times(other: Float): DpSize = DpSize(width * other, height * other)
+    operator fun times(other: Float): DpSize = DpSize(
+        packFloats(
+            (width * other).value,
+            (height * other).value
+        )
+    )
 
     @Stable
-    operator fun div(other: Int): DpSize = DpSize(width / other, height / other)
+    operator fun div(other: Int): DpSize = DpSize(
+        packFloats(
+            (width / other).value,
+            (height / other).value
+        )
+    )
 
     @Stable
-    operator fun div(other: Float): DpSize = DpSize(width / other, height / other)
+    operator fun div(other: Float): DpSize = DpSize(
+        packFloats(
+            (width / other).value,
+            (height / other).value
+        )
+    )
 
     @Stable
     override fun toString(): String =
@@ -418,14 +463,14 @@
         /**
          * A [DpSize] with 0 DP [width] and 0 DP [height] values.
          */
-        val Zero = DpSize(0.dp, 0.dp)
+        val Zero = DpSize(0x0L)
 
         /**
          * A size whose [width] and [height] are unspecified. This is usually a replacement for
          * `null` when a primitive value is desired.
          * Access to [width] or [height] on an unspecified size is not allowed.
          */
-        val Unspecified = DpSize(Dp.Unspecified, Dp.Unspecified)
+        val Unspecified = DpSize(0x7fc00000_7fc00000L)
     }
 }
 
@@ -456,7 +501,12 @@
  */
 @Stable
 val DpSize.center: DpOffset
-    get() = DpOffset(width / 2f, height / 2f)
+    get() = DpOffset(
+        packFloats(
+            (width / 2f).value,
+            (height / 2f).value
+        )
+    )
 
 @Stable
 inline operator fun Int.times(size: DpSize) = size * this
@@ -476,7 +526,12 @@
  */
 @Stable
 fun lerp(start: DpSize, stop: DpSize, fraction: Float): DpSize =
-    DpSize(lerp(start.width, stop.width, fraction), lerp(start.height, stop.height, fraction))
+    DpSize(
+        packFloats(
+            lerp(start.width, stop.width, fraction).value,
+            lerp(start.height, stop.height, fraction).value
+        )
+    )
 
 /**
  * A four dimensional bounds using [Dp] for units
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index c63e2a9..7af8753 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -252,236 +252,6 @@
         assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
     }
 
-    // Inserts a new Node at the top of an existing branch (tests removal of duplicate Nodes too).
-    @Test
-    fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithOnePointerId_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId1, listOf(pifNew1, pif1, pif2, pif3, pif4))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pifNew1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif1).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif2).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif3).apply {
-                                            pointerIds.add(pointerId1)
-                                            children.add(
-                                                Node(pif4).apply {
-                                                    pointerIds.add(pointerId1)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    @Test
-    fun addHitPath_dynamicNodeAddedTopPartiallyMatchingTreeWithTwoPointerIds_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pif5 = PointerInputNodeMock()
-        val pif6 = PointerInputNodeMock()
-        val pif7 = PointerInputNodeMock()
-        val pif8 = PointerInputNodeMock()
-
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        val pointerId2 = PointerId(2)
-
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
-        hitPathTracker.addHitPath(pointerId2, listOf(pifNew1, pif5, pif6, pif7, pif8))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif2).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif3).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif4).apply {
-                                            pointerIds.add(pointerId1)
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-
-            children.add(
-                Node(pifNew1).apply {
-                    pointerIds.add(pointerId2)
-                    children.add(
-                        Node(pif5).apply {
-                            pointerIds.add(pointerId2)
-                            children.add(
-                                Node(pif6).apply {
-                                    pointerIds.add(pointerId2)
-                                    children.add(
-                                        Node(pif7).apply {
-                                            pointerIds.add(pointerId2)
-                                            children.add(
-                                                Node(pif8).apply {
-                                                    pointerIds.add(pointerId2)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    // Inserts a new Node inside an existing branch (tests removal of duplicate Nodes too).
-    @Test
-    fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithOnePointerId_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pifNew1, pif2, pif3, pif4))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pifNew1).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif2).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif3).apply {
-                                            pointerIds.add(pointerId1)
-                                            children.add(
-                                                Node(pif4).apply {
-                                                    pointerIds.add(pointerId1)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
-    @Test
-    fun addHitPath_dynamicNodeAddedInsidePartiallyMatchingTreeWithTwoPointerIds_correctResult() {
-        val pif1 = PointerInputNodeMock()
-        val pif2 = PointerInputNodeMock()
-        val pif3 = PointerInputNodeMock()
-        val pif4 = PointerInputNodeMock()
-        val pif5 = PointerInputNodeMock()
-        val pif6 = PointerInputNodeMock()
-        val pif7 = PointerInputNodeMock()
-        val pif8 = PointerInputNodeMock()
-
-        val pifNew1 = PointerInputNodeMock()
-
-        val pointerId1 = PointerId(1)
-        val pointerId2 = PointerId(2)
-
-        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
-
-        hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pifNew1, pif7, pif8))
-
-        val expectedRoot = NodeParent().apply {
-            children.add(
-                Node(pif1).apply {
-                    pointerIds.add(pointerId1)
-                    children.add(
-                        Node(pif2).apply {
-                            pointerIds.add(pointerId1)
-                            children.add(
-                                Node(pif3).apply {
-                                    pointerIds.add(pointerId1)
-                                    children.add(
-                                        Node(pif4).apply {
-                                            pointerIds.add(pointerId1)
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-
-            children.add(
-                Node(pif5).apply {
-                    pointerIds.add(pointerId2)
-                    children.add(
-                        Node(pif6).apply {
-                            pointerIds.add(pointerId2)
-                            children.add(
-                                Node(pifNew1).apply {
-                                    pointerIds.add(pointerId2)
-                                    children.add(
-                                        Node(pif7).apply {
-                                            pointerIds.add(pointerId2)
-                                            children.add(
-                                                Node(pif8).apply {
-                                                    pointerIds.add(pointerId2)
-                                                }
-                                            )
-                                        }
-                                    )
-                                }
-                            )
-                        }
-                    )
-                }
-            )
-        }
-        assertThat(areEqual(hitPathTracker.root, expectedRoot)).isTrue()
-    }
-
     // Inserts a Node in the bottom of an existing branch (tests removal of duplicate Nodes too).
     @Test
     fun addHitPath_dynamicNodeAddedBelowPartiallyMatchingTreeWithOnePointerId_correctResult() {
@@ -492,8 +262,16 @@
         val pifNew1 = PointerInputNodeMock()
 
         val pointerId1 = PointerId(1)
+        // Modifier.Node(s) hit by the first pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
+        // Clear any old hits from previous calls (does not really apply here since it's the first
+        // call)
+        hitPathTracker.removeDetachedPointerInputNodes()
+
+        // Modifier.Node(s) hit by the second pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4, pifNew1))
+        // Clear any old hits from previous calls
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -541,10 +319,18 @@
         val pointerId1 = PointerId(1)
         val pointerId2 = PointerId(2)
 
+        // Modifier.Node(s) hit by the first pointer input event
         hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
         hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8))
+        // Clear any old hits from previous calls (does not really apply here since it's the first
+        // call)
+        hitPathTracker.removeDetachedPointerInputNodes()
 
+        // Modifier.Node(s) hit by the second pointer input event
+        hitPathTracker.addHitPath(pointerId1, listOf(pif1, pif2, pif3, pif4))
         hitPathTracker.addHitPath(pointerId2, listOf(pif5, pif6, pif7, pif8, pifNew1))
+        // Clear any old hits from previous calls
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1335,7 +1121,7 @@
     @Test
     fun removeDetachedPointerInputFilters_noNodes_hitResultJustHasRootAndDoesNotCrash() {
         val throwable = catchThrowable {
-            hitPathTracker.removeDetachedPointerInputFilters()
+            hitPathTracker.removeDetachedPointerInputNodes()
         }
 
         assertThat(throwable).isNull()
@@ -1373,7 +1159,7 @@
 
         // Act.
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         // Assert.
 
@@ -1451,7 +1237,7 @@
 
         hitPathTracker.addHitPath(PointerId(0), listOf(root, middle, leaf))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         assertThat(areEqual(hitPathTracker.root, NodeParent())).isTrue()
 
@@ -1478,7 +1264,7 @@
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, child))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1511,7 +1297,7 @@
         val pointerId = PointerId(0)
         hitPathTracker.addHitPath(pointerId, listOf(root, middle, leaf))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1570,7 +1356,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1648,7 +1434,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1727,7 +1513,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1825,7 +1611,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1897,7 +1683,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -1971,7 +1757,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2070,7 +1856,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent()
 
@@ -2135,7 +1921,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2204,7 +1990,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2294,7 +2080,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root2, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root3, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2371,7 +2157,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent()
 
@@ -2444,7 +2230,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2524,7 +2310,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2602,7 +2388,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle2, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle3, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2654,7 +2440,7 @@
         hitPathTracker.addHitPath(pointerId2, listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(pointerId3, listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2721,7 +2507,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
@@ -2787,7 +2573,7 @@
         hitPathTracker.addHitPath(PointerId(5), listOf(root, middle, leaf2))
         hitPathTracker.addHitPath(PointerId(7), listOf(root, middle, leaf3))
 
-        hitPathTracker.removeDetachedPointerInputFilters()
+        hitPathTracker.removeDetachedPointerInputNodes()
 
         val expectedRoot = NodeParent().apply {
             children.add(
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..1fac1b6
--- /dev/null
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/SharePointerInputWithSiblingTest.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.node
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.DrawerValue
+import androidx.compose.material.ModalDrawer
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.IntSize
+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 org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SharePointerInputWithSiblingTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun Drawer_drawerContentSharePointerInput_cantClickContent() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            val drawerState = rememberDrawerState(DrawerValue.Open)
+
+            ModalDrawer(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box1")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box1Clicked = true
+                        }
+                    )
+                },
+                content = {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            )
+        }
+
+        rule.onNodeWithTag("box1").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isFalse()
+    }
+
+    @Test
+    fun stackedBox_doSharePointer() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box2Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_parentDisallowShare_doSharePointerWithSibling() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier
+                .size(50.dp)
+                .testPointerInput(sharePointerInputWithSibling = false)
+            ) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box2Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_doSharePointerWithCousin() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier
+                    .fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()) {
+                    Box(Modifier
+                        .fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isTrue()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_parentDisallowShare_notSharePointerWithCousin() {
+        var box1Clicked = false
+        var box2Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier.fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testPointerInput(sharePointerInputWithSibling = false)
+                ) {
+                    Box(Modifier.fillMaxSize()
+                        .testTag("box2")
+                        .testPointerInput(sharePointerInputWithSibling = true) {
+                            box2Clicked = true
+                        }
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("box2").performClick()
+        assertThat(box1Clicked).isFalse()
+        assertThat(box2Clicked).isTrue()
+    }
+
+    @Test
+    fun stackedBox_doSharePointer_untilFirstBoxDisallowShare() {
+        var box1Clicked = false
+        var box2Clicked = false
+        var box3Clicked = false
+
+        rule.setContent {
+            Box(Modifier.size(50.dp)) {
+                Box(Modifier.fillMaxSize()
+                    .testTag("box1")
+                    .testPointerInput {
+                        box1Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testTag("box2")
+                    .testPointerInput(sharePointerInputWithSibling = false) {
+                        box2Clicked = true
+                    }
+                )
+
+                Box(Modifier.fillMaxSize()
+                    .testTag("box3")
+                    .testPointerInput(sharePointerInputWithSibling = true) {
+                        box3Clicked = true
+                    }
+                )
+            }
+        }
+
+        rule.onNodeWithTag("box3").performClick()
+        assertThat(box1Clicked).isFalse()
+        assertThat(box2Clicked).isTrue()
+        assertThat(box3Clicked).isTrue()
+    }
+}
+
+private fun Modifier.testPointerInput(
+    sharePointerInputWithSibling: Boolean = false,
+    onPointerEvent: () -> Unit = {}
+): Modifier = this.then(TestPointerInputElement(sharePointerInputWithSibling, onPointerEvent))
+
+private data class TestPointerInputElement(
+    val sharePointerInputWithSibling: Boolean,
+    val onPointerEvent: () -> Unit
+) : ModifierNodeElement<TestPointerInputNode>() {
+    override fun create(): TestPointerInputNode {
+        return TestPointerInputNode(sharePointerInputWithSibling, onPointerEvent)
+    }
+
+    override fun update(node: TestPointerInputNode) {
+        node.sharePointerInputWithSibling = sharePointerInputWithSibling
+        node.onPointerEvent = onPointerEvent
+    }
+}
+
+private class TestPointerInputNode(
+    var sharePointerInputWithSibling: Boolean,
+    var onPointerEvent: () -> Unit
+) : Modifier.Node(), PointerInputModifierNode {
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {
+        onPointerEvent.invoke()
+    }
+
+    override fun onCancelPointerInput() { }
+
+    override fun sharePointerInputWithSiblings(): Boolean {
+        return sharePointerInputWithSibling
+    }
+}
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 3631b76..d4a106f 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
@@ -26,6 +26,7 @@
 import android.os.Build.VERSION_CODES.M
 import android.os.Build.VERSION_CODES.N
 import android.os.Build.VERSION_CODES.O
+import android.os.Build.VERSION_CODES.P
 import android.os.Build.VERSION_CODES.Q
 import android.os.Build.VERSION_CODES.S
 import android.os.Looper
@@ -1426,20 +1427,22 @@
             return layer
         }
 
+        // enable new layers on versions supporting render nodes
+        if (isHardwareAccelerated && SDK_INT >= M && SDK_INT != P) {
+            return GraphicsLayerOwnerLayer(
+                graphicsLayer = graphicsContext.createGraphicsLayer(),
+                context = graphicsContext,
+                ownerView = this,
+                drawBlock = drawBlock,
+                invalidateParentLayer = invalidateParentLayer
+            )
+        }
+
         // RenderNode is supported on Q+ for certain, but may also be supported on M-O.
         // We can't be confident that RenderNode is supported, so we try and fail over to
         // the ViewLayer implementation. We'll try even on on P devices, but it will fail
         // until ART allows things on the unsupported list on P.
         if (isHardwareAccelerated && SDK_INT >= M && isRenderNodeCompatible) {
-            if (SDK_INT >= Q) {
-                return GraphicsLayerOwnerLayer(
-                    graphicsLayer = graphicsContext.createGraphicsLayer(),
-                    context = graphicsContext,
-                    ownerView = this,
-                    drawBlock = drawBlock,
-                    invalidateParentLayer = invalidateParentLayer
-                )
-            }
             try {
                 return RenderNodeLayer(
                     this,
diff --git a/compose/ui/ui/src/androidMain/res/values-af/strings.xml b/compose/ui/ui/src/androidMain/res/values-af/strings.xml
index d4ff363..374463d 100644
--- a/compose/ui/ui/src/androidMain/res/values-af/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-af/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Maak navigasiekieslys toe"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Maak sigblad toe"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ongeldige invoer"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Opspringvenster"</string>
     <string name="range_start" msgid="7097486360902471446">"Begingrens"</string>
     <string name="range_end" msgid="5941395253238309765">"Eindgrens"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-am/strings.xml b/compose/ui/ui/src/androidMain/res/values-am/strings.xml
index 63202e8..1a4e3dd 100644
--- a/compose/ui/ui/src/androidMain/res/values-am/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-am/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"የዳሰሳ ምናሌን ዝጋ"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ሉህን ዝጋ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ልክ ያልሆነ ግቤት"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ብቅ-ባይ መስኮት"</string>
     <string name="range_start" msgid="7097486360902471446">"የክልል መጀመሪያ"</string>
     <string name="range_end" msgid="5941395253238309765">"የክልል መጨረሻ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ar/strings.xml b/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
index 3cc8b9b..58c7262 100644
--- a/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ar/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"إغلاق قائمة التنقل"</string>
     <string name="close_sheet" msgid="7573152094250666567">"إغلاق الورقة"</string>
     <string name="default_error_message" msgid="8038256446254964252">"إدخال غير صالح"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"نافذة منبثقة"</string>
     <string name="range_start" msgid="7097486360902471446">"بداية النطاق"</string>
     <string name="range_end" msgid="5941395253238309765">"نهاية النطاق"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-as/strings.xml b/compose/ui/ui/src/androidMain/res/values-as/strings.xml
index c68278cc..b6a3786 100644
--- a/compose/ui/ui/src/androidMain/res/values-as/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-as/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"নেভিগেশ্বন মেনু বন্ধ কৰক"</string>
     <string name="close_sheet" msgid="7573152094250666567">"শ্বীট বন্ধ কৰক"</string>
     <string name="default_error_message" msgid="8038256446254964252">"অমান্য ইনপুট"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"পপ-আপ ৱিণ্ড’"</string>
     <string name="range_start" msgid="7097486360902471446">"পৰিসৰৰ আৰম্ভণি"</string>
     <string name="range_end" msgid="5941395253238309765">"পৰিসৰৰ সমাপ্তি"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-az/strings.xml b/compose/ui/ui/src/androidMain/res/values-az/strings.xml
index 929a2a6..ccac763 100644
--- a/compose/ui/ui/src/androidMain/res/values-az/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-az/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Naviqasiya menyusunu bağlayın"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Səhifəni bağlayın"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Yanlış daxiletmə"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Popap Pəncərəsi"</string>
     <string name="range_start" msgid="7097486360902471446">"Sıranın başlanğıcı"</string>
     <string name="range_end" msgid="5941395253238309765">"Sıranın sonu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml b/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
index 5f78ccf..794d4ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-b+sr+Latn/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zatvori meni za navigaciju"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zatvorite tabelu"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Unos je nevažeći"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Iskačući prozor"</string>
     <string name="range_start" msgid="7097486360902471446">"Početak opsega"</string>
     <string name="range_end" msgid="5941395253238309765">"Kraj opsega"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-be/strings.xml b/compose/ui/ui/src/androidMain/res/values-be/strings.xml
index f36b4bb..3be7010 100644
--- a/compose/ui/ui/src/androidMain/res/values-be/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-be/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Закрыць меню навігацыі"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Закрыць аркуш"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Памылка ўводу"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Усплывальнае акно"</string>
     <string name="range_start" msgid="7097486360902471446">"Пачатак пераліку"</string>
     <string name="range_end" msgid="5941395253238309765">"Канец пераліку"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bg/strings.xml b/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
index 68b6856..6814429 100644
--- a/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bg/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Затваряне на менюто за навигация"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Затваряне на таблицата"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Въведеното е невалидно"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Изскачащ прозорец"</string>
     <string name="range_start" msgid="7097486360902471446">"Начало на обхвата"</string>
     <string name="range_end" msgid="5941395253238309765">"Край на обхвата"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bn/strings.xml b/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
index ea9fe9b..2f84058 100644
--- a/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bn/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"নেভিগেশন মেনু বন্ধ করুন"</string>
     <string name="close_sheet" msgid="7573152094250666567">"শিট বন্ধ করুন"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ভুল ইনপুট"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"পপ-আপ উইন্ডো"</string>
     <string name="range_start" msgid="7097486360902471446">"রেঞ্জ শুরু"</string>
     <string name="range_end" msgid="5941395253238309765">"রেঞ্জ শেষ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-bs/strings.xml b/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
index acf3667..d8d8334 100644
--- a/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-bs/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zatvaranje navigacionog menija"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zatvaranje tabele"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Pogrešan unos"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Skočni prozor"</string>
     <string name="range_start" msgid="7097486360902471446">"Početak raspona"</string>
     <string name="range_end" msgid="5941395253238309765">"Kraj raspona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ca/strings.xml b/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
index f85f039..dd44ae7 100644
--- a/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ca/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Tanca el menú de navegació"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Tanca el full"</string>
     <string name="default_error_message" msgid="8038256446254964252">"L\'entrada no és vàlida"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Finestra emergent"</string>
     <string name="range_start" msgid="7097486360902471446">"Inici de l\'interval"</string>
     <string name="range_end" msgid="5941395253238309765">"Fi de l\'interval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-cs/strings.xml b/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
index a0c4662..e182a1a 100644
--- a/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-cs/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zavřít navigační panel"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zavřít sešit"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Neplatný údaj"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Vyskakovací okno"</string>
     <string name="range_start" msgid="7097486360902471446">"Začátek rozsahu"</string>
     <string name="range_end" msgid="5941395253238309765">"Konec rozsahu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-da/strings.xml b/compose/ui/ui/src/androidMain/res/values-da/strings.xml
index e440106..792e409 100644
--- a/compose/ui/ui/src/androidMain/res/values-da/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-da/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Luk navigationsmenuen"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Luk arket"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ugyldigt input"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop op-vindue"</string>
     <string name="range_start" msgid="7097486360902471446">"Startinterval"</string>
     <string name="range_end" msgid="5941395253238309765">"Slutinterval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-de/strings.xml b/compose/ui/ui/src/androidMain/res/values-de/strings.xml
index fce4e02..0022a48 100644
--- a/compose/ui/ui/src/androidMain/res/values-de/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-de/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Navigationsmenü schließen"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Tabelle schließen"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ungültige Eingabe"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up-Fenster"</string>
     <string name="range_start" msgid="7097486360902471446">"Bereichsstart"</string>
     <string name="range_end" msgid="5941395253238309765">"Bereichsende"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-el/strings.xml b/compose/ui/ui/src/androidMain/res/values-el/strings.xml
index 6a0e6ee..31496f4 100644
--- a/compose/ui/ui/src/androidMain/res/values-el/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-el/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Κλείσιμο του μενού πλοήγησης"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Κλείσιμο φύλλου"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Μη έγκυρη καταχώριση"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Αναδυόμενο παράθυρο"</string>
     <string name="range_start" msgid="7097486360902471446">"Αρχή εύρους"</string>
     <string name="range_end" msgid="5941395253238309765">"Τέλος εύρους"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rAU/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
     <string name="range_start" msgid="7097486360902471446">"Range start"</string>
     <string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
index 263002e13..af69c1f 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rCA/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+    <string name="state_empty" msgid="4139871816613051306">"Empty"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-Up Window"</string>
     <string name="range_start" msgid="7097486360902471446">"Range start"</string>
     <string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rGB/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
     <string name="range_start" msgid="7097486360902471446">"Range start"</string>
     <string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
index 4cd1620..6cec5e5 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rIN/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Close navigation menu"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Close sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Invalid input"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up window"</string>
     <string name="range_start" msgid="7097486360902471446">"Range start"</string>
     <string name="range_end" msgid="5941395253238309765">"Range end"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml b/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
index 4779808..e19716e 100644
--- a/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-en-rXC/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‏‎‎‎‎‎‎‎‎‏‏‎‎‏‏‏‏‎‎‎‎‏‏‏‏‎‏‎‎‏‏‏‎‎‏‎‏‎‎‏‏‎‎‎‏‎‏‎‏‎‎‎Close navigation menu‎‏‎‎‏‎"</string>
     <string name="close_sheet" msgid="7573152094250666567">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‏‎‎‏‎‎‏‎‏‏‏‏‏‎‏‎‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‏‎Close sheet‎‏‎‎‏‎"</string>
     <string name="default_error_message" msgid="8038256446254964252">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‏‏‎‏‏‎‎‏‏‏‏‏‏‎‏‎‎‏‏‎‎‎‏‏‏‏‎‎‏‎‎‎‎‏‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‏‎‎‎Invalid input‎‏‎‎‏‎"</string>
+    <string name="state_empty" msgid="4139871816613051306">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‎‏‏‏‎‎‏‏‏‏‎‎‎‏‏‏‎‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‏‏‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‎‏‎‏‎‎Empty‎‏‎‎‏‎"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‏‏‎‎‏‏‎‏‏‎‏‎‎‏‎‏‎‏‎‏‏‎‎‎‎‏‏‏‎‏‏‎‏‏‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‏‎‏‎‏‎‎Pop-Up Window‎‏‎‎‏‎"</string>
     <string name="range_start" msgid="7097486360902471446">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎‎‏‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‎‏‏‏‏‎‏‏‎‏‏‏‎‏‏‏‏‎‎‎‏‎‏‏‎‎‎‏‏‏‏‎‎‎‏‎‏‏‎‎Range start‎‏‎‎‏‎"</string>
     <string name="range_end" msgid="5941395253238309765">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‎‏‎‎‎‎‎‏‎‎‏‏‏‎‎‎‎‎‎‏‎‎‎‎‎‎‏‏‎‏‏‏‎‏‏‎‎‎‏‎‎‎‏‏‏‎‎‎‎‏‎‏‎Range end‎‏‎‎‏‎"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml b/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
index bf5e07e..db50050 100644
--- a/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-es-rUS/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Cerrar el menú de navegación"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Cerrar hoja"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrada no válida"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Ventana emergente"</string>
     <string name="range_start" msgid="7097486360902471446">"Inicio de intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Final de intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-es/strings.xml b/compose/ui/ui/src/androidMain/res/values-es/strings.xml
index 68c06e1..3b25bdd 100644
--- a/compose/ui/ui/src/androidMain/res/values-es/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-es/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Cerrar menú de navegación"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Cerrar hoja"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrada no válida"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Ventana emergente"</string>
     <string name="range_start" msgid="7097486360902471446">"Inicio del intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fin del intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-et/strings.xml b/compose/ui/ui/src/androidMain/res/values-et/strings.xml
index 803206d..dd9cae9 100644
--- a/compose/ui/ui/src/androidMain/res/values-et/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-et/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Sule navigeerimismenüü"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Sule leht"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Sobimatu sisend"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Hüpikaken"</string>
     <string name="range_start" msgid="7097486360902471446">"Vahemiku algus"</string>
     <string name="range_end" msgid="5941395253238309765">"Vahemiku lõpp"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-eu/strings.xml b/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
index edcc8f2..757bf68 100644
--- a/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-eu/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Itxi nabigazio-menua"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Itxi orria"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Sarrerak ez du balio"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Leiho gainerakorra"</string>
     <string name="range_start" msgid="7097486360902471446">"Barrutiaren hasiera"</string>
     <string name="range_end" msgid="5941395253238309765">"Barrutiaren amaiera"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fa/strings.xml b/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
index 39b1a92..c9c25ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fa/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"بستن منوی پیمایش"</string>
     <string name="close_sheet" msgid="7573152094250666567">"بستن برگ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ورودی نامعتبر"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"پنجره بالاپر"</string>
     <string name="range_start" msgid="7097486360902471446">"شروع محدوده"</string>
     <string name="range_end" msgid="5941395253238309765">"پایان محدوده"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fi/strings.xml b/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
index 5479432..db69277 100644
--- a/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fi/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Sulje navigointivalikko"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Sulje taulukko"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Virheellinen syöte"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Ponnahdusikkuna"</string>
     <string name="range_start" msgid="7097486360902471446">"Alueen alku"</string>
     <string name="range_end" msgid="5941395253238309765">"Alueen loppu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml b/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
index abcbedd..6c9a7ed 100644
--- a/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fr-rCA/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Fermer le menu de navigation"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Fermer la feuille"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrée incorrecte"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Fenêtre contextuelle"</string>
     <string name="range_start" msgid="7097486360902471446">"Début de plage"</string>
     <string name="range_end" msgid="5941395253238309765">"Fin de plage"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-fr/strings.xml b/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
index 7f6e679..29894ec 100644
--- a/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-fr/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Fermer le menu de navigation"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Fermer la feuille"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Données incorrectes"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Fenêtre pop-up"</string>
     <string name="range_start" msgid="7097486360902471446">"Début de plage"</string>
     <string name="range_end" msgid="5941395253238309765">"Fin de plage"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-gl/strings.xml b/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
index 045f898..e7a7c7b 100644
--- a/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-gl/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Pechar menú de navegación"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Pechar folla"</string>
     <string name="default_error_message" msgid="8038256446254964252">"O texto escrito non é válido"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Ventá emerxente"</string>
     <string name="range_start" msgid="7097486360902471446">"Inicio do intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fin do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-gu/strings.xml b/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
index a4cd743..23b9ccb 100644
--- a/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-gu/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"નૅવિગેશન મેનૂ બંધ કરો"</string>
     <string name="close_sheet" msgid="7573152094250666567">"શીટ બંધ કરો"</string>
     <string name="default_error_message" msgid="8038256446254964252">"અમાન્ય ઇનપુટ"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"પૉપ-અપ વિન્ડો"</string>
     <string name="range_start" msgid="7097486360902471446">"રેંજની શરૂઆત"</string>
     <string name="range_end" msgid="5941395253238309765">"રેંજની સમાપ્તિ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hi/strings.xml b/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
index fe6906a..103ecfa 100644
--- a/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hi/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"नेविगेशन मेन्यू बंद करें"</string>
     <string name="close_sheet" msgid="7573152094250666567">"शीट बंद करें"</string>
     <string name="default_error_message" msgid="8038256446254964252">"अमान्य इनपुट"</string>
+    <string name="state_empty" msgid="4139871816613051306">"कोई भी तार नहीं लगा है"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"पॉप-अप विंडो"</string>
     <string name="range_start" msgid="7097486360902471446">"रेंज की शुरुआत"</string>
     <string name="range_end" msgid="5941395253238309765">"रेंज की सीमा"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hr/strings.xml b/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
index 1723b39..c168ea0 100644
--- a/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hr/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zatvaranje izbornika za navigaciju"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zatvaranje lista"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Nevažeći unos"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Skočni prozor"</string>
     <string name="range_start" msgid="7097486360902471446">"Početak raspona"</string>
     <string name="range_end" msgid="5941395253238309765">"Kraj raspona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hu/strings.xml b/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
index fa50b3e..631ea36 100644
--- a/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hu/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Navigációs menü bezárása"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Munkalap bezárása"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Érvénytelen adat"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Előugró ablak"</string>
     <string name="range_start" msgid="7097486360902471446">"Tartomány kezdete"</string>
     <string name="range_end" msgid="5941395253238309765">"Tartomány vége"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-hy/strings.xml b/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
index 3acfd76..598a469 100644
--- a/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-hy/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Փակել նավիգացիայի ընտրացանկը"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Փակել թերթը"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Սխալ ներածում"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Ելնող պատուհան"</string>
     <string name="range_start" msgid="7097486360902471446">"Ընդգրկույթի սկիզբ"</string>
     <string name="range_end" msgid="5941395253238309765">"Ընդգրկույթի վերջ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-in/strings.xml b/compose/ui/ui/src/androidMain/res/values-in/strings.xml
index 577e85c..650487a 100644
--- a/compose/ui/ui/src/androidMain/res/values-in/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-in/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Tutup menu navigasi"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Tutup sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Input tidak valid"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Jendela Pop-Up"</string>
     <string name="range_start" msgid="7097486360902471446">"Rentang awal"</string>
     <string name="range_end" msgid="5941395253238309765">"Rentang akhir"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-is/strings.xml b/compose/ui/ui/src/androidMain/res/values-is/strings.xml
index af87f3b..71b16b1 100644
--- a/compose/ui/ui/src/androidMain/res/values-is/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-is/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Loka yfirlitsvalmynd"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Loka blaði"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ógildur innsláttur"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Sprettigluggi"</string>
     <string name="range_start" msgid="7097486360902471446">"Upphaf sviðs"</string>
     <string name="range_end" msgid="5941395253238309765">"Lok sviðs"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-it/strings.xml b/compose/ui/ui/src/androidMain/res/values-it/strings.xml
index e5727e4..509a24d 100644
--- a/compose/ui/ui/src/androidMain/res/values-it/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-it/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Chiudi il menu di navigazione"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Chiudi il foglio"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Valore non valido"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Finestra popup"</string>
     <string name="range_start" msgid="7097486360902471446">"Inizio intervallo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fine intervallo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-iw/strings.xml b/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
index f32fd3a..f567055 100644
--- a/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-iw/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"סגירת תפריט הניווט"</string>
     <string name="close_sheet" msgid="7573152094250666567">"סגירת הגיליון"</string>
     <string name="default_error_message" msgid="8038256446254964252">"הקלט לא תקין"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"חלון קופץ"</string>
     <string name="range_start" msgid="7097486360902471446">"תחילת הטווח"</string>
     <string name="range_end" msgid="5941395253238309765">"סוף הטווח"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ja/strings.xml b/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
index 0563fb4..cf83e57 100644
--- a/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ja/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ナビゲーションメニューを閉じる"</string>
     <string name="close_sheet" msgid="7573152094250666567">"シートを閉じる"</string>
     <string name="default_error_message" msgid="8038256446254964252">"入力値が無効です"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ポップアップウィンドウ"</string>
     <string name="range_start" msgid="7097486360902471446">"範囲の先頭"</string>
     <string name="range_end" msgid="5941395253238309765">"範囲の末尾"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ka/strings.xml b/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
index d108b025..eb093ba 100644
--- a/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ka/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ნავიგაციის მენიუს დახურვა"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ფურცლის დახურვა"</string>
     <string name="default_error_message" msgid="8038256446254964252">"შენატანი არასწორია"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ამომხტარი ფანჯარა"</string>
     <string name="range_start" msgid="7097486360902471446">"დიაპაზონის დასაწყისი"</string>
     <string name="range_end" msgid="5941395253238309765">"დიაპაზონის დასასრული"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-kk/strings.xml b/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
index 69c768d..d21df0b 100644
--- a/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-kk/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Навигация мәзірін жабу"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Парақты жабу"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Енгізілген мән жарамсыз."</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Қалқымалы терезе"</string>
     <string name="range_start" msgid="7097486360902471446">"Аралықтың басы"</string>
     <string name="range_end" msgid="5941395253238309765">"Аралықтың соңы"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-km/strings.xml b/compose/ui/ui/src/androidMain/res/values-km/strings.xml
index 5257840..e10e501 100644
--- a/compose/ui/ui/src/androidMain/res/values-km/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-km/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"បិទម៉ឺនុយរុករក"</string>
     <string name="close_sheet" msgid="7573152094250666567">"បិទសន្លឹក"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ការបញ្ចូល​មិនត្រឹមត្រូវ"</string>
+    <string name="state_empty" msgid="4139871816613051306">"ទទេ"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"វិនដូ​លោតឡើង"</string>
     <string name="range_start" msgid="7097486360902471446">"ចំណុចចាប់ផ្ដើម"</string>
     <string name="range_end" msgid="5941395253238309765">"ចំណុចបញ្ចប់"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-kn/strings.xml b/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
index 271b86a..5eb9b0b 100644
--- a/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-kn/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ನ್ಯಾವಿಗೇಷನ್‌ ಮೆನು ಮುಚ್ಚಿರಿ"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ಶೀಟ್ ಮುಚ್ಚಿರಿ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ಅಮಾನ್ಯ ಇನ್‌ಪುಟ್"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ಪಾಪ್-ಅಪ್ ವಿಂಡೋ"</string>
     <string name="range_start" msgid="7097486360902471446">"ಶ್ರೇಣಿಯ ಪ್ರಾರಂಭ"</string>
     <string name="range_end" msgid="5941395253238309765">"ಶ್ರೇಣಿಯ ಅಂತ್ಯ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ko/strings.xml b/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
index b486704..d452c4d82 100644
--- a/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ko/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"탐색 메뉴 닫기"</string>
     <string name="close_sheet" msgid="7573152094250666567">"시트 닫기"</string>
     <string name="default_error_message" msgid="8038256446254964252">"입력이 잘못됨"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"팝업 창"</string>
     <string name="range_start" msgid="7097486360902471446">"범위 시작"</string>
     <string name="range_end" msgid="5941395253238309765">"범위 끝"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ky/strings.xml b/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
index 686a577..4c6591e 100644
--- a/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ky/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Чабыттоо менюсун жабуу"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Баракты жабуу"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Киргизилген маалымат жараксыз"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Калкыма терезе"</string>
     <string name="range_start" msgid="7097486360902471446">"Диапазондун башы"</string>
     <string name="range_end" msgid="5941395253238309765">"Диапазондун аягы"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lo/strings.xml b/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
index 7c36d6e..c201f11 100644
--- a/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lo/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"ປິດ​ເມ​ນູການ​ນຳ​ທາງ"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ປິດຊີດ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ຂໍ້ມູນທີ່ປ້ອນເຂົ້າບໍ່ຖືກຕ້ອງ"</string>
+    <string name="state_empty" msgid="4139871816613051306">"ຫວ່າງເປົ່າ"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"ໜ້າຈໍປັອບອັບ"</string>
     <string name="range_start" msgid="7097486360902471446">"ເລີ່ມຕົ້ນໄລຍະ"</string>
     <string name="range_end" msgid="5941395253238309765">"ສິ້ນສຸດໄລຍະ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lt/strings.xml b/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
index 7ad81ab..9da20ee 100644
--- a/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lt/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Uždaryti naršymo meniu"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Uždaryti lapą"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Netinkama įvestis"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Iššokantysis langas"</string>
     <string name="range_start" msgid="7097486360902471446">"Diapazono pradžia"</string>
     <string name="range_end" msgid="5941395253238309765">"Diapazono pabaiga"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-lv/strings.xml b/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
index 4baf5f9..4a5726e 100644
--- a/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-lv/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Aizvērt navigācijas izvēlni"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Aizvērt izklājlapu"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Nederīga ievade"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Uznirstošais logs"</string>
     <string name="range_start" msgid="7097486360902471446">"Diapazona sākums"</string>
     <string name="range_end" msgid="5941395253238309765">"Diapazona beigas"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mk/strings.xml b/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
index b3f14062..6580504 100644
--- a/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mk/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Затворете го менито за навигација"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Затворете го листот"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Неважечки запис"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Скокачки прозорец"</string>
     <string name="range_start" msgid="7097486360902471446">"Почеток на опсегот"</string>
     <string name="range_end" msgid="5941395253238309765">"Крај на опсегот"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ml/strings.xml b/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
index 7e1a80d..0514441 100644
--- a/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ml/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"നാവിഗേഷൻ മെനു അടയ്‌ക്കുക"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ഷീറ്റ് അടയ്ക്കുക"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ഇൻപുട്ട് അസാധുവാണ്"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"പോപ്പ്-അപ്പ് വിൻഡോ"</string>
     <string name="range_start" msgid="7097486360902471446">"ശ്രേണിയുടെ ആരംഭം"</string>
     <string name="range_end" msgid="5941395253238309765">"ശ്രേണിയുടെ അവസാനം"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mn/strings.xml b/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
index 9fd17b1..df96b02 100644
--- a/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mn/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Навигацын цэсийг хаах"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Хүснэгтийг хаах"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Буруу оролт"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Попап цонх"</string>
     <string name="range_start" msgid="7097486360902471446">"Мужийн эхлэл"</string>
     <string name="range_end" msgid="5941395253238309765">"Мужийн төгсгөл"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-mr/strings.xml b/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
index ad44056..54aaab6 100644
--- a/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-mr/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"नेव्हिगेशन मेनू बंद करा"</string>
     <string name="close_sheet" msgid="7573152094250666567">"शीट बंद करा"</string>
     <string name="default_error_message" msgid="8038256446254964252">"इनपुट चुकीचे आहे"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"पॉप-अप विंडो"</string>
     <string name="range_start" msgid="7097486360902471446">"रेंजची सुरुवात"</string>
     <string name="range_end" msgid="5941395253238309765">"रेंजचा शेवट"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ms/strings.xml b/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
index d33a81a..72af8eb 100644
--- a/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ms/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Tutup menu navigasi"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Tutup helaian"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Input tidak sah"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Tetingkap Timbul"</string>
     <string name="range_start" msgid="7097486360902471446">"Permulaan julat"</string>
     <string name="range_end" msgid="5941395253238309765">"Penghujung julat"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-my/strings.xml b/compose/ui/ui/src/androidMain/res/values-my/strings.xml
index 1ef3aab..a44f8ed 100644
--- a/compose/ui/ui/src/androidMain/res/values-my/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-my/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"လမ်းညွှန် မီနူး ပိတ်ရန်"</string>
     <string name="close_sheet" msgid="7573152094250666567">"စာမျက်နှာ ပိတ်ရန်"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ထည့်သွင်းမှု မမှန်ကန်ပါ"</string>
+    <string name="state_empty" msgid="4139871816613051306">"မရှိပါ"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"ပေါ့ပ်အပ် ဝင်းဒိုး"</string>
     <string name="range_start" msgid="7097486360902471446">"အပိုင်းအခြား အစ"</string>
     <string name="range_end" msgid="5941395253238309765">"အပိုင်းအခြား အဆုံး"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-nb/strings.xml b/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
index 1395a49..2a4c5d6 100644
--- a/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-nb/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Lukk navigasjonsmenyen"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Lukk arket"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ugyldige inndata"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Forgrunnsvindu"</string>
     <string name="range_start" msgid="7097486360902471446">"Områdestart"</string>
     <string name="range_end" msgid="5941395253238309765">"Områdeslutt"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ne/strings.xml b/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
index 53b2ce10..f8a9002 100644
--- a/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ne/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"नेभिगेसन मेनु बन्द गर्नुहोस्"</string>
     <string name="close_sheet" msgid="7573152094250666567">"पाना बन्द गर्नुहोस्"</string>
     <string name="default_error_message" msgid="8038256446254964252">"अवैद्य इन्पुट"</string>
+    <string name="state_empty" msgid="4139871816613051306">"खाली"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"पपअप विन्डो"</string>
     <string name="range_start" msgid="7097486360902471446">"दायराको सुरुवात बिन्दु"</string>
     <string name="range_end" msgid="5941395253238309765">"दायराको अन्तिम बिन्दु"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-nl/strings.xml b/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
index c2a6f5b..129ccb0 100644
--- a/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-nl/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Navigatiemenu sluiten"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Blad sluiten"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ongeldige invoer"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-upvenster"</string>
     <string name="range_start" msgid="7097486360902471446">"Start bereik"</string>
     <string name="range_end" msgid="5941395253238309765">"Einde bereik"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-or/strings.xml b/compose/ui/ui/src/androidMain/res/values-or/strings.xml
index 32a9c02..6773c55 100644
--- a/compose/ui/ui/src/androidMain/res/values-or/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-or/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ନାଭିଗେସନ୍ ମେନୁ ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ସିଟ୍ ବନ୍ଦ କରନ୍ତୁ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ଅବୈଧ ଇନପୁଟ୍"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ପପ୍-ଅପ୍ ୱିଣ୍ଡୋ"</string>
     <string name="range_start" msgid="7097486360902471446">"ରେଞ୍ଜ ଆରମ୍ଭ"</string>
     <string name="range_end" msgid="5941395253238309765">"ରେଞ୍ଜ ଶେଷ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pa/strings.xml b/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
index 1a25393..a88164a 100644
--- a/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pa/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ਨੈਵੀਗੇਸ਼ਨ ਮੀਨੂ ਬੰਦ ਕਰੋ"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ਸ਼ੀਟ ਬੰਦ ਕਰੋ"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ਅਵੈਧ ਇਨਪੁੱਟ"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"ਪੌਪ-ਅੱਪ ਵਿੰਡੋ"</string>
     <string name="range_start" msgid="7097486360902471446">"ਰੇਂਜ ਸ਼ੁਰੂ"</string>
     <string name="range_end" msgid="5941395253238309765">"ਰੇਂਜ ਸਮਾਪਤ"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pl/strings.xml b/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
index 6029183..aaff133 100644
--- a/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pl/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zamknij menu nawigacyjne"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zamknij arkusz"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Nieprawidłowe dane wejściowe"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Wyskakujące okienko"</string>
     <string name="range_start" msgid="7097486360902471446">"Początek zakresu"</string>
     <string name="range_end" msgid="5941395253238309765">"Koniec zakresu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
index 0c70403..e5d9ff3 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt-rBR/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Fechar planilha"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
     <string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
index c77ce8e..be24761 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt-rPT/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Fechar folha"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+    <string name="state_empty" msgid="4139871816613051306">"Vazio"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
     <string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-pt/strings.xml b/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
index 0c70403..e5d9ff3 100644
--- a/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-pt/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Fechar menu de navegação"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Fechar planilha"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Entrada inválida"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Janela pop-up"</string>
     <string name="range_start" msgid="7097486360902471446">"Início do intervalo"</string>
     <string name="range_end" msgid="5941395253238309765">"Fim do intervalo"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ro/strings.xml b/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
index c2da926..7e8a0dc 100644
--- a/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ro/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Închide meniul de navigare"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Închide foaia"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Intrare nevalidă"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Fereastră pop-up"</string>
     <string name="range_start" msgid="7097486360902471446">"Început de interval"</string>
     <string name="range_end" msgid="5941395253238309765">"Sfârșit de interval"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ru/strings.xml b/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
index 3b9a833..218c2c1 100644
--- a/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ru/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Закрыть меню навигации"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Закрыть лист"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Неправильный ввод"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Всплывающее окно"</string>
     <string name="range_start" msgid="7097486360902471446">"Начало диапазона"</string>
     <string name="range_end" msgid="5941395253238309765">"Конец диапазона"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-si/strings.xml b/compose/ui/ui/src/androidMain/res/values-si/strings.xml
index ebf3e39..787156c 100644
--- a/compose/ui/ui/src/androidMain/res/values-si/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-si/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"සංචාලන මෙනුව වසන්න"</string>
     <string name="close_sheet" msgid="7573152094250666567">"පත්‍රය වසන්න"</string>
     <string name="default_error_message" msgid="8038256446254964252">"වලංගු නොවන ආදානයකි"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"උත්පතන කවුළුව"</string>
     <string name="range_start" msgid="7097486360902471446">"පරාස ආරම්භය"</string>
     <string name="range_end" msgid="5941395253238309765">"පරාස අන්තය"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sk/strings.xml b/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
index d576b04..0b28e8c 100644
--- a/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sk/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zavrieť navigačnú ponuku"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zavrieť hárok"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Neplatný vstup"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Vyskakovacie okno"</string>
     <string name="range_start" msgid="7097486360902471446">"Začiatok rozsahu"</string>
     <string name="range_end" msgid="5941395253238309765">"Koniec rozsahu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sl/strings.xml b/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
index 02e0df2..eb9301f5 100644
--- a/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sl/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Zapri meni za krmarjenje"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Zapri list"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Neveljaven vnos"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pojavno okno"</string>
     <string name="range_start" msgid="7097486360902471446">"Začetek razpona"</string>
     <string name="range_end" msgid="5941395253238309765">"Konec razpona"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sq/strings.xml b/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
index 7d26d4c..a188d25 100644
--- a/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sq/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Mbyll menynë e navigimit"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Mbyll fletën"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Hyrje e pavlefshme"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Dritare kërcyese"</string>
     <string name="range_start" msgid="7097486360902471446">"Fillimi i diapazonit"</string>
     <string name="range_end" msgid="5941395253238309765">"Fundi i diapazonit"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sr/strings.xml b/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
index 3502abf..9708e29 100644
--- a/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sr/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Затвори мени за навигацију"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Затворите табелу"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Унос је неважећи"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Искачући прозор"</string>
     <string name="range_start" msgid="7097486360902471446">"Почетак опсега"</string>
     <string name="range_end" msgid="5941395253238309765">"Крај опсега"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sv/strings.xml b/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
index f005131b..0bf933b 100644
--- a/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sv/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Stäng navigeringsmenyn"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Stäng kalkylarket"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ogiltiga indata"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Popup-fönster"</string>
     <string name="range_start" msgid="7097486360902471446">"Intervallets början"</string>
     <string name="range_end" msgid="5941395253238309765">"Intervallets slut"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-sw/strings.xml b/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
index 42d3e1f..98421b2 100644
--- a/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-sw/strings.xml
@@ -31,6 +31,7 @@
     <string name="close_drawer" msgid="406453423630273620">"Funga menyu ya kusogeza"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Funga laha"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Ulichoweka si sahihi"</string>
+    <string name="state_empty" msgid="4139871816613051306">"Tupu"</string>
     <string name="default_popup_window_title" msgid="6312721426453364202">"Dirisha Ibukizi"</string>
     <string name="range_start" msgid="7097486360902471446">"Mwanzo wa masafa"</string>
     <string name="range_end" msgid="5941395253238309765">"Mwisho wa masafa"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ta/strings.xml b/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
index 8e584e7..cbaf6f8 100644
--- a/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ta/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"வழிசெலுத்தல் மெனுவை மூடும்"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ஷீட்டை மூடும்"</string>
     <string name="default_error_message" msgid="8038256446254964252">"தவறான உள்ளீடு"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"பாப்-அப் சாளரம்"</string>
     <string name="range_start" msgid="7097486360902471446">"வரம்பு தொடக்கம்"</string>
     <string name="range_end" msgid="5941395253238309765">"வரம்பு முடிவு"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-te/strings.xml b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
index cecac59e..fec67d4e 100644
--- a/compose/ui/ui/src/androidMain/res/values-te/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-te/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"నావిగేషన్ మెనూను మూసివేయండి"</string>
     <string name="close_sheet" msgid="7573152094250666567">"షీట్‌ను మూసివేయండి"</string>
     <string name="default_error_message" msgid="8038256446254964252">"ఇన్‌పుట్ చెల్లదు"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"పాప్-అప్ విండో"</string>
     <string name="range_start" msgid="7097486360902471446">"పరిధి ప్రారంభమయింది"</string>
     <string name="range_end" msgid="5941395253238309765">"పరిధి ముగిసింది"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-th/strings.xml b/compose/ui/ui/src/androidMain/res/values-th/strings.xml
index 3ceed7d..1391214 100644
--- a/compose/ui/ui/src/androidMain/res/values-th/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-th/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"ปิดเมนูการนำทาง"</string>
     <string name="close_sheet" msgid="7573152094250666567">"ปิดชีต"</string>
     <string name="default_error_message" msgid="8038256446254964252">"อินพุตไม่ถูกต้อง"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"หน้าต่างป๊อปอัป"</string>
     <string name="range_start" msgid="7097486360902471446">"จุดเริ่มต้นของช่วง"</string>
     <string name="range_end" msgid="5941395253238309765">"จุดสิ้นสุดของช่วง"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-tl/strings.xml b/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
index 9ab42b2d..af0b7d2 100644
--- a/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-tl/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Isara ang menu ng navigation"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Isara ang sheet"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Invalid na input"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Window ng Pop-Up"</string>
     <string name="range_start" msgid="7097486360902471446">"Simula ng range"</string>
     <string name="range_end" msgid="5941395253238309765">"Katapusan ng range"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-tr/strings.xml b/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
index 909d057..aa8f590 100644
--- a/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-tr/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Gezinme menüsünü kapat"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Sayfayı kapat"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Geçersiz giriş"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Pop-up Pencere"</string>
     <string name="range_start" msgid="7097486360902471446">"Aralık başlangıcı"</string>
     <string name="range_end" msgid="5941395253238309765">"Aralık sonu"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-uk/strings.xml b/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
index 2cec971..b93fba07 100644
--- a/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-uk/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Закрити меню навігації"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Закрити аркуш"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Введено недійсні дані"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Спливаюче вікно"</string>
     <string name="range_start" msgid="7097486360902471446">"Початок діапазону"</string>
     <string name="range_end" msgid="5941395253238309765">"Кінець діапазону"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-ur/strings.xml b/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
index e9c0509..7ee15a1 100644
--- a/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-ur/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"نیویگیشن مینیو بند کریں"</string>
     <string name="close_sheet" msgid="7573152094250666567">"شیٹ بند کریں"</string>
     <string name="default_error_message" msgid="8038256446254964252">"غلط ان پٹ"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"پاپ اپ ونڈو"</string>
     <string name="range_start" msgid="7097486360902471446">"رینج کی شروعات"</string>
     <string name="range_end" msgid="5941395253238309765">"رینج کا اختتام"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-uz/strings.xml b/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
index 7115c76..8ccc776 100644
--- a/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-uz/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Navigatsiya menyusini yopish"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Varaqni yopish"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Kiritilgan axborot xato"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Qalqib chiquvchi oyna"</string>
     <string name="range_start" msgid="7097486360902471446">"Oraliq boshi"</string>
     <string name="range_end" msgid="5941395253238309765">"Oraliq oxiri"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-vi/strings.xml b/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
index a7728063..1313dc4 100644
--- a/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-vi/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Đóng trình đơn điều hướng"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Đóng trang tính"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Giá trị nhập không hợp lệ"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Cửa sổ bật lên"</string>
     <string name="range_start" msgid="7097486360902471446">"Điểm bắt đầu phạm vi"</string>
     <string name="range_end" msgid="5941395253238309765">"Điểm kết thúc phạm vi"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
index 2145293..b7031c6 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rCN/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"关闭导航菜单"</string>
     <string name="close_sheet" msgid="7573152094250666567">"关闭工作表"</string>
     <string name="default_error_message" msgid="8038256446254964252">"输入无效"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"弹出式窗口"</string>
     <string name="range_start" msgid="7097486360902471446">"范围起点"</string>
     <string name="range_end" msgid="5941395253238309765">"范围终点"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
index a5804a8..155d6d3 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rHK/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"閂導覽選單"</string>
     <string name="close_sheet" msgid="7573152094250666567">"閂表單"</string>
     <string name="default_error_message" msgid="8038256446254964252">"輸入嘅資料無效"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"彈出式視窗"</string>
     <string name="range_start" msgid="7097486360902471446">"範圍開始"</string>
     <string name="range_end" msgid="5941395253238309765">"範圍結束"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml b/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
index 36aa760..198d101 100644
--- a/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zh-rTW/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"關閉導覽選單"</string>
     <string name="close_sheet" msgid="7573152094250666567">"關閉功能表"</string>
     <string name="default_error_message" msgid="8038256446254964252">"輸入內容無效"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"彈出式視窗"</string>
     <string name="range_start" msgid="7097486360902471446">"範圍起點"</string>
     <string name="range_end" msgid="5941395253238309765">"範圍終點"</string>
diff --git a/compose/ui/ui/src/androidMain/res/values-zu/strings.xml b/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
index 3237ddb..54fca1b 100644
--- a/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
+++ b/compose/ui/ui/src/androidMain/res/values-zu/strings.xml
@@ -31,6 +31,8 @@
     <string name="close_drawer" msgid="406453423630273620">"Vala imenyu yokuzulazula"</string>
     <string name="close_sheet" msgid="7573152094250666567">"Vala ishidi"</string>
     <string name="default_error_message" msgid="8038256446254964252">"Okufakwayo okungalungile"</string>
+    <!-- no translation found for state_empty (4139871816613051306) -->
+    <skip />
     <string name="default_popup_window_title" msgid="6312721426453364202">"Iwindi Lesikhashana"</string>
     <string name="range_start" msgid="7097486360902471446">"Ukuqala kobubanzi"</string>
     <string name="range_end" msgid="5941395253238309765">"Umkhawulo wobubanzi"</string>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
new file mode 100644
index 0000000..36b2ec8
--- /dev/null
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/HitTestSharePointerInputWithSiblingTest.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.node
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.unit.IntSize
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class HitTestSharePointerInputWithSiblingTest {
+    @Test
+    fun hitTest_sharePointerWithSibling() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSibling_utilFirstNodeNotSharing() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSibling_whenParentDisallowShare() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier()) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        // The parent node doesn't share pointer events, the two children can still share events.
+        assertThat(hit).isEqualTo(
+            listOf(pointerInputModifier1, pointerInputModifier3, pointerInputModifier2)
+        )
+    }
+
+    @Test
+    fun hitTest_sharePointerWithSiblingTrue_shareWithCousin() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier1))
+    }
+
+    @Test
+    fun hitTest_parentDisallowShare_notShareWithCousin() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1, pointerInputModifier2.toModifier()) {
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        // PointerInputModifier1 can't receive events because pointerInputModifier2 doesn't share.
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier2, pointerInputModifier3))
+    }
+
+    @Test
+    fun hitTest_sharePointerWithCousin_untilFirstNodeNotSharing() {
+        val pointerInputModifier1 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+        val pointerInputModifier2 = FakePointerInputModifierNode(sharePointerWithSiblings = false)
+        val pointerInputModifier3 = FakePointerInputModifierNode(sharePointerWithSiblings = true)
+
+        val outerNode = LayoutNode(0, 0, 1, 1) {
+            childNode(0, 0, 1, 1, pointerInputModifier1.toModifier())
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier2.toModifier())
+            }
+            childNode(0, 0, 1, 1) {
+                childNode(0, 0, 1, 1, pointerInputModifier3.toModifier())
+            }
+        }
+
+        val hit = mutableListOf<Modifier.Node>()
+
+        outerNode.hitTest(Offset(0f, 0f), hit)
+
+        assertThat(hit).isEqualTo(listOf(pointerInputModifier3, pointerInputModifier2))
+    }
+}
+
+private fun LayoutNode(
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+    block: LayoutNode.() -> Unit
+): LayoutNode {
+    val root = LayoutNode(left, top, right, bottom).apply {
+        attach(MockOwner())
+    }
+
+    block.invoke(root)
+    return root
+}
+
+private fun LayoutNode.childNode(
+    left: Int,
+    top: Int,
+    right: Int,
+    bottom: Int,
+    modifier: Modifier = Modifier,
+    block: LayoutNode.() -> Unit = {}
+): LayoutNode {
+    val layoutNode = LayoutNode(left, top, right, bottom, modifier)
+    add(layoutNode)
+    layoutNode.onNodePlaced()
+    block.invoke(layoutNode)
+    return layoutNode
+}
+
+private fun FakePointerInputModifierNode.toModifier(): Modifier {
+    return object : ModifierNodeElement<FakePointerInputModifierNode>() {
+        override fun create(): FakePointerInputModifierNode = this@toModifier
+
+        override fun update(node: FakePointerInputModifierNode) { }
+
+        override fun hashCode(): Int {
+            return if (this@toModifier.sharePointerWithSiblings) 1 else 0
+        }
+
+        override fun equals(other: Any?): Boolean {
+           return this@toModifier.sharePointerWithSiblings
+        }
+    }
+}
+
+private class FakePointerInputModifierNode(
+    var sharePointerWithSiblings: Boolean = false
+) : Modifier.Node(), PointerInputModifierNode {
+    override fun onPointerEvent(
+        pointerEvent: PointerEvent,
+        pass: PointerEventPass,
+        bounds: IntSize
+    ) {}
+
+    override fun onCancelPointerInput() {}
+
+    override fun sharePointerInputWithSiblings(): Boolean = this.sharePointerWithSiblings
+}
+
+private fun LayoutNode.hitTest(
+    pointerPosition: Offset,
+    hitPointerInputFilters: MutableList<Modifier.Node>,
+    isTouchEvent: Boolean = false
+) {
+    val hitTestResult = HitTestResult()
+    hitTest(pointerPosition, hitTestResult, isTouchEvent)
+    hitPointerInputFilters.addAll(hitTestResult)
+}
+
+private fun LayoutNode.onNodePlaced() = measurePassDelegate.onNodePlaced()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index e9a8947..f387981 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -49,6 +49,7 @@
 /**
  * Paint the content using [painter].
  *
+ * @param painter [Painter] to be drawn by this [Modifier]
  * @param sizeToIntrinsics `true` to size the element relative to [Painter.intrinsicSize]
  * @param alignment specifies alignment of the [painter] relative to content
  * @param contentScale strategy for scaling [painter] if its size does not match the content size
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
index f3703e0..e48553d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/Shadow.kt
@@ -82,6 +82,9 @@
  * Use a [androidx.compose.ui.zIndex] modifier if you want to draw the elements with larger
  * [elevation] after all the elements with a smaller one.
  *
+ * Note that this parameter is only supported on Android 9 (Pie) and above. On older versions,
+ * this property always returns [Color.Black] and setting new values is ignored.
+ *
  * Usage of this API renders this composable into a separate graphics layer
  * @see graphicsLayer
  *
@@ -92,6 +95,8 @@
  * @param elevation The elevation for the shadow in pixels
  * @param shape Defines a shape of the physical object
  * @param clip When active, the content drawing clips to the shape.
+ * @param ambientColor Color of the ambient shadow drawn when [elevation] > 0f
+ * @param spotColor Color of the spot shadow that is drawn when [elevation] > 0f
  */
 @Stable
 fun Modifier.shadow(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
index c3995f3..b678844 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.PathFillType
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeJoin
@@ -285,7 +286,7 @@
          * @param trimPathStart specifies the fraction of the path to trim from the start in the
          * range from 0 to 1. Values outside the range will wrap around the length of the path.
          * Default is 0.
-         * @param trimPathStart specifies the fraction of the path to trim from the end in the
+         * @param trimPathEnd specifies the fraction of the path to trim from the end in the
          * range from 0 to 1. Values outside the range will wrap around the length of the path.
          * Default is 1.
          * @param trimPathOffset specifies the fraction to shift the path trim region in the range
@@ -701,6 +702,8 @@
  * @param strokeLineCap specifies the linecap for a stroked path
  * @param strokeLineJoin specifies the linejoin for a stroked path
  * @param strokeLineMiter specifies the miter limit for a stroked path
+ * @param pathFillType specifies the winding rule that decides how the interior of a [Path] is
+ * calculated.
  * @param pathBuilder [PathBuilder] lambda for adding [PathNode]s to this path.
  */
 inline fun ImageVector.Builder.path(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
index c381de4..3071bfc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/VectorPainter.kt
@@ -112,6 +112,8 @@
  * @param [name] optional identifier used to identify the root of this vector graphic
  * @param [tintColor] optional color used to tint the root group of this vector graphic
  * @param [tintBlendMode] BlendMode used in combination with [tintColor]
+ * @param [autoMirror] Determines if the contents of the Vector should be mirrored for right to left
+ * layouts.
  * @param [content] Composable used to define the structure and contents of the vector graphic
  */
 @Composable
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
index f442482..d84b8d3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/HitPathTracker.kt
@@ -17,6 +17,9 @@
 package androidx.compose.ui.input.pointer
 
 import androidx.collection.LongSparseArray
+import androidx.collection.MutableLongObjectMap
+import androidx.collection.MutableObjectList
+import androidx.collection.mutableObjectListOf
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,8 +45,7 @@
     /*@VisibleForTesting*/
     internal val root: NodeParent = NodeParent()
 
-    // Only used when removing duplicate Nodes from the Node tree ([removeDuplicateNode]).
-    private val vectorForHandlingDuplicateNodes: MutableVector<NodeParent> = mutableVectorOf()
+    private val hitPointerIdsAndNodes = MutableLongObjectMap<MutableObjectList<Node>>(10)
 
     /**
      * Associates a [pointerId] to a list of hit [pointerInputNodes] and keeps track of them.
@@ -56,21 +58,34 @@
      * @param pointerId The id of the pointer that was hit tested against [PointerInputFilter]s
      * @param pointerInputNodes The [PointerInputFilter]s that were hit by [pointerId].  Must be
      * ordered from ancestor to descendant.
+     * @param prunePointerIdsAndChangesNotInNodesList Prune [PointerId]s (and associated changes)
+     * that are NOT in the pointerInputNodes parameter from the cached tree of ParentNode/Node.
      */
-    fun addHitPath(pointerId: PointerId, pointerInputNodes: List<Modifier.Node>) {
+    fun addHitPath(
+        pointerId: PointerId,
+        pointerInputNodes: List<Modifier.Node>,
+        prunePointerIdsAndChangesNotInNodesList: Boolean = false
+    ) {
         var parent: NodeParent = root
+        hitPointerIdsAndNodes.clear()
         var merging = true
-        var nodeBranchPathToSkipDuringDuplicateNodeRemoval: Node? = null
 
         eachPin@ for (i in pointerInputNodes.indices) {
             val pointerInputNode = pointerInputNodes[i]
+
             if (merging) {
                 val node = parent.children.firstOrNull {
                     it.modifierNode == pointerInputNode
                 }
+
                 if (node != null) {
                     node.markIsIn()
                     node.pointerIds.add(pointerId)
+
+                    val mutableObjectList =
+                        hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+                    mutableObjectList.add(node)
                     parent = node
                     continue@eachPin
                 } else {
@@ -82,52 +97,30 @@
                 pointerIds.add(pointerId)
             }
 
-            if (nodeBranchPathToSkipDuringDuplicateNodeRemoval == null) {
-                // Null means this is the first new Node created that will need a new branch path
-                // (possibly from a pre-existing cached version of the node chain).
-                // If that is the case, we need to skip this path when looking for duplicate
-                // nodes to remove (that may have previously existed somewhere else in the tree).
-                nodeBranchPathToSkipDuringDuplicateNodeRemoval = node
-            } else {
-                // Every node after the top new node (that is, the top Node in the new path)
-                // could have potentially existed somewhere else in the cached node tree, and
-                // we need to remove it if we are adding it to this new branch.
-                removeDuplicateNode(node, nodeBranchPathToSkipDuringDuplicateNodeRemoval)
-            }
+            val mutableObjectList =
+                hitPointerIdsAndNodes.getOrPut(pointerId.value) { mutableObjectListOf() }
+
+            mutableObjectList.add(node)
 
             parent.children.add(node)
             parent = node
         }
-    }
 
-    /*
-     * Removes duplicate nodes when using a cached version of the node tree. Uses breadth-first
-     * search for simplicity (and because the tree will be very small).
-     */
-    private fun removeDuplicateNode(
-        duplicateNodeToRemove: Node,
-        headOfPathToSkip: Node
-    ) {
-        vectorForHandlingDuplicateNodes.clear()
-        vectorForHandlingDuplicateNodes.add(root)
-
-        while (vectorForHandlingDuplicateNodes.isNotEmpty()) {
-            val parent = vectorForHandlingDuplicateNodes.removeAt(0)
-
-            for (index in parent.children.indices) {
-                val child = parent.children[index]
-                if (child == headOfPathToSkip) continue
-                if (child.modifierNode == duplicateNodeToRemove.modifierNode) {
-                    // Assumes there is only one unique Node in the tree (not copies).
-                    // This also removes all children attached below the node.
-                    parent.children.remove(child)
-                    return
-                }
-                vectorForHandlingDuplicateNodes.add(child)
+        if (prunePointerIdsAndChangesNotInNodesList) {
+            hitPointerIdsAndNodes.forEach { key, value ->
+                removeInvalidPointerIdsAndChanges(key, value)
             }
         }
     }
 
+    // Removes pointers/changes that are not in the latest hit test
+    private fun removeInvalidPointerIdsAndChanges(
+        pointerId: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        root.removeInvalidPointerIdsAndChanges(pointerId, hitNodes)
+    }
+
     /**
      * Dispatches [internalPointerEvent] through the hierarchy.
      *
@@ -175,13 +168,13 @@
     }
 
     /**
-     * Removes [PointerInputFilter]s that have been removed from the component tree.
+     * Removes detached Pointer Input Modifier Nodes.
      */
     // TODO(shepshapard): Ideally, we can process the detaching of PointerInputFilters at the time
     //  that either their associated LayoutNode is removed from the three, or their
     //  associated PointerInputModifier is removed from a LayoutNode.
-    fun removeDetachedPointerInputFilters() {
-        root.removeDetachedPointerInputFilters()
+    fun removeDetachedPointerInputNodes() {
+        root.removeDetachedPointerInputModifierNodes()
     }
 }
 
@@ -272,19 +265,29 @@
         children.clear()
     }
 
+    open fun removeInvalidPointerIdsAndChanges(
+        pointerIdValue: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        children.forEach {
+            it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+        }
+    }
+
     /**
      * Removes all child [Node]s that are no longer attached to the compose tree.
      */
-    fun removeDetachedPointerInputFilters() {
+    fun removeDetachedPointerInputModifierNodes() {
         var index = 0
         while (index < children.size) {
             val child = children[index]
+
             if (!child.modifierNode.isAttached) {
-                children.removeAt(index)
                 child.dispatchCancel()
+                children.removeAt(index)
             } else {
                 index++
-                child.removeDetachedPointerInputFilters()
+                child.removeDetachedPointerInputModifierNodes()
             }
         }
     }
@@ -327,6 +330,22 @@
     private var isIn = true
     private var hasExited = true
 
+    override fun removeInvalidPointerIdsAndChanges(
+        pointerIdValue: Long,
+        hitNodes: MutableObjectList<Node>
+    ) {
+        if (this.pointerIds.contains(pointerIdValue)) {
+            if (!hitNodes.contains(this)) {
+                this.pointerIds.remove(pointerIdValue)
+                this.relevantChanges.remove(pointerIdValue)
+            }
+        }
+
+        children.forEach {
+            it.removeInvalidPointerIdsAndChanges(pointerIdValue, hitNodes)
+        }
+    }
+
     override fun dispatchMainEventPass(
         changes: LongSparseArray<PointerInputChange>,
         parentCoordinates: LayoutCoordinates,
@@ -342,6 +361,7 @@
         return dispatchIfNeeded {
             val event = pointerEvent!!
             val size = coordinates!!.size
+
             // Dispatch on the tunneling pass.
             modifierNode.dispatchForKind(Nodes.PointerInput) {
                 it.onPointerEvent(event, PointerEventPass.Initial, size)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
index f2b624c..61e45fa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerIcon.kt
@@ -168,24 +168,37 @@
         if (pass == Main) {
             // Cursor within the surface area of this node's bounds
             if (pointerEvent.type == PointerEventType.Enter) {
-                cursorInBoundsOfNode = true
-                displayIconIfDescendantsDoNotHavePriority()
+                onEnter()
             } else if (pointerEvent.type == PointerEventType.Exit) {
-                cursorInBoundsOfNode = false
+                onExit()
+            }
+        }
+    }
+
+    private fun onEnter() {
+        cursorInBoundsOfNode = true
+        displayIconIfDescendantsDoNotHavePriority()
+    }
+
+    private fun onExit() {
+        if (cursorInBoundsOfNode) {
+            cursorInBoundsOfNode = false
+
+            if (isAttached) {
                 displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
             }
         }
     }
 
     override fun onCancelPointerInput() {
-        // We aren't processing the event (only listening for enter/exit), so we don't need to
-        // do anything.
+        // While pointer icon only really cares about enter/exit, there are some cases (dynamically
+        // adding Modifier Nodes) where a modifier might be cancelled but hasn't been detached or
+        // exited, so we need to cover that case.
+        onExit()
     }
 
     override fun onDetach() {
-        cursorInBoundsOfNode = false
-        displayIconFromAncestorNodeWithCursorInBoundsOrDefaultIcon()
-
+        onExit()
         super.onDetach()
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
index 4230b9c..e514e78 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt
@@ -98,15 +98,22 @@
                     val isTouchEvent = pointerInputChange.type == PointerType.Touch
                     root.hitTest(pointerInputChange.position, hitResult, isTouchEvent)
                     if (hitResult.isNotEmpty()) {
-                        hitPathTracker.addHitPath(pointerInputChange.id, hitResult)
+                        hitPathTracker.addHitPath(
+                            pointerId = pointerInputChange.id,
+                            pointerInputNodes = hitResult,
+                            // Prunes PointerIds (and changes) to support dynamically
+                            // adding/removing pointer input modifier nodes.
+                            // Note: We do not do this for hover because hover relies on those
+                            // non hit PointerIds to trigger hover exit events.
+                            prunePointerIdsAndChangesNotInNodesList =
+                            pointerInputChange.changedToDownIgnoreConsumed()
+                        )
                         hitResult.clear()
                     }
                 }
             }
 
-            // Remove [PointerInputFilter]s that are no longer valid and refresh the offset information
-            // for those that are.
-            hitPathTracker.removeDetachedPointerInputFilters()
+            hitPathTracker.removeDetachedPointerInputNodes()
 
             // Dispatch to PointerInputFilters
             val dispatchedToSomething =
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 91c2d87..45c7b33 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
@@ -190,11 +190,21 @@
 internal inline fun DelegatableNode.visitLocalDescendants(
     mask: Int,
     block: (Modifier.Node) -> Unit
+) = visitLocalDescendants(
+    mask = mask,
+    includeSelf = false,
+    block = block
+)
+
+internal inline fun DelegatableNode.visitLocalDescendants(
+    mask: Int,
+    includeSelf: Boolean = false,
+    block: (Modifier.Node) -> Unit
 ) {
     checkPrecondition(node.isAttached) { "visitLocalDescendants called on an unattached node" }
     val self = node
     if (self.aggregateChildKindSet and mask == 0) return
-    var next = self.child
+    var next = if (includeSelf) self else self.child
     while (next != null) {
         if (next.kindSet and mask != 0) {
             block(next)
@@ -217,6 +227,13 @@
     }
 }
 
+internal inline fun <reified T> DelegatableNode.visitSelfAndLocalDescendants(
+    type: NodeKind<T>,
+    block: (T) -> Unit
+) = visitLocalDescendants(mask = type.mask, includeSelf = true) {
+    it.dispatchForKind(type, block)
+}
+
 internal inline fun <reified T> DelegatableNode.visitLocalDescendants(
     type: NodeKind<T>,
     block: (T) -> Unit
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
index c41e989..0203f66 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/HitTestResult.kt
@@ -40,6 +40,8 @@
     override var size: Int = 0
         private set
 
+    var shouldSharePointerInputWithSibling = true
+
     /**
      * `true` when there has been a direct hit within touch bounds ([hit] called) or
      * `false` otherwise.
@@ -95,6 +97,9 @@
      */
     fun hit(node: Modifier.Node, isInLayer: Boolean, childHitTest: () -> Unit) {
         hitInMinimumTouchTarget(node, -1f, isInLayer, childHitTest)
+        if (node.coordinator?.shouldSharePointerInputWithSiblings() == false) {
+            shouldSharePointerInputWithSibling = false
+        }
     }
 
     /**
@@ -238,6 +243,7 @@
     fun clear() {
         hitDepth = -1
         resizeToHitDepth()
+        shouldSharePointerInputWithSibling = true
     }
 
     private inner class HitTestResultIterator(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index 26d852c..3b79baa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -240,9 +240,7 @@
                         val continueHitTest: Boolean
                         if (!wasHit) {
                             continueHitTest = true
-                        } else if (
-                            child.outerCoordinator.shouldSharePointerInputWithSiblings()
-                        ) {
+                        } else if (hitTestResult.shouldSharePointerInputWithSibling) {
                             hitTestResult.acceptHits()
                             continueHitTest = true
                         } else {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 8a6b586..c3aa5ec 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1215,7 +1215,10 @@
         val start = headNode(Nodes.PointerInput.includeSelfInTraversal) ?: return false
 
         if (start.isAttached) {
-            start.visitLocalDescendants(Nodes.PointerInput) {
+            // We have to check both the self and local descendants, because the `start` can also
+            // be a `PointerInputModifierNode` (when the first modifier node on the LayoutNode is
+            // a `PointerInputModifierNode`).
+            start.visitSelfAndLocalDescendants(Nodes.PointerInput) {
                 if (it.sharePointerInputWithSiblings()) return true
             }
         }
diff --git a/core/core/src/main/java/androidx/core/text/HtmlCompat.java b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
index 7ae8f24..61ec500 100644
--- a/core/core/src/main/java/androidx/core/text/HtmlCompat.java
+++ b/core/core/src/main/java/androidx/core/text/HtmlCompat.java
@@ -46,49 +46,49 @@
 public final class HtmlCompat {
     /**
      * Option for {@link #fromHtml(String, int)}: Wrap consecutive lines of text delimited by '\n'
-     * inside &lt;p&gt; elements. {@link BulletSpan}s are ignored.
+     * inside <code>&lt;p&gt;</code> elements. {@link BulletSpan}s are ignored.
      */
     public static final int TO_HTML_PARAGRAPH_LINES_CONSECUTIVE =
             Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE;
     /**
      * Option for {@link #fromHtml(String, int)}: Wrap each line of text delimited by '\n' inside a
-     * &lt;p&gt; or a &lt;li&gt; element. This allows {@link ParagraphStyle}s attached to be
-     * encoded as CSS styles within the corresponding &lt;p&gt; or &lt;li&gt; element.
+     * <code>&lt;p&gt;</code> or a <code>&lt;li&gt;</code> element. This allows {@link ParagraphStyle}s attached to be
+     * encoded as CSS styles within the corresponding <code>&lt;p&gt;</code> or <code>&lt;li&gt;</code> element.
      */
     public static final int TO_HTML_PARAGRAPH_LINES_INDIVIDUAL =
             Html.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL;
     /**
-     * Flag indicating that texts inside &lt;p&gt; elements will be separated from other texts with
+     * Flag indicating that texts inside <code>&lt;p&gt;</code> elements will be separated from other texts with
      * one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH =
             Html.FROM_HTML_SEPARATOR_LINE_BREAK_PARAGRAPH;
     /**
-     * Flag indicating that texts inside &lt;h1&gt;~&lt;h6&gt; elements will be separated from
+     * Flag indicating that texts inside <code>&lt;h1&gt;</code>~<code>&lt;h6&gt;</code> elements will be separated from
      * other texts with one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_HEADING =
             Html.FROM_HTML_SEPARATOR_LINE_BREAK_HEADING;
     /**
-     * Flag indicating that texts inside &lt;li&gt; elements will be separated from other texts
+     * Flag indicating that texts inside <code>&lt;li&gt;</code> elements will be separated from other texts
      * with one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM =
             Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM;
     /**
-     * Flag indicating that texts inside &lt;ul&gt; elements will be separated from other texts
+     * Flag indicating that texts inside <code>&lt;ul&gt;</code> elements will be separated from other texts
      * with one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_LIST =
             Html.FROM_HTML_SEPARATOR_LINE_BREAK_LIST;
     /**
-     * Flag indicating that texts inside &lt;div&gt; elements will be separated from other texts
+     * Flag indicating that texts inside <code>&lt;div&gt;<c/ode> elements will be separated from other texts
      * with one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_DIV =
             Html.FROM_HTML_SEPARATOR_LINE_BREAK_DIV;
     /**
-     * Flag indicating that texts inside &lt;blockquote&gt; elements will be separated from other
+     * Flag indicating that texts inside <code>&lt;blockquote&gt;</code> elements will be separated from other
      * texts with one newline character by default.
      */
     public static final int FROM_HTML_SEPARATOR_LINE_BREAK_BLOCKQUOTE =
diff --git a/datastore/datastore-compose-samples/build.gradle b/datastore/datastore-compose-samples/build.gradle
index 698b179..2e5f736 100644
--- a/datastore/datastore-compose-samples/build.gradle
+++ b/datastore/datastore-compose-samples/build.gradle
@@ -33,9 +33,10 @@
 }
 
 dependencies {
+    compileOnly(projectOrArtifact(":datastore:datastore-preferences-external-protobuf"))
+
     implementation(libs.protobufLite)
     implementation(libs.kotlinStdlib)
-
     implementation('androidx.core:core-ktx:1.7.0')
     implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.3.1')
     implementation('androidx.activity:activity-compose:1.3.1')
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index cff152d..8dacda8 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -21,7 +21,6 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.BundleInsideHelper
 import androidx.build.LibraryType
 import androidx.build.PlatformIdentifier
 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
@@ -67,6 +66,9 @@
         }
         jvmMain {
             dependsOn(commonMain)
+            dependencies {
+                implementation(project(":datastore:datastore-preferences-proto"))
+            }
         }
         jvmTest {
             dependsOn(commonTest)
@@ -107,19 +109,6 @@
     }
 }
 
-BundleInsideHelper.forInsideJarKmp(
-        project,
-        /* from = */ "com.google.protobuf",
-        /* to =   */ "androidx.datastore.preferences.protobuf",
-        // proto-lite dependency includes .proto files, which are not used and would clash if
-        // users also use proto library directly
-        /* dropResourcesWithSuffix = */ ".proto"
-)
-
-dependencies {
-    bundleInside(project(":datastore:datastore-preferences-proto"))
-}
-
 androidx {
     name = "Preferences DataStore Core"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/datastore/datastore-preferences-external-protobuf/api/current.txt b/datastore/datastore-preferences-external-protobuf/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt b/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/datastore/datastore-preferences-external-protobuf/build.gradle b/datastore/datastore-preferences-external-protobuf/build.gradle
new file mode 100644
index 0000000..506da76
--- /dev/null
+++ b/datastore/datastore-preferences-external-protobuf/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("AndroidXRepackagePlugin")
+    id("java-library")
+}
+
+repackage {
+    addRelocation {
+        sourcePackage = "com.google.protobuf"
+        targetPackage =  "androidx.datastore.preferences.protobuf"
+    }
+    artifactId = "datastore-preferences-external-protobuf"
+}
+
+dependencies {
+    repackage(libs.protobufLite)
+}
+
+androidx {
+    name = "Preferences External Protobuf"
+    type = LibraryType.PUBLISHED_LIBRARY
+    inceptionYear = "2024"
+    description =  "Repackaged proto-lite dependency for use by datastore preferences"
+    doNotDocumentReason = "Repackaging only"
+    license.name = "BSD-3-Clause"
+    license.url = "https://opensource.org/licenses/BSD-3-Clause"
+}
diff --git a/datastore/datastore-preferences-proto/build.gradle b/datastore/datastore-preferences-proto/build.gradle
index d5b5bb1..860174f 100644
--- a/datastore/datastore-preferences-proto/build.gradle
+++ b/datastore/datastore-preferences-proto/build.gradle
@@ -21,17 +21,30 @@
  * Please use that script when creating a new project, rather than copying an existing project and
  * modifying its settings.
  */
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.LibraryType
+import androidx.build.RunApiTasks
 
 plugins {
     id("AndroidXPlugin")
+    id("AndroidXRepackagePlugin")
     id("kotlin")
     id("com.google.protobuf")
 }
 
+repackage {
+    // Must match what is in datastore/datastore-preferences-external-protobuf/build.gradle
+    addRelocation {
+        sourcePackage = "com.google.protobuf"
+        targetPackage =  "androidx.datastore.preferences.protobuf"
+    }
+}
+
 dependencies {
-    implementation(libs.protobufLite)
+    api(project(":datastore:datastore-preferences-external-protobuf"))
+    // Must be compileOnly to not bring in protobufLite in runtime
+    // Repackaged protobufLite brought in by
+    // project(":datastore:datastore-preferences-external-protobuf") and used at runtime
+    compileOnly(libs.protobufLite)
     compileOnly(project(":datastore:datastore-core"))
 }
 
@@ -61,8 +74,9 @@
 
 androidx {
     name = "Preferences DataStore Proto"
-    publish = Publish.NONE
+    type = LibraryType.PUBLISHED_LIBRARY
     inceptionYear = "2020"
-    description = "Jarjar the generated proto and proto-lite dependency for use by " +
-            "datastore-preferences."
+    description = "Jarjar the generated proto for use by datastore-preferences."
+    runApiTasks = new RunApiTasks.No("Metalava doesn't properly parse the proto sources " +
+            "(b/180579063)")
 }
diff --git a/development/update_studio.sh b/development/update_studio.sh
index ef74a284..324322f 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -7,8 +7,8 @@
 
 # Versions that the user should update when running this script
 echo Getting Studio version and link
-AGP_VERSION=${1:-8.4.0-alpha12}
-STUDIO_VERSION_STRING=${2:-"Android Studio Jellyfish | 2023.3.1 Canary 12"}
+AGP_VERSION=${1:-8.5.0-alpha06}
+STUDIO_VERSION_STRING=${2:-"Android Studio Koala | 2024.1.1 Canary 6"}
 
 # Get studio version number from version name
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep "<iframe " | sed "s/.* src=\"\([^\"]*\)\".*/\1/g"`
diff --git a/gradle.properties b/gradle.properties
index 7a42aa7..68fc0ca 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,6 +21,7 @@
 # fullsdk-linux/**/package.xml -> b/291331139
 org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=**/prebuilts/fullsdk-linux;**/prebuilts/fullsdk-linux/platforms/android-*/package.xml
 
+android.javaCompile.suppressSourceTargetDeprecationWarning=true
 android.lint.baselineOmitLineNumbers=true
 android.lint.printStackTrace=true
 android.builder.sdkDownload=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 9c01869..da3f63c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,13 +2,13 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "8.4.0-alpha12"
+androidGradlePlugin = "8.5.0-alpha06"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.4.0-alpha12"
+androidLint = "31.5.0-alpha06"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2023.3.1.12"
+androidStudio = "2024.1.1.4"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e5c45a0..1d107cd 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=../../../../tools/external/gradle/gradle-8.7-bin.zip
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionUrl=../../../../tools/external/gradle/gradle-8.8-rc-1-bin.zip
+distributionSha256Sum=a2e1cfee7ffdeee86015b85b2dd2a435032c40eedc01d8172285556c7d8fea13
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index e13d44f..dce5e38 100755
--- a/gradlew
+++ b/gradlew
@@ -16,7 +16,6 @@
     OUT_DIR="$(cd $OUT_DIR && pwd -P)"
     export GRADLE_USER_HOME="$OUT_DIR/.gradle"
     export TMPDIR="$OUT_DIR/tmp"
-    mkdir -p "$TMPDIR"
 else
     CHECKOUT_ROOT="$(cd $SCRIPT_PATH/../.. && pwd -P)"
     export OUT_DIR="$CHECKOUT_ROOT/out"
@@ -230,9 +229,6 @@
 if [ "$GRADLE_USER_HOME" != "" ]; then
     HOME_SYSTEM_PROPERTY_ARGUMENT="-Duser.home=$GRADLE_USER_HOME"
 fi
-if [ "$TMPDIR" != "" ]; then
-  TMPDIR_ARG="-Djava.io.tmpdir=$TMPDIR"
-fi
 
 if [[ " ${@} " =~ " --clean " ]]; then
   cleanCaches=true
@@ -407,6 +403,11 @@
 }
 
 function runGradle() {
+  if [ "$TMPDIR" != "" ]; then
+    mkdir -p "$TMPDIR"
+    TMPDIR_ARG="-Djava.io.tmpdir=$TMPDIR"
+  fi
+
   processOutput=false
   if [[ " ${@} " =~ " -Pandroidx.validateNoUnrecognizedMessages " ]]; then
     processOutput=true
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
index 97d6f65..469140c 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/opengl/egl/EGLSpec.kt
@@ -111,6 +111,7 @@
      * See https://www.khronos.org/registry/EGL/sdk/docs/man/html/eglMakeCurrent.xhtml for more
      * information
      *
+     * @param context EGL rendering context to be attached to the surfaces
      * @param drawSurface EGLSurface to draw pixels into.
      * @param readSurface EGLSurface used for read/copy operations.
      */
diff --git a/kruth/kruth/api/current.ignore b/kruth/kruth/api/current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/current.ignore
+++ b/kruth/kruth/api/current.ignore
@@ -73,8 +73,6 @@
     Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
 RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
     Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
-    Removed class androidx.kruth.TableSubject
 RemovedClass: androidx.kruth.Truth:
     Removed class androidx.kruth.Truth
 RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/current.txt b/kruth/kruth/api/current.txt
index 857e856..26a1b93 100644
--- a/kruth/kruth/api/current.txt
+++ b/kruth/kruth/api/current.txt
@@ -201,6 +201,7 @@
     method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
     method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
     method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+    method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
     method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
     method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
   }
@@ -418,6 +419,21 @@
     method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
   }
 
+  public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+    method public void contains(R rowKey, C columnKey);
+    method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void containsCell(R rowKey, C colKey, V value);
+    method public void containsColumn(C columnKey);
+    method public void containsRow(R rowKey);
+    method public void containsValue(V value);
+    method public void doesNotContain(R rowKey, C columnKey);
+    method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void doesNotContainCell(R rowKey, C colKey, V value);
+    method public void hasSize(int expectedSize);
+    method public void isEmpty();
+    method public void isNotEmpty();
+  }
+
   public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
     ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
     method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/api/restricted_current.ignore b/kruth/kruth/api/restricted_current.ignore
index 2fc7dd5..5589f1d 100644
--- a/kruth/kruth/api/restricted_current.ignore
+++ b/kruth/kruth/api/restricted_current.ignore
@@ -73,8 +73,6 @@
     Removed class androidx.kruth.PrimitiveDoubleArraySubject.DoubleArrayAsIterable
 RemovedClass: androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable:
     Removed class androidx.kruth.PrimitiveFloatArraySubject.FloatArrayAsIterable
-RemovedClass: androidx.kruth.TableSubject:
-    Removed class androidx.kruth.TableSubject
 RemovedClass: androidx.kruth.Truth:
     Removed class androidx.kruth.Truth
 RemovedClass: androidx.kruth.TruthJUnit:
diff --git a/kruth/kruth/api/restricted_current.txt b/kruth/kruth/api/restricted_current.txt
index 90ec259..4d8c416 100644
--- a/kruth/kruth/api/restricted_current.txt
+++ b/kruth/kruth/api/restricted_current.txt
@@ -201,6 +201,7 @@
     method public static <T> androidx.kruth.GuavaOptionalSubject<T> assertThat(com.google.common.base.Optional<T>? actual);
     method public static <K, V> androidx.kruth.MultimapSubject<K,V> assertThat(com.google.common.collect.Multimap<K,V> actual);
     method public static <T> androidx.kruth.MultisetSubject<T> assertThat(com.google.common.collect.Multiset<T> actual);
+    method public static <R, C, V> androidx.kruth.TableSubject<R,C,V> assertThat(com.google.common.collect.Table<R,C,V> actual);
     method public static androidx.kruth.ClassSubject assertThat(Class<?> actual);
     method public static androidx.kruth.BigDecimalSubject assertThat(java.math.BigDecimal actual);
   }
@@ -419,6 +420,21 @@
     method public SubjectT createSubject(androidx.kruth.FailureMetadata metadata, ActualT? actual);
   }
 
+  public final class TableSubject<R, C, V> extends androidx.kruth.Subject<com.google.common.collect.Table<R,C,V>> {
+    method public void contains(R rowKey, C columnKey);
+    method public void containsCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void containsCell(R rowKey, C colKey, V value);
+    method public void containsColumn(C columnKey);
+    method public void containsRow(R rowKey);
+    method public void containsValue(V value);
+    method public void doesNotContain(R rowKey, C columnKey);
+    method public void doesNotContainCell(com.google.common.collect.Table.Cell<R,C,V>? cell);
+    method public void doesNotContainCell(R rowKey, C colKey, V value);
+    method public void hasSize(int expectedSize);
+    method public void isEmpty();
+    method public void isNotEmpty();
+  }
+
   public class ThrowableSubject<T extends java.lang.Throwable> extends androidx.kruth.Subject<T> {
     ctor protected ThrowableSubject(androidx.kruth.FailureMetadata metadata, T? actual);
     method public final androidx.kruth.ThrowableSubject<java.lang.Throwable> hasCauseThat();
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
index ea1fbc7..9b8c123 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/Kruth.jvm.kt
@@ -19,6 +19,7 @@
 import com.google.common.base.Optional
 import com.google.common.collect.Multimap
 import com.google.common.collect.Multiset
+import com.google.common.collect.Table
 import java.math.BigDecimal
 
 fun assertThat(actual: Class<*>): ClassSubject =
@@ -35,3 +36,6 @@
 
 fun <K, V> assertThat(actual: Multimap<K, V>): MultimapSubject<K, V> =
     MultimapSubject(actual = actual)
+
+fun <R, C, V> assertThat(actual: Table<R, C, V>): TableSubject<R, C, V> =
+    TableSubject(actual = actual)
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
index 891e0a2..e4030e6 100644
--- a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/PlatformStandardSubjectBuilder.jvm.kt
@@ -19,6 +19,7 @@
 import com.google.common.base.Optional
 import com.google.common.collect.Multimap
 import com.google.common.collect.Multiset
+import com.google.common.collect.Table
 import java.math.BigDecimal
 
 internal actual interface PlatformStandardSubjectBuilder {
@@ -28,6 +29,7 @@
     fun that(actual: BigDecimal): BigDecimalSubject
     fun <T> that(actual: Multiset<T>): MultisetSubject<T>
     fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V>
+    fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V>
 }
 
 internal actual class PlatformStandardSubjectBuilderImpl actual constructor(
@@ -48,4 +50,7 @@
 
     override fun <K, V> that(actual: Multimap<K, V>): MultimapSubject<K, V> =
         MultimapSubject(actual = actual, metadata = metadata)
+
+    override fun <R, C, V> that(actual: Table<R, C, V>): TableSubject<R, C, V> =
+        TableSubject(actual = actual, metadata = metadata)
 }
diff --git a/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
new file mode 100644
index 0000000..80f873e
--- /dev/null
+++ b/kruth/kruth/src/jvmMain/kotlin/androidx/kruth/TableSubject.jvm.kt
@@ -0,0 +1,134 @@
+/*
+ * 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.kruth
+
+import androidx.kruth.Fact.Companion.fact
+import androidx.kruth.Fact.Companion.simpleFact
+import com.google.common.collect.Table
+import com.google.common.collect.Table.Cell
+import com.google.common.collect.Tables.immutableCell
+
+class TableSubject<R, C, V> internal constructor(
+    actual: Table<R, C, V>,
+    metadata: FailureMetadata = FailureMetadata(),
+) : Subject<Table<R, C, V>>(actual, metadata, typeDescriptionOverride = null) {
+
+    /** Fails if the table is not empty. */
+    fun isEmpty() {
+        requireNonNull(actual)
+
+        if (!actual.isEmpty) {
+            failWithActual(simpleFact("expected to be empty"))
+        }
+    }
+
+    /** Fails if the table is empty. */
+    fun isNotEmpty() {
+        requireNonNull(actual)
+
+        if (actual.isEmpty) {
+            failWithoutActual(simpleFact("expected not to be empty"))
+        }
+    }
+
+    /** Fails if the table does not have the given size. */
+    fun hasSize(expectedSize: Int) {
+        require(expectedSize >= 0) { "expectedSize($expectedSize) must be >= 0" }
+        requireNonNull(actual)
+
+        check("size()").that(actual.size()).isEqualTo(expectedSize)
+    }
+
+    /** Fails if the table does not contain a mapping for the given row key and column key. */
+    fun contains(rowKey: R, columnKey: C) {
+        requireNonNull(actual)
+
+        if (!actual.contains(rowKey, columnKey)) {
+            failWithActual(
+                simpleFact("expected to contain mapping for row-column key pair"),
+                fact("row key", rowKey),
+                fact("column key", columnKey),
+            )
+        }
+    }
+
+    /** Fails if the table contains a mapping for the given row key and column key. */
+    fun doesNotContain(rowKey: R, columnKey: C) {
+        requireNonNull(actual)
+
+        if (actual.contains(rowKey, columnKey)) {
+            failWithoutActual(
+                simpleFact("expected not to contain mapping for row-column key pair"),
+                fact("row key", rowKey),
+                fact("column key", columnKey),
+                fact("but contained value", actual[rowKey, columnKey]),
+                fact("full contents", actual),
+            )
+        }
+    }
+
+    /** Fails if the table does not contain the given cell. */
+    fun containsCell(rowKey: R, colKey: C, value: V) {
+        containsCell(immutableCell(rowKey, colKey, value))
+    }
+
+    /** Fails if the table does not contain the given cell. */
+    fun containsCell(cell: Cell<R, C, V>?) {
+        requireNonNull(cell)
+        requireNonNull(actual)
+
+        checkNoNeedToDisplayBothValues("cellSet()")
+            .that(actual.cellSet())
+            .contains(cell)
+    }
+
+    /** Fails if the table contains the given cell. */
+    fun doesNotContainCell(rowKey: R, colKey: C, value: V) {
+        doesNotContainCell(immutableCell(rowKey, colKey, value))
+    }
+
+    /** Fails if the table contains the given cell. */
+    fun doesNotContainCell(cell: Cell<R, C, V>?) {
+        requireNonNull(cell)
+        requireNonNull(actual)
+
+        checkNoNeedToDisplayBothValues("cellSet()")
+            .that(actual.cellSet())
+            .doesNotContain(cell)
+    }
+
+    /** Fails if the table does not contain the given row key. */
+    fun containsRow(rowKey: R) {
+        requireNonNull(actual)
+
+        check("rowKeySet()").that(actual.rowKeySet()).contains(rowKey)
+    }
+
+    /** Fails if the table does not contain the given column key. */
+    fun containsColumn(columnKey: C) {
+        requireNonNull(actual)
+
+        check("columnKeySet()").that(actual.columnKeySet()).contains(columnKey)
+    }
+
+    /** Fails if the table does not contain the given value. */
+    fun containsValue(value: V) {
+        requireNonNull(actual)
+
+        check("values()").that(actual.values()).contains(value)
+    }
+}
diff --git a/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
new file mode 100644
index 0000000..a2a1157
--- /dev/null
+++ b/kruth/kruth/src/jvmTest/kotlin/androidx/kruth/TableSubjectTest.jvm.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.kruth
+
+import com.google.common.collect.ImmutableTable
+import com.google.common.collect.Tables.immutableCell
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class TableSubjectTest {
+
+    @Test
+    fun tableIsEmpty() {
+        val table = ImmutableTable.of<String, String, String>()
+        assertThat(table).isEmpty()
+    }
+
+    @Test
+    fun tableIsEmptyWithFailure() {
+        val table = ImmutableTable.of(1, 5, 7)
+        assertFailsWith<AssertionError> {
+            assertThat(table).isEmpty()
+        }
+    }
+
+    @Test
+    fun tableIsNotEmpty() {
+        val table = ImmutableTable.of(1, 5, 7)
+        assertThat(table).isNotEmpty()
+    }
+
+    @Test
+    fun tableIsNotEmptyWithFailure() {
+        val table = ImmutableTable.of<Int, Int, Int>()
+        assertFailsWith<AssertionError> {
+            assertThat(table).isNotEmpty()
+        }
+    }
+
+    @Test
+    fun hasSize() {
+        assertThat(ImmutableTable.of(1, 2, 3)).hasSize(1)
+    }
+
+    @Test
+    fun hasSizeZero() {
+        assertThat(ImmutableTable.of<Any, Any, Any>()).hasSize(0)
+    }
+
+    @Test
+    fun hasSizeNegative() {
+        assertFailsWith<IllegalArgumentException> {
+            assertThat(ImmutableTable.of(1, 2, 3)).hasSize(-1)
+        }
+    }
+
+    @Test
+    fun contains() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).contains("row", "col")
+    }
+
+    @Test
+    fun containsFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+
+        assertFailsWith<AssertionError> {
+            assertThat(table).contains("row", "otherCol")
+        }
+    }
+
+    @Test
+    fun doesNotContain() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).doesNotContain("row", "row")
+        assertThat(table).doesNotContain("col", "row")
+        assertThat(table).doesNotContain("col", "col")
+        assertThat(table).doesNotContain(null, null)
+    }
+
+    @Test
+    fun doesNotContainFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).doesNotContain("row", "col")
+        }
+    }
+
+    @Test
+    fun containsCell() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).containsCell("row", "col", "val")
+        assertThat(table).containsCell(immutableCell("row", "col", "val"))
+    }
+
+    @Test
+    fun containsCellFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).containsCell("row", "row", "val")
+        }
+    }
+
+    @Test
+    fun doesNotContainCell() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertThat(table).doesNotContainCell("row", "row", "val")
+        assertThat(table).doesNotContainCell("col", "row", "val")
+        assertThat(table).doesNotContainCell("col", "col", "val")
+        assertThat(table).doesNotContainCell(null, null, null)
+        assertThat(table).doesNotContainCell(immutableCell("row", "row", "val"))
+        assertThat(table).doesNotContainCell(immutableCell("col", "row", "val"))
+        assertThat(table).doesNotContainCell(immutableCell("col", "col", "val"))
+        assertThat(table).doesNotContainCell(immutableCell(null, null, null))
+    }
+
+    @Test
+    fun doesNotContainCellFailure() {
+        val table = ImmutableTable.of("row", "col", "val")
+        assertFailsWith<AssertionError> {
+            assertThat(table).doesNotContainCell("row", "col", "val")
+        }
+    }
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
index 8bcf5f0..a42b7d8 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidInstrumentedTest/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaverTest.android.kt
@@ -359,6 +359,76 @@
         assertThat(getOptionalCount!!()).isEqualTo(1)
         assertThat(savedStateHandle?.keys()).isEqualTo(setOf("count"))
     }
+
+    @OptIn(SavedStateHandleSaveableApi::class)
+    @Test
+    fun noConflictKeys_delegate_simpleRestore() {
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val viewModel = viewModel<SavingTestViewModel>(activity)
+                val firstClass = FirstClass(viewModel.savedStateHandle)
+                val secondClass = SecondClass(viewModel.savedStateHandle)
+                assertThat(firstClass.savedProperty).isEqualTo("One")
+                assertThat(secondClass.savedProperty).isEqualTo("Two")
+            }
+        }
+
+        activityTestRuleScenario.scenario.recreate()
+
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val viewModel = viewModel<SavingTestViewModel>(activity)
+                val firstClass = FirstClass(viewModel.savedStateHandle)
+                val secondClass = SecondClass(viewModel.savedStateHandle)
+                assertThat(firstClass.savedProperty).isEqualTo("One")
+                assertThat(secondClass.savedProperty).isEqualTo("Two")
+            }
+        }
+    }
+
+    @OptIn(SavedStateHandleSaveableApi::class)
+    @Test
+    fun conflictKeys_local_delegate_simpleRestore() {
+
+        fun firstFunction(handle: SavedStateHandle): String {
+            val localProperty by handle.saveable { mutableStateOf("One") }
+            return localProperty
+        }
+
+        fun secondFunction(handle: SavedStateHandle): String {
+            val localProperty by handle.saveable { mutableStateOf("Two") }
+            return localProperty
+        }
+
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val savedStateHandle = viewModel<SavingTestViewModel>(activity).savedStateHandle
+                firstFunction(savedStateHandle)
+                secondFunction(savedStateHandle)
+            }
+        }
+
+        activityTestRuleScenario.scenario.recreate()
+
+        activityTestRuleScenario.scenario.onActivity { activity ->
+            activity.setContent {
+                val savedStateHandle = viewModel<SavingTestViewModel>(activity).savedStateHandle
+                // TODO(b/331695354): Fix local property saveable delegate key conflict
+                assertThat(firstFunction(savedStateHandle)).isEqualTo("Two")
+                assertThat(secondFunction(savedStateHandle)).isEqualTo("Two")
+            }
+        }
+    }
 }
 
 class SavingTestViewModel(val savedStateHandle: SavedStateHandle) : ViewModel()
+
+class FirstClass(savedStateHandle: SavedStateHandle) {
+    @OptIn(SavedStateHandleSaveableApi::class)
+    val savedProperty by savedStateHandle.saveable { mutableStateOf("One") }
+}
+
+class SecondClass(savedStateHandle: SavedStateHandle) {
+    @OptIn(SavedStateHandleSaveableApi::class)
+    val savedProperty by savedStateHandle.saveable { mutableStateOf("Two") }
+}
diff --git a/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt b/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
index ada4c35..019167e 100644
--- a/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
+++ b/lifecycle/lifecycle-viewmodel-compose/src/androidMain/kotlin/androidx/lifecycle/viewmodel/compose/SavedStateHandleSaver.android.kt
@@ -117,9 +117,10 @@
     saver: Saver<T, out Any> = autoSaver(),
     init: () -> T,
 ): PropertyDelegateProvider<Any?, ReadOnlyProperty<Any?, T>> =
-    PropertyDelegateProvider { _, property ->
+    PropertyDelegateProvider { thisRef, property ->
+        val classNamePrefix = if (thisRef != null) thisRef::class.qualifiedName + "." else ""
         val value = saveable(
-            key = property.name,
+            key = classNamePrefix + property.name,
             saver = saver,
             init = init
         )
@@ -155,9 +156,10 @@
     stateSaver: Saver<T, out Any> = autoSaver(),
     init: () -> M,
 ): PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> =
-    PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> { _, property ->
+    PropertyDelegateProvider<Any?, ReadWriteProperty<Any?, T>> { thisRef, property ->
+        val classNamePrefix = if (thisRef != null) thisRef::class.qualifiedName + "." else ""
         val mutableState = saveable(
-            key = property.name,
+            key = classNamePrefix + property.name,
             stateSaver = stateSaver,
             init = init
         )
diff --git a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
index 6057cc1..9a36c5f 100644
--- a/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/ClassVerificationFailureDetector.kt
@@ -487,7 +487,7 @@
 
             // Builtin R8 desugaring, such as rewriting compare calls (see b/36390874)
             if (owner.startsWith("java.") &&
-                DesugaredMethodLookup.isDesugared(owner, name, desc, context.sourceSetType)) {
+                DesugaredMethodLookup.isDesugaredMethod(owner, name, desc, context.sourceSetType)) {
                 return
             }
 
@@ -573,7 +573,7 @@
             api: Int
         ): LintFix? {
             val callPsi = call.sourcePsi ?: return null
-            if (isKotlin(callPsi)) {
+            if (isKotlin(callPsi.language)) {
                 // We only support Java right now.
                 return null
             }
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
index a9f7fbd..a773acc 100644
--- a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -59,7 +59,7 @@
                 return
             }
 
-            if (!isKotlin(node)) return
+            if (!isKotlin(node.language)) return
             if (!node.isInterface) return
             if (node.annotatedWithAnyOf(
                     // If the interface is not stable, it doesn't need the annotation
diff --git a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
index 6ed71ee..bf0227e 100644
--- a/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/NullabilityAnnotationsDetector.kt
@@ -40,7 +40,8 @@
 
     private inner class AnnotationChecker(val context: JavaContext) : UElementHandler() {
         override fun visitAnnotation(node: UAnnotation) {
-            if (isJava(node.sourcePsi)) {
+            val element = node.sourcePsi
+            if (element != null && isJava(element.language)) {
                 checkForAnnotation(node, "NotNull", "NonNull")
                 checkForAnnotation(node, "Nullable", "Nullable")
             }
diff --git a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
index 36cbf9cd..9eaf45c 100644
--- a/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/RestrictToDetector.kt
@@ -86,7 +86,9 @@
             // here, but that points to impl classes in its hierarchy which leads to
             // class loading trouble.
             val sourcePsi = element.sourcePsi
-            if (isKotlin(sourcePsi) && sourcePsi?.parent?.toString() == "CONSTRUCTOR_CALLEE") {
+            if (sourcePsi != null &&
+                isKotlin(sourcePsi.language) &&
+                sourcePsi.parent?.toString() == "CONSTRUCTOR_CALLEE") {
                 return
             }
         }
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
index 51fbfe1..19045af 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RemotePlaybackClient.java
@@ -62,6 +62,7 @@
     /**
      * Creates a remote playback client for a route.
      *
+     * @param context The {@link Context}.
      * @param route The media route.
      */
     public RemotePlaybackClient(@NonNull Context context, @NonNull MediaRouter.RouteInfo route) {
diff --git a/mediarouter/mediarouter/src/main/res/values-iw/strings.xml b/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
index 03371fa..7bafb1a 100644
--- a/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
+++ b/mediarouter/mediarouter/src/main/res/values-iw/strings.xml
@@ -18,11 +18,11 @@
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
     <string name="mr_system_route_name" msgid="7449553026175453403">"מערכת"</string>
     <string name="mr_user_route_category_name" msgid="4088331695424166162">"מכשירים"</string>
-    <string name="mr_button_content_description" msgid="2939063992730535343">"‏העברה (cast)"</string>
-    <string name="mr_cast_button_disconnected" msgid="8071109333469380363">"‏העברה (cast). אין חיבור"</string>
-    <string name="mr_cast_button_connecting" msgid="6629927151350192407">"‏העברה (cast). מתבצעת התחברות"</string>
-    <string name="mr_cast_button_connected" msgid="6073720094880410356">"‏העברה (cast). מחובר"</string>
-    <string name="mr_chooser_title" msgid="1419936397646839840">"העברה אל"</string>
+    <string name="mr_button_content_description" msgid="2939063992730535343">"‏הפעלת Cast"</string>
+    <string name="mr_cast_button_disconnected" msgid="8071109333469380363">"‏הפעלת Cast. אין מכשיר מחובר"</string>
+    <string name="mr_cast_button_connecting" msgid="6629927151350192407">"‏הפעלת Cast. מתחברים למכשיר"</string>
+    <string name="mr_cast_button_connected" msgid="6073720094880410356">"‏הפעלת Cast. יש מכשיר מחובר"</string>
+    <string name="mr_chooser_title" msgid="1419936397646839840">"‏הפעלת Cast אל"</string>
     <string name="mr_chooser_searching" msgid="6114250663023140921">"מתבצע חיפוש מכשירים"</string>
     <string name="mr_chooser_looking_for_devices" msgid="4257319068277776035">"מתבצע חיפוש מכשירים…"</string>
     <string name="mr_controller_disconnect" msgid="7812275474138309497">"ניתוק"</string>
@@ -50,5 +50,5 @@
     <string name="mr_chooser_wifi_warning_description_car" msgid="2998902945608081567">"‏מוודאים שהמכשיר השני והרכב מחוברים לאותה רשת Wi-Fi"</string>
     <string name="mr_chooser_wifi_warning_description_unknown" msgid="3459891599800041449">"‏מוודאים ששני המכשירים מחוברים לאותה רשת Wi-Fi"</string>
     <string name="mr_chooser_wifi_learn_more" msgid="3799500840179081429"><a href="https://support.google.com/chromecast/?p=trouble-finding-devices">"מידע נוסף"</a></string>
-    <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"‏איך להעביר (cast)"</string>
+    <string name="ic_media_route_learn_more_accessibility" msgid="9119039724000326934">"‏איך להפעיל Cast"</string>
 </resources>
diff --git a/playground-common/gradle/wrapper/gradle-wrapper.properties b/playground-common/gradle/wrapper/gradle-wrapper.properties
index 61f9702..fc4b959 100644
--- a/playground-common/gradle/wrapper/gradle-wrapper.properties
+++ b/playground-common/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
-distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-bin.zip
+distributionSha256Sum=a2e1cfee7ffdeee86015b85b2dd2a435032c40eedc01d8172285556c7d8fea13
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index 51022df..84fb587 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -656,6 +656,7 @@
 includeProject(":datastore:datastore-compose-samples", [BuildType.COMPOSE])
 includeProject(":datastore:datastore-preferences", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-core", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
+includeProject(":datastore:datastore-preferences-external-protobuf", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-proto", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-rxjava2", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-rxjava3", [BuildType.MAIN, BuildType.INFRAROGUE, BuildType.KMP])
diff --git a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
index 2e7601c..4d9ce75 100644
--- a/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
+++ b/stableaidl/stableaidl-gradle-plugin/src/test/java/com/android/build/gradle/internal/fixtures/FakeGradleProperty.kt
@@ -111,6 +111,10 @@
         throw NotImplementedError()
     }
 
+    override fun replace(transformation: Transformer<out Provider<out T>?, in Provider<T>>) {
+        throw NotImplementedError()
+    }
+
     @Deprecated("Deprecated in Java")
     override fun forUseAtConfigurationTime(): Provider<T> {
         throw NotImplementedError()
diff --git a/testutils/testutils-datastore/build.gradle b/testutils/testutils-datastore/build.gradle
index 2721d70..a2057f8 100644
--- a/testutils/testutils-datastore/build.gradle
+++ b/testutils/testutils-datastore/build.gradle
@@ -23,14 +23,12 @@
  */
 import androidx.build.LibraryType
 
-import static org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.*
-
 plugins {
     id("AndroidXPlugin")
 }
 
 androidXMultiplatform {
-    jvm {}
+    jvm()
     mac()
     linux()
     ios()
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
index 825a3ee..3ab85ce 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
@@ -44,6 +44,7 @@
  * than the curved column, either at the [CurvedAlignment.Angular.Start] of the layout,
  * at the [CurvedAlignment.Angular.End], or [CurvedAlignment.Angular.Center].
  * If unspecified or null, they can choose for themselves.
+ * @param contentBuilder Scope used to provide the content for this column.
  */
 public fun CurvedScope.curvedColumn(
     modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
index f4fa3ca..4f5daea 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
@@ -42,6 +42,7 @@
  * and if those needs to be reversed in a Rtl layout.
  * If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
  * See [CurvedDirection.Angular].
+ * @param contentBuilder Scope used to provide the content for this row.
  */
 public fun CurvedScope.curvedRow(
     modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index e344c9f..2087c5c3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -677,17 +677,19 @@
 @RestrictTo(LIBRARY_GROUP)
 public object SwipeableV2Defaults {
     /**
-     * The default animation used by [SwipeableV2State].
+     * The default animation that will be used to animate to a new state.
      */
     val AnimationSpec = SpringSpec<Float>()
 
     /**
-     * The default velocity threshold (1.8 dp per millisecond) used by [rememberSwipeableV2State].
+     * The default velocity threshold (in dp per second) that the end velocity has to
+     * exceed in order to animate to the next state.
      */
     val VelocityThreshold: Dp = 125.dp
 
     /**
-     * The default positional threshold (56 dp) used by [rememberSwipeableV2State]
+     * The default positional threshold used when calculating the target state while a swipe is in
+     * progress and when settling after the swipe ends.
      */
     val PositionalThreshold: Density.(totalDistance: Float) -> Float =
         fixedPositionalThreshold(56.dp)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
index 1fdbc11..114fd30 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
@@ -310,10 +310,9 @@
     // Canvas internally uses Spacer.drawBehind.
     // Using Spacer.drawWithCache to optimize the stroke allocations.
     Spacer(
+        // NB We must set the semantic role to Role.RadioButton in the parent Button,
+        // not here in the selection control - see b/330869742
         modifier = modifier
-            .semantics {
-                this.role = Role.RadioButton
-            }
             .maybeSelectable(
                 onClick, enabled, selected, interactionSource, ripple, width, height
             )
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
index 792bd00..3b0332a 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
@@ -211,6 +211,9 @@
                         indication = ripple,
                         interactionSource = interactionSource
                     )
+                    // For a toggleable button, the role could be Checkbox or Switch,
+                    // so we cannot set the semantics here. Instead,
+                    // we set them in the toggle control
                 } else {
                     Modifier.selectable(
                         enabled = enabled,
@@ -218,7 +221,12 @@
                         onClick = { onCheckedChange(true) },
                         indication = ripple,
                         interactionSource = interactionSource
-                    )
+                    ).semantics {
+                        // For a selectable button, the role is always RadioButton.
+                        // See also b/330869742 for issue with setting the RadioButton role
+                        // within the selection control.
+                        role = Role.RadioButton
+                    }
                 }
             )
             .padding(contentPadding),
@@ -375,6 +383,12 @@
                     indication = ripple,
                     interactionSource = checkedInteractionSource
                 )
+                .semantics {
+                    // For a selectable button, the role is always RadioButton.
+                    // See also b/330869742 for issue with setting the RadioButton role
+                    // within the selection control.
+                    role = Role.RadioButton
+                }
             }
 
         Box(
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
index 2fde27b..c3d34d8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
@@ -136,6 +136,28 @@
     }
 
     @Test
+    fun selectable_chip_has_role_radiobutton() {
+        rule.setContentWithTheme {
+            SelectableChip(
+                selected = true,
+                onClick = {},
+                enabled = false,
+                label = { Text("Label") },
+                selectionControl = { TestImage() },
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.Role,
+                    Role.RadioButton
+                )
+            )
+    }
+
+    @Test
     fun split_chip_has_clickaction_when_disabled() {
         rule.setContentWithTheme {
             SplitSelectableChip(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
index fe84508..c9c736c 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Card.kt
@@ -90,6 +90,7 @@
  * still happen internally.
  * @param role The type of user interface element. Accessibility services might use this
  * to describe the element or do customizations
+ * @param content Slot for composable body content displayed on the Card
  */
 @Composable
 public fun Card(
@@ -179,6 +180,7 @@
  * set.
  * @param timeColor The default color to use for time() slot unless explicitly set.
  * @param titleColor The default color to use for title() slot unless explicitly set.
+ * @param content Slot for composable body content displayed on the Card
  */
 @Composable
 public fun AppCard(
@@ -289,6 +291,7 @@
  * @param contentColor The default color to use for content() slot unless explicitly set.
  * @param titleColor The default color to use for title() slot unless explicitly set.
  * @param timeColor The default color to use for time() slot unless explicitly set.
+ * @param content Slot for composable body content displayed on the Card
  */
 @Composable
 public fun TitleCard(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
index d80d069..5fed098 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Chip.kt
@@ -172,6 +172,7 @@
  * still happen internally.
  * @param role The type of user interface element. Accessibility services might use this
  * to describe the element or do customizations
+ * @param content Slot for composable body content displayed on the Chip
  */
 @Composable
 public fun Chip(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
index a7c7a0c..0566bf5 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/ListHeader.kt
@@ -43,6 +43,7 @@
  * @param modifier The modifier for the list header
  * @param backgroundColor The background color to apply - typically Color.Transparent
  * @param contentColor The color to apply to content
+ * @param content Slot for displayed header text
  */
 @Composable
 public fun ListHeader(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
index ed866b9..8f7bdbf 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/MaterialTheme.kt
@@ -53,6 +53,7 @@
  * @param colors A complete definition of the Wear Material Color theme for this hierarchy
  * @param typography A set of text styles to be used as this hierarchy's typography system
  * @param shapes A set of shapes to be used by the components in this hierarchy
+ * @param content Slot for composable content displayed with this theme
  */
 @Composable
 public fun MaterialTheme(
diff --git a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
index e9a468b..fb7965a 100644
--- a/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
+++ b/wear/compose/compose-material/src/main/java/androidx/wear/compose/material/Scaffold.kt
@@ -51,6 +51,7 @@
  * page indicator is a pager with horizontally swipeable pages.
  * @param timeText time and potential application status message to display at the top middle of the
  * screen. Expected to be a TimeText component.
+ * @param content Slot for composable screen content
  */
 @Composable
 public fun Scaffold(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
index f5c3ef0..3997c14 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
@@ -82,6 +82,23 @@
     }
 
     @Test
+    fun radio_button_has_role_radiobutton() {
+        rule.setContentWithTheme {
+            RadioButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.Role,
+                    Role.RadioButton
+                )
+            )
+    }
+
+    @Test
     fun radio_button_samples_build() {
         rule.setContentWithTheme {
             RadioButton()
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index 600e737..a8ac0da 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -22,10 +22,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.captureToImage
@@ -67,25 +63,6 @@
     }
 
     @Test
-    fun radio_control_has_role_radiobutton() {
-        rule.setContentWithTheme {
-            with(SelectionControlScope(isEnabled = true, isSelected = true)) {
-                Radio(
-                    modifier = Modifier.testTag(TEST_TAG)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(TEST_TAG)
-            .assert(
-                SemanticsMatcher.expectValue(
-                    SemanticsProperties.Role,
-                    Role.RadioButton
-                )
-            )
-    }
-
-    @Test
     fun radio_control_is_correctly_enabled() {
         rule.setContentWithTheme {
             with(SelectionControlScope(isEnabled = true, isSelected = true)) {
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 2dc5958..3755e89 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
@@ -106,6 +106,7 @@
  * emitting [Interaction]s for this button. You can use this to change the button's appearance
  * or preview the button in different states. Note that if `null` is provided, interactions will
  * still happen internally.
+ * @param content Slot for composable body content displayed on the Button
  */
 @Composable
 fun Button(
@@ -176,6 +177,7 @@
  * emitting [Interaction]s for this button. You can use this to change the button's appearance
  * or preview the button in different states. Note that if `null` is provided, interactions will
  * still happen internally.
+ * @param content Slot for composable body content displayed on the Button
  */
 @Composable
 fun FilledTonalButton(
@@ -245,6 +247,7 @@
  * emitting [Interaction]s for this button. You can use this to change the button's appearance
  * or preview the button in different states. Note that if `null` is provided, interactions will
  * still happen internally.
+ * @param content Slot for composable body content displayed on the OutlinedButton
  */
 @Composable
 fun OutlinedButton(
@@ -314,6 +317,7 @@
  * emitting [Interaction]s for this button. You can use this to change the button's appearance
  * or preview the button in different states. Note that if `null` is provided, interactions will
  * still happen internally.
+ * @param content Slot for composable body content displayed on the ChildButton
  */
 @Composable
 fun ChildButton(
@@ -1058,6 +1062,8 @@
     /**
      * Creates a [BorderStroke], such as for an [OutlinedButton]
      *
+     * @param enabled Controls the color of the border based on the enabled/disabled state of the
+     * button
      * @param borderColor The color to use for the border for this outline when enabled
      * @param disabledBorderColor The color to use for the border for this outline when
      * disabled
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
index 5ed024f..9ef0bde 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/MaterialTheme.kt
@@ -50,6 +50,7 @@
  * @param colorScheme A complete definition of the Wear Material Color theme for this hierarchy
  * @param typography A set of text styles to be used as this hierarchy's typography system
  * @param shapes A set of shapes to be used by the components in this hierarchy
+ * @param content Slot for composable content displayed with this theme
  */
 @Composable
 fun MaterialTheme(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index 70dbab71..b4a6f53 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -144,7 +144,13 @@
                 indication = rippleOrFallbackImplementation(),
                 interactionSource = interactionSource
             )
-            .padding(contentPadding),
+            .padding(contentPadding)
+            .semantics {
+                // For a selectable button, the role is always RadioButton.
+                // See also b/330869742 for issue with setting the RadioButton role
+                // within the selection control.
+                role = Role.RadioButton
+            },
         verticalAlignment = Alignment.CenterVertically
     ) {
         if (icon != null) {
@@ -329,7 +335,13 @@
                 .width(SPLIT_WIDTH)
                 .wrapContentHeight(align = Alignment.CenterVertically)
                 .wrapContentWidth(align = Alignment.End)
-                .then(endPadding),
+                .then(endPadding)
+                .semantics {
+                    // For a selectable button, the role is always RadioButton.
+                    // See also b/330869742 for issue with setting the RadioButton role
+                    // within the selection control.
+                    role = Role.RadioButton
+                },
         ) {
             val scope = remember(enabled, selected) { SelectionControlScope(enabled, selected) }
             selectionControl(scope)
diff --git a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
index 7981cf2..5b295fc 100644
--- a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
+++ b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
@@ -34,7 +34,7 @@
  * note, the above list is not exhaustive. It previews the composables on a small round Wear device.
  *
  * @sample androidx.wear.compose.material.samples.TitleCardWithImagePreview
- * @see [Preview.fontScale]
+ * @see Preview.fontScale
  */
 @Preview(
     device = WearDevices.SMALL_ROUND,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 28d0392..d68e962 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
     defaultConfig {
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
-        versionCode 23
-        versionName "1.23"
+        versionCode 24
+        versionName "1.24"
     }
 
     buildTypes {
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -14,7 +14,7 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationParameters.Builder {
-    ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationSpec.Builder {
-    ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
-    method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
   }
 
   public static final class AnimationParameterBuilders.Repeatable.Builder {
-    ctor public AnimationParameterBuilders.Repeatable.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -14,7 +14,7 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationParameters.Builder {
-    ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationSpec.Builder {
-    ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
-    method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
   }
 
   public static final class AnimationParameterBuilders.Repeatable.Builder {
-    ctor public AnimationParameterBuilders.Repeatable.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 44eb9a7..a800b2a 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -140,6 +140,7 @@
                     AnimationParameterProto.AnimationSpec.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-2136602843);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets animation parameters including duration, easing and repeat delay. */
@@ -158,6 +159,7 @@
              * Sets the repeatable mode to be used for specifying repetition parameters for the
              * animation. If not set, animation won't be repeated.
              */
+            @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setRepeatable(@NonNull Repeatable repeatable) {
                 mImpl.setRepeatable(repeatable.toProto());
@@ -261,6 +263,7 @@
                     AnimationParameterProto.AnimationParameters.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1301308590);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -491,7 +494,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         AnimationParameterProto.CubicBezierEasing toProto() {
             return mImpl;
@@ -525,6 +527,7 @@
                     AnimationParameterProto.CubicBezierEasing.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(856403705);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -704,6 +707,7 @@
                     AnimationParameterProto.Repeatable.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(2110475048);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
index 36e1b0b..5a088b9 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
@@ -48,12 +48,14 @@
         }
 
         /** Sets the value to use as the value when true in a conditional expression. */
-        public @NonNull IfTrueScope<T, RawT> use(T valueWhenTrue) {
+        @NonNull
+        public IfTrueScope<T, RawT> use(T valueWhenTrue) {
             return new IfTrueScope<>(valueWhenTrue, conditionBuilder, rawTypeMapper);
         }
 
         /** Sets the value to use as the value when true in a conditional expression. */
-        public @NonNull IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
+        @NonNull
+        public IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
             return use(rawTypeMapper.apply(valueWhenTrue));
         }
     }
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 17d89ce..500985d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -708,7 +708,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -1255,7 +1255,7 @@
             /** Sets the value to start animating from. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableFixedInt32.Builder setFromValue(int fromValue) {
+            public Builder setFromValue(int fromValue) {
                 mImpl.setFromValue(fromValue);
                 mFingerprint.recordPropertyUpdate(1, fromValue);
                 return this;
@@ -1264,7 +1264,7 @@
             /** Sets the value to animate to. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableFixedInt32.Builder setToValue(int toValue) {
+            public Builder setToValue(int toValue) {
                 mImpl.setToValue(toValue);
                 mFingerprint.recordPropertyUpdate(2, toValue);
                 return this;
@@ -1398,7 +1398,7 @@
             /** Sets the value to watch, and animate when it changes. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableDynamicInt32.Builder setInput(@NonNull DynamicInt32 input) {
+            public Builder setInput(@NonNull DynamicInt32 input) {
                 mImpl.setInput(input.toDynamicInt32Proto());
                 mFingerprint.recordPropertyUpdate(
                         1, checkNotNull(input.getFingerprint()).aggregateValueAsInt());
@@ -2413,7 +2413,7 @@
 
             /** Returns whether digit grouping is used or not. */
             public boolean isGroupingUsed() {
-                return mInt32FormatOp.getGroupingUsed();
+                return mInt32FormatOp.isGroupingUsed();
             }
 
             /** Builder to create {@link IntFormatter} objects. */
@@ -2579,7 +2579,7 @@
          * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
          * with 1234 would yield "1,234".
          */
-        public boolean getGroupingUsed() {
+        public boolean isGroupingUsed() {
             return mImpl.getGroupingUsed();
         }
 
@@ -2638,13 +2638,13 @@
                     + ", minIntegerDigits="
                     + getMinIntegerDigits()
                     + ", groupingUsed="
-                    + getGroupingUsed()
+                    + isGroupingUsed()
                     + "}";
         }
 
         /** Builder for {@link Int32FormatOp}. */
         public static final class Builder implements DynamicString.Builder {
-            final DynamicProto.Int32FormatOp.Builder mImpl =
+            private final DynamicProto.Int32FormatOp.Builder mImpl =
                     DynamicProto.Int32FormatOp.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(196209833);
 
@@ -2792,7 +2792,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -3143,7 +3143,7 @@
          * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
          * with 1234.56 would yield "1,234.56".
          */
-        public boolean getGroupingUsed() {
+        public boolean isGroupingUsed() {
             return mImpl.getGroupingUsed();
         }
 
@@ -3206,7 +3206,7 @@
                     + ", minIntegerDigits="
                     + getMinIntegerDigits()
                     + ", groupingUsed="
-                    + getGroupingUsed()
+                    + isGroupingUsed()
                     + "}";
         }
 
@@ -3736,7 +3736,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -5012,7 +5012,7 @@
 
             /** Returns whether digit grouping is used or not. */
             public boolean isGroupingUsed() {
-                return mFloatFormatOp.getGroupingUsed();
+                return mFloatFormatOp.isGroupingUsed();
             }
 
             /** Builder to create {@link FloatFormatter} objects. */
@@ -5228,8 +5228,8 @@
         @NonNull
         public DynamicProto.DynamicBool toDynamicBoolProto(boolean withFingerprint) {
             if (withFingerprint) {
-                return DynamicProto.DynamicBool.newBuilder().
-                        setStateSource(mImpl)
+                return DynamicProto.DynamicBool.newBuilder()
+                        .setStateSource(mImpl)
                         .setFingerprint(checkNotNull(mFingerprint).toProto())
                         .build();
             }
@@ -5265,7 +5265,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -6144,7 +6144,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
index 69c88e3..c3b5c45 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
@@ -28,7 +28,6 @@
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicType;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedBool;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedColor;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedDuration;
@@ -51,7 +50,7 @@
 
     /** Interface defining a dynamic data value. */
     @RequiresSchemaVersion(major = 1, minor = 200)
-    public interface DynamicDataValue<T extends DynamicType> {
+    public interface DynamicDataValue<T extends DynamicBuilders.DynamicType> {
         /** Get the protocol buffer representation of this object. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -310,7 +309,7 @@
 
         /** Builder to create {@link DynamicDataValue} objects. */
         @RestrictTo(Scope.LIBRARY_GROUP)
-        interface Builder<T extends DynamicType> {
+        interface Builder<T extends DynamicBuilders.DynamicType> {
 
             /** Builds an instance with values accumulated in this Builder. */
             @NonNull
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index ced6685..e16e059 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -78,7 +78,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedInt32 toProto() {
             return mImpl;
@@ -144,6 +143,7 @@
             private final FixedProto.FixedInt32.Builder mImpl = FixedProto.FixedInt32.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(974881783);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -203,7 +203,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedString toProto() {
             return mImpl;
@@ -271,6 +270,7 @@
                     FixedProto.FixedString.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(1963352072);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -333,7 +333,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedFloat toProto() {
             return mImpl;
@@ -399,6 +398,7 @@
             private final FixedProto.FixedFloat.Builder mImpl = FixedProto.FixedFloat.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-144724541);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -461,7 +461,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedBool toProto() {
             return mImpl;
@@ -527,6 +526,7 @@
             private final FixedProto.FixedBool.Builder mImpl = FixedProto.FixedBool.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-665116398);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -587,7 +587,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedColor toProto() {
             return mImpl;
@@ -653,6 +652,7 @@
             private final FixedProto.FixedColor.Builder mImpl = FixedProto.FixedColor.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1895809356);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the color value, in ARGB format. */
@@ -733,7 +733,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedInstant toProto() {
             return mImpl;
@@ -778,6 +777,7 @@
                     FixedProto.FixedInstant.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1986552556);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -860,7 +860,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedDuration toProto() {
             return mImpl;
@@ -905,6 +904,7 @@
                     FixedProto.FixedDuration.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(9029504);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets duration in seconds. */
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
index 6d3cbd4..b36d18c 100644
--- a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -21,7 +21,6 @@
 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
 import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
-
 import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.GAP_END_ANGLE;
 import static androidx.wear.protolayout.material.ProgressIndicatorDefaults.GAP_START_ANGLE;
 
@@ -406,7 +405,7 @@
                 "التسمية الأولية",
                 "نص اختباري.",
                 "نص طويل جدًا لا يمكن احتواؤه في المربع الأصلي الخاص به، لذا يجب تغيير حجمه بشكل"
-                    + " صحيح قبل السطر الأخير");
+                        + " صحيح قبل السطر الأخير");
     }
 
     /**
@@ -415,7 +414,7 @@
      * as it should point on the same size independent image.
      */
     @NonNull
-    @SuppressWarnings("deprecation")    // TEXT_OVERFLOW_ELLIPSIZE_END
+    @SuppressWarnings("deprecation") // TEXT_OVERFLOW_ELLIPSIZE_END
     private static ImmutableMap<String, Layout> generateTextTestCasesForLanguage(
             @NonNull Context context,
             @NonNull DeviceParameters deviceParameters,
@@ -439,18 +438,9 @@
                         .setColor(argb(Color.YELLOW))
                         .setWeight(LayoutElementBuilders.FONT_WEIGHT_BOLD)
                         .setTypography(Typography.TYPOGRAPHY_BODY2)
-                        .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
                         .build());
         testCases.put(
-                "overflow_text_golden" + goldenSuffix,
-                new Text.Builder(context, longText)
-                        .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_START)
-                        .build());
-        testCases.put(
-                "overflow_text_center_golden" + goldenSuffix,
-                new Text.Builder(context, longText)
-                        .setMultilineAlignment(LayoutElementBuilders.TEXT_ALIGN_CENTER)
-                        .build());
+                "overflow_text_golden" + goldenSuffix, new Text.Builder(context, longText).build());
         testCases.put(
                 "overflow_ellipsize_maxlines_notreached" + goldenSuffix,
                 new Box.Builder()
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
new file mode 100644
index 0000000..d6a206c
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License = 0 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 = 0 software
+ * distributed under the License is distributed on an "AS IS" BASIS = 0
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND = 0 either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Shared constants. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Constants {
+
+    private Constants() {}
+
+    /** The reason why an update was requested. */
+    @IntDef({
+        UPDATE_REQUEST_REASON_UNKNOWN,
+        UPDATE_REQUEST_REASON_SYSUI_CAROUSEL,
+        UPDATE_REQUEST_REASON_FRESHNESS,
+        UPDATE_REQUEST_REASON_USER_INTERACTION,
+        UPDATE_REQUEST_REASON_UPDATE_REQUESTER,
+        UPDATE_REQUEST_REASON_CACHE_INVALIDATION,
+        UPDATE_REQUEST_REASON_RETRY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UpdateRequestReason {}
+
+    /** Unknown reason. */
+    public static final int UPDATE_REQUEST_REASON_UNKNOWN = 0;
+
+    /** Update triggered by SysUI Carousel. */
+    public static final int UPDATE_REQUEST_REASON_SYSUI_CAROUSEL = 1;
+
+    /** Update triggered by freshness. */
+    public static final int UPDATE_REQUEST_REASON_FRESHNESS = 2;
+
+    /** Update triggered by user interaction (e.g. clicking on the tile). */
+    public static final int UPDATE_REQUEST_REASON_USER_INTERACTION = 3;
+
+    /** Update triggered using update requester. */
+    public static final int UPDATE_REQUEST_REASON_UPDATE_REQUESTER = 4;
+
+    /** Update triggered due to clearing the cache. */
+    public static final int UPDATE_REQUEST_REASON_CACHE_INVALIDATION = 5;
+
+    /** Update triggered by retry policy. */
+    public static final int UPDATE_REQUEST_REASON_RETRY = 6;
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
new file mode 100644
index 0000000..f292c98
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+/** A No-Op implementation of {@link ProviderStatsLogger}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class NoOpProviderStatsLogger implements ProviderStatsLogger {
+    private static final String TAG = "NoOpProviderStatsLogger";
+
+    /** Creates an instance of {@link NoOpProviderStatsLogger}. */
+    public NoOpProviderStatsLogger(@NonNull String reason) {
+        Log.i(TAG, "Instance used because " + reason);
+    }
+
+    /** No-op method. */
+    @Override
+    public void logLayoutSchemaVersion(int major, int minor) {}
+
+    /** No-op method. */
+    @Override
+    public void logStateStructure(@NonNull State state, boolean isInitialState) {}
+
+    /** No-op method. */
+    @Override
+    public void logIgnoredFailure(int failure) {}
+
+    /** No-op method. */
+    @Override
+    public void logInflationFailed(@InflationFailureReason int failureReason) {}
+
+    /** No-op method. */
+    @Override
+    @NonNull
+    public InflaterStatsLogger createInflaterStatsLogger() {
+        return new NoOpInflaterStatsLogger();
+    }
+
+    /** No-op method. */
+    @Override
+    public void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger) {}
+
+    /** No-op method. */
+    @Override
+    public void logTileRequestReason(@UpdateRequestReason int updateRequestReason) {}
+
+    /** A No-Op implementation of {@link InflaterStatsLogger}. */
+    public static class NoOpInflaterStatsLogger implements InflaterStatsLogger {
+
+        private NoOpInflaterStatsLogger() {}
+
+        @Override
+        public void logMutationChangedNodes(int changedNodesCount) {}
+
+        @Override
+        public void logTotalNodeCount(int totalNodesCount) {}
+
+        /** No-op method. */
+        @Override
+        public void logDrawableUsage(@NonNull Drawable drawable) {}
+
+        /** No-op method. */
+        @Override
+        public void logIgnoredFailure(@IgnoredFailure int failure) {}
+
+        /** No-op method. */
+        @Override
+        public void logInflationFailed(@InflationFailureReason int failureReason) {}
+    }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
index 65f4fa2..6860d79 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProtoLayoutDiffer.java
@@ -35,7 +35,7 @@
 import java.util.Collections;
 import java.util.List;
 
-/** Utility to diff 2 proto layouts in order to be able to partially update the display. */
+/** Utility to diff two proto layouts in order to be able to partially update the display. */
 @RestrictTo(Scope.LIBRARY_GROUP)
 public class ProtoLayoutDiffer {
     /** Prefix for all node IDs generated by this differ. */
@@ -63,7 +63,9 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static final int FIRST_CHILD_INDEX = 0;
 
-    private enum NodeChangeType {
+    /** Type of the change applied to the node. */
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    public enum NodeChangeType {
         NO_CHANGE,
         CHANGE_IN_SELF_ONLY,
         CHANGE_IN_SELF_AND_ALL_CHILDREN,
@@ -108,8 +110,8 @@
         }
 
         @NonNull
-        TreeNodeWithChange withChange(boolean isSelfOnlyChange) {
-            return new TreeNodeWithChange(this, isSelfOnlyChange);
+        TreeNodeWithChange withChange(@NonNull NodeChangeType nodeChangeType) {
+            return new TreeNodeWithChange(this, nodeChangeType);
         }
     }
 
@@ -117,11 +119,11 @@
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class TreeNodeWithChange {
         @NonNull private final TreeNode mTreeNode;
-        private final boolean mIsSelfOnlyChange;
+        @NonNull private final NodeChangeType mNodeChangeType;
 
-        TreeNodeWithChange(@NonNull TreeNode treeNode, boolean isSelfOnlyChange) {
+        TreeNodeWithChange(@NonNull TreeNode treeNode, @NonNull NodeChangeType nodeChangeType) {
             this.mTreeNode = treeNode;
-            this.mIsSelfOnlyChange = isSelfOnlyChange;
+            this.mNodeChangeType = nodeChangeType;
         }
 
         /**
@@ -167,7 +169,20 @@
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         public boolean isSelfOnlyChange() {
-            return mIsSelfOnlyChange;
+            switch (mNodeChangeType) {
+                case CHANGE_IN_SELF_ONLY:
+                case CHANGE_IN_SELF_AND_SOME_CHILDREN:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        /** Returns the {@link NodeChangeType} of this {@link TreeNodeWithChange}. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public NodeChangeType getChangeType() {
+            return mNodeChangeType;
         }
     }
 
@@ -295,15 +310,14 @@
             @NonNull TreeNode node,
             @NonNull List<TreeNodeWithChange> changedNodes)
             throws InconsistentFingerprintException {
-        switch (getChangeType(prevNodeFingerprint, node.mFingerprint)) {
+        NodeChangeType changeType = getChangeType(prevNodeFingerprint, node.mFingerprint);
+        switch (changeType) {
             case CHANGE_IN_SELF_ONLY:
-                changedNodes.add(node.withChange(/* isSelfOnlyChange= */ true));
-                break;
             case CHANGE_IN_SELF_AND_ALL_CHILDREN:
-                changedNodes.add(node.withChange(/* isSelfOnlyChange= */ false));
+                changedNodes.add(node.withChange(changeType));
                 break;
             case CHANGE_IN_SELF_AND_SOME_CHILDREN:
-                changedNodes.add(node.withChange(/* isSelfOnlyChange= */ true));
+                changedNodes.add(node.withChange(changeType));
                 addChangedChildNodes(prevNodeFingerprint, node, changedNodes);
                 break;
             case CHANGE_IN_CHILDREN:
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
new file mode 100644
index 0000000..96a6fb0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Logger used for collecting metrics. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ProviderStatsLogger {
+
+    /** Failures that doesn't cause the inflation to fail. */
+    @IntDef({
+        IGNORED_FAILURE_UNKNOWN,
+        IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION,
+        IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED,
+        IGNORED_FAILURE_DIFFING_FAILURE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IgnoredFailure {}
+
+    /** Unknown failure. */
+    int IGNORED_FAILURE_UNKNOWN = 0;
+
+    /** Failure applying the diff mutation. */
+    int IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION = 1;
+
+    /** Failure caused by exceeding animation quota. */
+    int IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED = 2;
+
+    /** Failure diffing the layout. */
+    int IGNORED_FAILURE_DIFFING_FAILURE = 3;
+
+    /** Failures that causes the inflation to fail. */
+    @IntDef({
+        INFLATION_FAILURE_REASON_UNKNOWN,
+        INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED,
+        INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface InflationFailureReason {}
+
+    /** Unknown failure. */
+    int INFLATION_FAILURE_REASON_UNKNOWN = 0;
+
+    /** Failure caused by exceeding maximum layout depth. */
+    int INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED = 1;
+
+    /** Failure caused by exceeding maximum expression node count. */
+    int INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED = 2;
+
+    /** Log the schema version of the received layout. */
+    void logLayoutSchemaVersion(int major, int minor);
+
+    /** Log Protolayout state structure. */
+    void logStateStructure(@NonNull State state, boolean isInitialState);
+
+    /** Log the occurrence of an ignored failure. */
+    @UiThread
+    void logIgnoredFailure(@IgnoredFailure int failure);
+
+    /** Log the reason for inflation failure. */
+    @UiThread
+    void logInflationFailed(@InflationFailureReason int failureReason);
+
+    /**
+     * Creates an {@link InflaterStatsLogger} and marks the start of inflation. The atoms will be
+     * logged to statsd only when {@link #logInflationFinished} is called.
+     */
+    @UiThread
+    @NonNull
+    InflaterStatsLogger createInflaterStatsLogger();
+
+    /** Makes the end of inflation and log the inflation results. */
+    @UiThread
+    void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger);
+
+    /** Log tile request reason. */
+    void logTileRequestReason(@UpdateRequestReason int updateRequestReason);
+
+    /** Logger used for logging inflation stats. */
+    interface InflaterStatsLogger {
+        /** log the mutation changed nodes count for the ongoing inflation. */
+        @UiThread
+        void logMutationChangedNodes(int changedNodesCount);
+
+        /** Log the total nodes count for the ongoing inflation. */
+        @UiThread
+        void logTotalNodeCount(int totalNodesCount);
+
+        /**
+         * Log the usage of a drawable. This method should be called between {@link
+         * #createInflaterStatsLogger()} and {@link #logInflationFinished(InflaterStatsLogger)}.
+         */
+        @UiThread
+        void logDrawableUsage(@NonNull Drawable drawable);
+
+        /**
+         * Log the occurrence of an ignored failure. The usage of this method is not restricted to
+         * inflation start or end.
+         */
+        @UiThread
+        void logIgnoredFailure(@IgnoredFailure int failure);
+
+        /**
+         * Log the reason for inflation failure. This will make any future call {@link
+         * #logInflationFinished(InflaterStatsLogger)} a Noop.
+         */
+        @UiThread
+        void logInflationFailed(@InflationFailureReason int failureReason);
+    }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
new file mode 100644
index 0000000..8424728
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+
+/** Artifacts resulted from the layout rendering. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface RenderingArtifact {
+
+    /** Creates a {@link RenderingArtifact} instance. */
+    @NonNull
+    static RenderingArtifact create(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+        return new SuccessfulRenderingArtifact(inflaterStatsLogger);
+    }
+
+    /** Creates a {@link RenderingArtifact} instance for a skipped inflation. */
+    @NonNull
+    static RenderingArtifact skipped() {
+        return new SkippedRenderingArtifact();
+    }
+
+    /** Creates a {@link RenderingArtifact} instance for a failed inflation. */
+    @NonNull
+    static RenderingArtifact failed() {
+        return new FailedRenderingArtifact();
+    }
+
+    /** Artifacts resulted from a successful layout rendering. */
+    class SuccessfulRenderingArtifact implements RenderingArtifact {
+        @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
+
+        private SuccessfulRenderingArtifact(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+            mInflaterStatsLogger = inflaterStatsLogger;
+        }
+
+        /**
+         * Returns the {@link ProviderStatsLogger.InflaterStatsLogger} used log inflation stats.
+         * This will return {@code null} if the inflation was skipped or failed.
+         */
+        @NonNull
+        public InflaterStatsLogger getInflaterStatsLogger() {
+            return mInflaterStatsLogger;
+        }
+    }
+
+    /** Artifacts resulted from a skipped layout rendering. */
+    class SkippedRenderingArtifact implements RenderingArtifact {}
+
+    /** Artifacts resulted from a failed layout rendering. */
+    class FailedRenderingArtifact implements RenderingArtifact {}
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
index 18b5fa9..c2ad693 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTree.java
@@ -25,6 +25,8 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
 import androidx.annotation.VisibleForTesting;
 import androidx.wear.protolayout.renderer.dynamicdata.PositionIdTree.TreeNode;
 
@@ -45,10 +47,11 @@
  *
  * <p>This class is not thread-safe.
  */
-final class PositionIdTree<T extends TreeNode> {
+@RestrictTo(Scope.LIBRARY_GROUP)
+public final class PositionIdTree<T extends TreeNode> {
 
     /** Interface for nodes stored in this tree. */
-    interface TreeNode {
+    public interface TreeNode {
         /** Will be called after a node is removed from the tree. */
         void destroy();
     }
@@ -61,7 +64,7 @@
     }
 
     /** Removes all of the nodes in the tree and calls their {@link TreeNode#destroy()}. */
-    void clear() {
+    public void clear() {
         mPosIdToTreeNode.values().forEach(TreeNode::destroy);
         mPosIdToTreeNode.clear();
     }
@@ -71,7 +74,7 @@
      * {@link TreeNode#destroy()} on all of the removed node. Note that the {@code posId} node won't
      * be removed.
      */
-    void removeChildNodesFor(@NonNull String posId) {
+    public void removeChildNodesFor(@NonNull String posId) {
         removeChildNodesFor(posId, /* removeRoot= */ false);
     }
 
@@ -92,7 +95,7 @@
      * Adds the {@code newNode} to the tree. If the tree already contains a node at that position,
      * the old node will be removed and will be destroyed.
      */
-    void addOrReplace(@NonNull String posId, @NonNull T newNode) {
+    public void addOrReplace(@NonNull String posId, @NonNull T newNode) {
         T oldNode = mPosIdToTreeNode.put(posId, newNode);
         if (oldNode != null) {
             oldNode.destroy();
@@ -107,16 +110,35 @@
 
     /** Returns the node with {@code posId} or null if it doesn't exist. */
     @Nullable
-    T get(String posId) {
+    public T get(@NonNull String posId) {
         return mPosIdToTreeNode.get(posId);
     }
 
     /**
-     * Returns all of the ancestors of the node with {@code posId} matching the {@code predicate}.
+     * Returns all of the ancestors of the node {@code posId} and value matching the {@code
+     * predicate}.
      */
     @NonNull
-    List<T> findAncestorsFor(@NonNull String posId, @NonNull Predicate<? super T> predicate) {
+    public List<T> findAncestorsFor(
+            @NonNull String posId, @NonNull Predicate<? super T> predicate) {
         List<T> result = new ArrayList<>();
+        for (String id : findAncestorsNodesFor(posId, predicate)) {
+            T value = mPosIdToTreeNode.get(id);
+            if (value != null) {
+                result.add(value);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns all of the ancestors' posIds of the node {@code posId} with value matching the {@code
+     * predicate}.
+     */
+    @NonNull
+    public List<String> findAncestorsNodesFor(
+            @NonNull String posId, @NonNull Predicate<? super T> predicate) {
+        List<String> result = new ArrayList<>();
         while (true) {
             String parentPosId = getParentNodePosId(posId);
             if (parentPosId == null) {
@@ -124,7 +146,7 @@
             }
             T value = mPosIdToTreeNode.get(parentPosId);
             if (value != null && predicate.test(value)) {
-                result.add(value);
+                result.add(parentPosId);
             }
             posId = parentPosId;
         }
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 08f0228..8f18ca8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -20,6 +20,13 @@
 import static android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY;
 
 import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED;
+
+import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -41,6 +48,7 @@
 import androidx.wear.protolayout.expression.PlatformDataKey;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
 import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
+import androidx.wear.protolayout.expression.pipeline.QuotaManager;
 import androidx.wear.protolayout.expression.pipeline.StateStore;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase;
@@ -52,7 +60,11 @@
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
 import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
 import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -114,6 +126,7 @@
     @NonNull private final ListeningExecutorService mUiExecutorService;
     @NonNull private final ListeningExecutorService mBgExecutorService;
     @NonNull private final String mClickableIdExtra;
+    @NonNull private final ProviderStatsLogger mProviderStatsLogger;
     @Nullable private final LoggingUtils mLoggingUtils;
 
     @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@@ -219,10 +232,11 @@
          */
         @UiThread
         @NonNull
-        ListenableFuture<Void> postInflate(
+        ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching);
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger);
     }
 
     /** Result of a {@link #renderOrComputeMutations} call when no changes are required. */
@@ -234,11 +248,12 @@
 
         @NonNull
         @Override
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
-            return Futures.immediateVoidFuture();
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
+            return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
         }
     }
 
@@ -251,11 +266,12 @@
 
         @NonNull
         @Override
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
-            return Futures.immediateVoidFuture();
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
+            return immediateFuture(RenderingArtifact.failed());
         }
     }
 
@@ -278,10 +294,11 @@
         @NonNull
         @Override
         @UiThread
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
             InflateResult inflateResult =
                     checkNotNull(
                             mNewInflateParentData.mInflateResult,
@@ -292,7 +309,7 @@
             attachParent.addView(
                     inflateResult.inflateParent, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
             inflateResult.updateDynamicDataPipeline(isReattaching);
-            return Futures.immediateVoidFuture();
+            return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
         }
     }
 
@@ -318,10 +335,11 @@
         @NonNull
         @Override
         @UiThread
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
             return mInflater.applyMutation(checkNotNull(prevInflateParent), mMutation);
         }
     }
@@ -345,6 +363,7 @@
         @NonNull private final String mClickableIdExtra;
 
         @Nullable private final LoggingUtils mLoggingUtils;
+        @NonNull private final ProviderStatsLogger mProviderStatsLogger;
         private final boolean mAnimationEnabled;
         private final int mRunningAnimationsLimit;
 
@@ -366,6 +385,7 @@
                 @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
                 @NonNull String clickableIdExtra,
                 @Nullable LoggingUtils loggingUtils,
+                @NonNull ProviderStatsLogger providerStatsLogger,
                 boolean animationEnabled,
                 int runningAnimationsLimit,
                 boolean updatesEnabled,
@@ -384,6 +404,7 @@
             this.mExtensionViewProvider = extensionViewProvider;
             this.mClickableIdExtra = clickableIdExtra;
             this.mLoggingUtils = loggingUtils;
+            this.mProviderStatsLogger = providerStatsLogger;
             this.mAnimationEnabled = animationEnabled;
             this.mRunningAnimationsLimit = runningAnimationsLimit;
             this.mUpdatesEnabled = updatesEnabled;
@@ -468,6 +489,13 @@
             return mLoggingUtils;
         }
 
+        /** Returns the provider stats logger used for telemetry. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public ProviderStatsLogger getProviderStatsLogger() {
+            return mProviderStatsLogger;
+        }
+
         /** Returns whether animations are enabled. */
         @RestrictTo(Scope.LIBRARY)
         public boolean getAnimationEnabled() {
@@ -529,6 +557,7 @@
             @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider;
             @NonNull private final String mClickableIdExtra;
             @Nullable private LoggingUtils mLoggingUtils;
+            @Nullable private ProviderStatsLogger mProviderStatsLogger;
             private boolean mAnimationEnabled = true;
             private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS;
 
@@ -632,6 +661,15 @@
                 return this;
             }
 
+            /** Sets the provider stats logger used for telemetry. */
+            @RestrictTo(Scope.LIBRARY_GROUP)
+            @NonNull
+            public Builder setProviderStatsLogger(
+                    @NonNull ProviderStatsLogger providerStatsLogger) {
+                this.mProviderStatsLogger = providerStatsLogger;
+                return this;
+            }
+
             /**
              * Sets whether animation are enabled. If disabled, none of the animation will be
              * played.
@@ -715,6 +753,12 @@
                 if (mRendererResources == null) {
                     this.mRendererResources = mUiContext.getResources();
                 }
+
+                if (mProviderStatsLogger == null) {
+                    mProviderStatsLogger =
+                            new NoOpProviderStatsLogger(
+                                    "ProviderStatsLogger not provided to " + TAG);
+                }
                 return new Config(
                         mUiContext,
                         mRendererResources,
@@ -728,6 +772,7 @@
                         mExtensionViewProvider,
                         mClickableIdExtra,
                         mLoggingUtils,
+                        mProviderStatsLogger,
                         mAnimationEnabled,
                         mRunningAnimationsLimit,
                         mUpdatesEnabled,
@@ -754,24 +799,51 @@
         this.mWasFullyVisibleBefore = false;
         this.mAllowLayoutChangingBindsWithoutDefault =
                 config.getAllowLayoutChangingBindsWithoutDefault();
+        this.mProviderStatsLogger = config.getProviderStatsLogger();
 
         StateStore stateStore = config.getStateStore();
-        if (stateStore != null) {
-            mDataPipeline =
-                    config.getAnimationEnabled()
-                            ? new ProtoLayoutDynamicDataPipeline(
-                                    config.getPlatformDataProviders(),
-                                    stateStore,
-                                    new FixedQuotaManagerImpl(
-                                            config.getRunningAnimationsLimit(), "animations"),
-                                    new FixedQuotaManagerImpl(
-                                            DYNAMIC_NODES_MAX_COUNT, "dynamic nodes"))
-                            : new ProtoLayoutDynamicDataPipeline(
-                                    config.getPlatformDataProviders(), stateStore);
-            mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
-        } else {
+        if (stateStore == null) {
             mDataPipeline = null;
+            return;
         }
+
+        if (config.getAnimationEnabled()) {
+            QuotaManager nodeQuotaManager =
+                    new FixedQuotaManagerImpl(DYNAMIC_NODES_MAX_COUNT, "dynamic nodes") {
+                        @Override
+                        public boolean tryAcquireQuota(int quota) {
+                            boolean success = super.tryAcquireQuota(quota);
+                            if (!success) {
+                                mProviderStatsLogger.logInflationFailed(
+                                        INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED);
+                            }
+                            return success;
+                        }
+                    };
+            mDataPipeline =
+                    new ProtoLayoutDynamicDataPipeline(
+                            config.getPlatformDataProviders(),
+                            stateStore,
+                            new FixedQuotaManagerImpl(
+                                    config.getRunningAnimationsLimit(), "animations") {
+                                @Override
+                                public boolean tryAcquireQuota(int quota) {
+                                    boolean success = super.tryAcquireQuota(quota);
+                                    if (!success) {
+                                        mProviderStatsLogger.logIgnoredFailure(
+                                                IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED);
+                                    }
+                                    return success;
+                                }
+                            },
+                            nodeQuotaManager);
+        } else {
+            mDataPipeline =
+                    new ProtoLayoutDynamicDataPipeline(
+                            config.getPlatformDataProviders(), stateStore);
+        }
+
+        mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
     }
 
     @WorkerThread
@@ -780,7 +852,8 @@
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @Nullable RenderedMetadata prevRenderedMetadata,
-            @NonNull ViewProperties parentViewProp) {
+            @NonNull ViewProperties parentViewProp,
+            @NonNull InflaterStatsLogger inflaterStatsLogger) {
         ResourceResolvers resolvers =
                 mResourceResolversProvider.getResourceResolvers(
                         mUiContext, resources, mUiExecutorService, mAnimationEnabled);
@@ -797,10 +870,10 @@
 
         if (sameFingerprint) {
             if (mPrevLayoutAlreadyFailingDepthCheck) {
-                throwExceptionForLayoutDepthCheckFailure();
+                handleLayoutDepthCheckFailure(inflaterStatsLogger);
             }
         } else {
-            checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH);
+            checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH, inflaterStatsLogger);
         }
 
         mPrevLayoutAlreadyFailingDepthCheck = false;
@@ -815,6 +888,7 @@
                         .setClickableIdExtra(mClickableIdExtra)
                         .setAllowLayoutChangingBindsWithoutDefault(
                                 mAllowLayoutChangingBindsWithoutDefault)
+                        .setInflaterStatsLogger(inflaterStatsLogger)
                         .setApplyFontVariantBodyAsDefault(true);
         if (mDataPipeline != null) {
             inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline);
@@ -886,6 +960,18 @@
         return new InflateParentData(result);
     }
 
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public ListenableFuture<RenderingArtifact> renderLayoutAndAttach(
+            @NonNull Layout layout,
+            @NonNull ResourceProto.Resources resources,
+            @NonNull ViewGroup attachParent) {
+
+        return renderAndAttach(
+                layout, resources, attachParent, mProviderStatsLogger.createInflaterStatsLogger());
+    }
+
     /**
      * Render the layout for this layout and attach this layout instance to a {@code attachParent}
      * container. Note that this method may clear all of {@code attachParent}'s children before
@@ -911,6 +997,47 @@
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @NonNull ViewGroup attachParent) {
+        SettableFuture<Void> result = SettableFuture.create();
+        ListenableFuture<RenderingArtifact> future =
+                renderLayoutAndAttach(layout, resources, attachParent);
+        if (future.isDone()) {
+            if (future.isCancelled()) {
+                return immediateCancelledFuture();
+            }
+            return immediateFuture(null);
+        } else {
+            future.addListener(
+                    () -> {
+                        if (future.isCancelled()) {
+                            result.cancel(/* mayInterruptIfRunning= */ false);
+                        } else {
+                            try {
+                                RenderingArtifact ignored = future.get();
+                                result.set(null);
+                            } catch (ExecutionException
+                                    | InterruptedException
+                                    | CancellationException e) {
+                                Log.e(TAG, "Failed to render layout", e);
+                                result.setException(e);
+                            }
+                        }
+                    },
+                    mUiExecutorService);
+        }
+        return result;
+    }
+
+    @UiThread
+    @SuppressWarnings({
+        "ReferenceEquality",
+        "ExecutorTaskName",
+    }) // layout == prevLayout is intentional (and enough in this case)
+    @NonNull
+    private ListenableFuture<RenderingArtifact> renderAndAttach(
+            @NonNull Layout layout,
+            @NonNull ResourceProto.Resources resources,
+            @NonNull ViewGroup attachParent,
+            @NonNull InflaterStatsLogger inflaterStatsLogger) {
         if (mLoggingUtils != null && mLoggingUtils.canLogD(TAG)) {
             mLoggingUtils.logD(TAG, "Layout received in #renderAndAttach:\n %s", layout.toString());
             mLoggingUtils.logD(
@@ -930,7 +1057,7 @@
 
         if (layout == mPrevLayout && mInflateParent != null) {
             // Nothing to do.
-            return Futures.immediateVoidFuture();
+            return Futures.immediateFuture(RenderingArtifact.skipped());
         }
 
         boolean isReattaching = false;
@@ -1000,10 +1127,11 @@
                                             layout,
                                             resources,
                                             prevRenderedMetadata,
-                                            parentViewProp));
+                                            parentViewProp,
+                                            inflaterStatsLogger));
             mCanReattachWithoutRendering = false;
         }
-        SettableFuture<Void> result = SettableFuture.create();
+        SettableFuture<RenderingArtifact> result = SettableFuture.create();
         if (!checkNotNull(mRenderFuture).isDone()) {
             ListenableFuture<RenderResult> rendererFuture = mRenderFuture;
             mRenderFuture.addListener(
@@ -1023,7 +1151,8 @@
                                                 checkNotNull(rendererFuture).get(),
                                                 /* isReattaching= */ false,
                                                 layout,
-                                                resources));
+                                                resources,
+                                                inflaterStatsLogger));
                             } catch (ExecutionException
                                     | InterruptedException
                                     | CancellationException e) {
@@ -1048,7 +1177,8 @@
                                 mRenderFuture.get(),
                                 isReattaching,
                                 layout,
-                                resources));
+                                resources,
+                                inflaterStatsLogger));
             } catch (ExecutionException | InterruptedException | CancellationException e) {
                 Log.e(TAG, "Failed to render layout", e);
                 result.setException(e);
@@ -1064,6 +1194,12 @@
      */
     public void invalidateCache() {
         mPrevResourcesVersion = null;
+        // Cancel any ongoing rendering which might have a reference to older app resources.
+        if (mRenderFuture != null && !mRenderFuture.isDone()) {
+            mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+            mRenderFuture = null;
+            Log.w(TAG, "Cancelled ongoing rendering due to cache invalidation.");
+        }
     }
 
     @Nullable
@@ -1080,13 +1216,14 @@
     @UiThread
     @SuppressWarnings("ExecutorTaskName")
     @NonNull
-    private ListenableFuture<Void> postInflate(
+    private ListenableFuture<RenderingArtifact> postInflate(
             @NonNull ViewGroup attachParent,
             @Nullable ViewGroup prevInflateParent,
             @NonNull RenderResult renderResult,
             boolean isReattaching,
             @NonNull Layout layout,
-            @NonNull ResourceProto.Resources resources) {
+            @NonNull ResourceProto.Resources resources,
+            InflaterStatsLogger inflaterStatsLogger) {
         mCanReattachWithoutRendering = renderResult.canReattachWithoutRendering();
 
         if (renderResult instanceof InflatedIntoNewParentRenderResult) {
@@ -1101,9 +1238,10 @@
                             .inflateParent;
         }
 
-        ListenableFuture<Void> postInflateFuture =
-                renderResult.postInflate(attachParent, prevInflateParent, isReattaching);
-        SettableFuture<Void> result = SettableFuture.create();
+        ListenableFuture<RenderingArtifact> postInflateFuture =
+                renderResult.postInflate(
+                        attachParent, prevInflateParent, isReattaching, inflaterStatsLogger);
+        SettableFuture<RenderingArtifact> result = SettableFuture.create();
         if (!postInflateFuture.isDone()) {
             postInflateFuture.addListener(
                     () -> {
@@ -1114,20 +1252,24 @@
                                 | CancellationException e) {
                             result.setFuture(
                                     handlePostInflateFailure(
-                                            e, layout, resources, prevInflateParent, attachParent));
+                                            e,
+                                            layout,
+                                            resources,
+                                            prevInflateParent,
+                                            attachParent,
+                                            inflaterStatsLogger));
                         }
                     },
                     mUiExecutorService);
         } else {
             try {
-                postInflateFuture.get();
-                return Futures.immediateVoidFuture();
+                return immediateFuture(postInflateFuture.get());
             } catch (ExecutionException
                     | InterruptedException
                     | CancellationException
                     | ViewMutationException e) {
                 return handlePostInflateFailure(
-                        e, layout, resources, prevInflateParent, attachParent);
+                        e, layout, resources, prevInflateParent, attachParent, inflaterStatsLogger);
             }
         }
         return result;
@@ -1136,22 +1278,24 @@
     @UiThread
     @SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional
     @NonNull
-    private ListenableFuture<Void> handlePostInflateFailure(
+    private ListenableFuture<RenderingArtifact> handlePostInflateFailure(
             @NonNull Throwable error,
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @Nullable ViewGroup prevInflateParent,
-            @NonNull ViewGroup parent) {
+            @NonNull ViewGroup parent,
+            InflaterStatsLogger inflaterStatsLogger) {
         // If a RuntimeError is thrown, it'll be wrapped in an UncheckedExecutionException
         Throwable e = error.getCause();
         if (e instanceof ViewMutationException) {
+            inflaterStatsLogger.logIgnoredFailure(IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION);
             Log.w(TAG, "applyMutation failed." + e.getMessage());
             if (mPrevLayout == layout && parent == mAttachParent) {
                 Log.w(TAG, "Retrying full inflation.");
                 // Clear rendering metadata and prevLayout to force a full reinflation.
                 ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
                 mPrevLayout = null;
-                return renderAndAttach(layout, resources, parent);
+                return renderAndAttach(layout, resources, parent, inflaterStatsLogger);
             }
         } else {
             Log.e(TAG, "postInflate failed.", error);
@@ -1176,6 +1320,7 @@
     private void detachInternal() {
         if (mRenderFuture != null && !mRenderFuture.isDone()) {
             mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+            mRenderFuture = null;
         }
         setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE);
 
@@ -1227,9 +1372,12 @@
     }
 
     /** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */
-    private void checkLayoutDepth(LayoutElement layoutElement, int allowedDepth) {
+    private void checkLayoutDepth(
+            LayoutElement layoutElement,
+            int allowedDepth,
+            InflaterStatsLogger inflaterStatsLogger) {
         if (allowedDepth <= 0) {
-            throwExceptionForLayoutDepthCheckFailure();
+            handleLayoutDepthCheckFailure(inflaterStatsLogger);
         }
         List<LayoutElement> children = ImmutableList.of();
         switch (layoutElement.getInnerCase()) {
@@ -1245,28 +1393,32 @@
             case ARC:
                 List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList();
                 if (!arcElements.isEmpty() && allowedDepth == 1) {
-                    throwExceptionForLayoutDepthCheckFailure();
+                    handleLayoutDepthCheckFailure(inflaterStatsLogger);
                 }
                 for (ArcLayoutElement element : arcElements) {
                     if (element.getInnerCase() == InnerCase.ADAPTER) {
-                        checkLayoutDepth(element.getAdapter().getContent(), allowedDepth - 1);
+                        checkLayoutDepth(
+                                element.getAdapter().getContent(),
+                                allowedDepth - 1,
+                                inflaterStatsLogger);
                     }
                 }
                 break;
             case SPANNABLE:
                 if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) {
-                    throwExceptionForLayoutDepthCheckFailure();
+                    handleLayoutDepthCheckFailure(inflaterStatsLogger);
                 }
                 break;
             default:
                 // Other LayoutElements have depth of one.
         }
         for (LayoutElement child : children) {
-            checkLayoutDepth(child, allowedDepth - 1);
+            checkLayoutDepth(child, allowedDepth - 1, inflaterStatsLogger);
         }
     }
 
-    private void throwExceptionForLayoutDepthCheckFailure() {
+    private void handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger) {
+        inflaterStatsLogger.logInflationFailed(INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED);
         mPrevLayoutAlreadyFailingDepthCheck = true;
         throw new IllegalStateException(
                 "Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java
deleted file mode 100644
index 40db96c..0000000
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpan.java
+++ /dev/null
@@ -1,180 +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.wear.protolayout.renderer.inflater;
-
-import static java.lang.Math.abs;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.Layout;
-import android.text.Layout.Alignment;
-import android.text.StaticLayout;
-import android.text.style.LeadingMarginSpan;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * Helper class fixing the indentation for the last broken line by translating the canvas in the
- * opposite direction.
- *
- * <p>Applying letter spacing, center alignment and ellipsis to a text causes incorrect indentation
- * of the truncated line. For example, the last line is indented in a way where the start of the
- * line is outside of the boundaries of text.
- *
- * <p>It should be applied to a text only when those three attributes are set.
- */
-// Branched from androidx.compose.ui.text.android.style.IndentationFixSpan
-class IndentationFixSpan implements LeadingMarginSpan {
-    @VisibleForTesting static final String ELLIPSIS_CHAR = "…";
-    @Nullable private Layout mOverrideLayoutForMeasuring = null;
-
-    @Override
-    public int getLeadingMargin(boolean first) {
-        return 0;
-    }
-
-    /**
-     * Creates an instance of {@link IndentationFixSpan} used for fixing the text in {@link
-     * android.widget.TextView} when ellipsize, letter spacing and alignment are set.
-     */
-    IndentationFixSpan() {}
-
-    /**
-     * Creates an instance of {@link IndentationFixSpan} used for fixing the text in {@link
-     * android.widget.TextView} when ellipsize, letter spacing and alignment are set.
-     *
-     * @param layout The {@link StaticLayout} used for measuring how much Canvas should be rotated
-     *               in {@link #drawLeadingMargin}.
-     */
-    IndentationFixSpan(@NonNull StaticLayout layout) {
-        this.mOverrideLayoutForMeasuring = layout;
-    }
-
-    /**
-     * See {@link LeadingMarginSpan#drawLeadingMargin}.
-     *
-     * <p>If {@code IndentationFixSpan(StaticLayout)} has been used, the given {@code layout} would
-     * be ignored when doing measurements.
-     */
-    @Override
-    public void drawLeadingMargin(
-            @NonNull Canvas canvas,
-            @Nullable Paint paint,
-            int x,
-            int dir,
-            int top,
-            int baseline,
-            int bottom,
-            @Nullable CharSequence text,
-            int start,
-            int end,
-            boolean first,
-            @Nullable Layout layout) {
-        // If StaticLayout has been provided, we should use that one for measuring instead of the
-        // passed in one.
-        if (mOverrideLayoutForMeasuring != null) {
-            layout = mOverrideLayoutForMeasuring;
-        }
-
-        if (layout == null || paint == null) {
-            return;
-        }
-
-        float padding = calculatePadding(paint, start, layout);
-
-        if (padding != 0f) {
-            canvas.translate(padding, 0f);
-        }
-    }
-
-    /** Calculates the extra padding on ellipsized last line. Otherwise, returns 0. */
-    @VisibleForTesting
-    static float calculatePadding(@NonNull Paint paint, int start, @NonNull Layout layout) {
-        int lineIndex = layout.getLineForOffset(start);
-
-        // No action needed if line is not ellipsized and that is not the last line.
-        if (lineIndex != layout.getLineCount() - 1 || !isLineEllipsized(layout, lineIndex)) {
-            return 0f;
-        }
-
-        return layout.getParagraphDirection(lineIndex) == Layout.DIR_LEFT_TO_RIGHT
-                ? getEllipsizedPaddingForLtr(layout, lineIndex, paint)
-                : getEllipsizedPaddingForRtl(layout, lineIndex, paint);
-    }
-
-    /** Returns whether the given line is ellipsized. */
-    private static boolean isLineEllipsized(@NonNull Layout layout, int lineIndex) {
-        return layout.getEllipsisCount(lineIndex) > 0;
-    }
-
-    /**
-     * Gets the extra padding that is on the left when line is ellipsized on left-to-right layout
-     * direction. Otherwise, returns 0.
-     */
-    private static float getEllipsizedPaddingForLtr(
-            @NonNull Layout layout, int lineIndex, @NonNull Paint paint) {
-        float lineLeft = layout.getLineLeft(lineIndex);
-
-        if (lineLeft >= 0) {
-            return 0;
-        }
-
-        int ellipsisIndex = getEllipsisIndex(layout, lineIndex);
-        float horizontal = getHorizontalPosition(layout, ellipsisIndex);
-        float length = (horizontal - lineLeft) + paint.measureText(ELLIPSIS_CHAR);
-        float divideFactor = getDivideFactor(layout, lineIndex);
-
-        return abs(lineLeft) + ((layout.getWidth() - length) / divideFactor);
-    }
-
-    /**
-     * Gets the extra padding that is on the right when line is ellipsized on right-to-left layout
-     * direction. Otherwise, returns 0.
-     */
-    // TODO: b/323180070 - Investigate how to improve this so that text doesn't get clipped on large
-    // sizes as there is a bug in platform with letter spacing on formatting characters.
-    private static float getEllipsizedPaddingForRtl(
-            @NonNull Layout layout, int lineIndex, @NonNull Paint paint) {
-        float width = layout.getWidth();
-
-        if (width >= layout.getLineRight(lineIndex)) {
-            return 0;
-        }
-
-        int ellipsisIndex = getEllipsisIndex(layout, lineIndex);
-        float horizontal = getHorizontalPosition(layout, ellipsisIndex);
-        float length = (layout.getLineRight(lineIndex) - horizontal)
-                + paint.measureText(ELLIPSIS_CHAR);
-        float divideFactor = getDivideFactor(layout, lineIndex);
-
-        return width - layout.getLineRight(lineIndex) - ((width - length) / divideFactor);
-    }
-
-    private static float getHorizontalPosition(@NonNull Layout layout, int ellipsisIndex) {
-        return layout.getPrimaryHorizontal(ellipsisIndex);
-    }
-
-    private static int getEllipsisIndex(@NonNull Layout layout, int lineIndex) {
-        return layout.getLineStart(lineIndex) + layout.getEllipsisStart(lineIndex);
-    }
-
-    private static float getDivideFactor(@NonNull Layout layout, int lineIndex) {
-        return layout.getParagraphAlignment(lineIndex) == Alignment.ALIGN_CENTER ? 2f : 1f;
-    }
-}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 04eab6a..4997dde 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -28,7 +28,7 @@
 import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
 
 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
-import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
 
 import static java.lang.Math.max;
 import static java.lang.Math.min;
@@ -53,7 +53,6 @@
 import android.graphics.drawable.GradientDrawable;
 import android.text.SpannableStringBuilder;
 import android.text.Spanned;
-import android.text.StaticLayout;
 import android.text.TextPaint;
 import android.text.TextUtils;
 import android.text.TextUtils.TruncateAt;
@@ -181,9 +180,12 @@
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme.FontSet;
 import androidx.wear.protolayout.renderer.R;
 import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.LayoutDiff;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.TreeNodeWithChange;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LayoutInfo;
@@ -302,6 +304,7 @@
     final String mClickableIdExtra;
 
     @Nullable private final LoggingUtils mLoggingUtils;
+    @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
 
     @Nullable final Executor mLoadActionExecutor;
     final LoadActionListener mLoadActionListener;
@@ -529,6 +532,7 @@
         @NonNull private final String mClickableIdExtra;
 
         @Nullable private final LoggingUtils mLoggingUtils;
+        @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
         @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
         private final boolean mAnimationEnabled;
 
@@ -548,6 +552,7 @@
                 @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
                 @NonNull String clickableIdExtra,
                 @Nullable LoggingUtils loggingUtils,
+                @NonNull InflaterStatsLogger inflaterStatsLogger,
                 boolean animationEnabled,
                 boolean allowLayoutChangingBindsWithoutDefault,
                 boolean applyFontVariantBodyAsDefault) {
@@ -563,6 +568,7 @@
             this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault;
             this.mClickableIdExtra = clickableIdExtra;
             this.mLoggingUtils = loggingUtils;
+            this.mInflaterStatsLogger = inflaterStatsLogger;
             this.mExtensionViewProvider = extensionViewProvider;
             this.mApplyFontVariantBodyAsDefault = applyFontVariantBodyAsDefault;
         }
@@ -639,6 +645,12 @@
             return mLoggingUtils;
         }
 
+        /** Stats logger used for telemetry. */
+        @NonNull
+        public InflaterStatsLogger getInflaterStatsLogger() {
+            return mInflaterStatsLogger;
+        }
+
         /** View provider for the renderer extension. */
         @Nullable
         public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
@@ -679,6 +691,7 @@
             @Nullable private String mClickableIdExtra;
 
             @Nullable private LoggingUtils mLoggingUtils;
+            @Nullable private InflaterStatsLogger mInflaterStatsLogger;
 
             @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider = null;
 
@@ -789,6 +802,14 @@
                 return this;
             }
 
+            /** Sets the stats logger used for telemetry. */
+            @NonNull
+            public Builder setInflaterStatsLogger(
+                    @NonNull InflaterStatsLogger inflaterStatsLogger) {
+                this.mInflaterStatsLogger = inflaterStatsLogger;
+                return this;
+            }
+
             /**
              * Sets whether a "layout changing" data bind can be applied without the
              * "value_for_layout" field being filled in, or being set to zero / empty. Defaults to
@@ -835,7 +856,11 @@
                 if (mClickableIdExtra == null) {
                     mClickableIdExtra = DEFAULT_CLICKABLE_ID_EXTRA;
                 }
-
+                if (mInflaterStatsLogger == null) {
+                    mInflaterStatsLogger =
+                            new NoOpProviderStatsLogger("No implementation was provided")
+                                    .createInflaterStatsLogger();
+                }
                 return new Config(
                         mUiContext,
                         mLayout,
@@ -848,6 +873,7 @@
                         mExtensionViewProvider,
                         checkNotNull(mClickableIdExtra),
                         mLoggingUtils,
+                        mInflaterStatsLogger,
                         mAnimationEnabled,
                         mAllowLayoutChangingBindsWithoutDefault,
                         mApplyFontVariantBodyAsDefault);
@@ -874,6 +900,7 @@
                 config.getAllowLayoutChangingBindsWithoutDefault();
         this.mClickableIdExtra = config.getClickableIdExtra();
         this.mLoggingUtils = config.getLoggingUtils();
+        this.mInflaterStatsLogger = config.getInflaterStatsLogger();
         this.mExtensionViewProvider = config.getExtensionViewProvider();
         this.mApplyFontVariantBodyAsDefault = config.getApplyFontVariantBodyAsDefault();
     }
@@ -1732,7 +1759,9 @@
 
         if (modifiers.hasTransformation()) {
             applyTransformation(
-                    wrapper == null ? view : wrapper, modifiers.getTransformation(), posId,
+                    wrapper == null ? view : wrapper,
+                    modifiers.getTransformation(),
+                    posId,
                     pipelineMaker);
         }
 
@@ -2566,8 +2595,7 @@
                     }
 
                     @Override
-                    public void onViewDetachedFromWindow(@NonNull View v) {
-                    }
+                    public void onViewDetachedFromWindow(@NonNull View v) {}
                 });
     }
 
@@ -2673,10 +2701,13 @@
                 text.getText(),
                 t -> {
                     // Underlines are applied using a Spannable here, rather than setting paint bits
-                    // (or using Paint#setTextUnderline). When multiple fonts are mixed on the same
-                    // line (especially when mixing anything with NotoSans-CJK), multiple
-                    // underlines can appear. Using UnderlineSpan instead though causes the
-                    // correct behaviour to happen (only a single underline).
+                    // (or
+                    // using Paint#setTextUnderline). When multiple fonts are mixed on the same line
+                    // (especially when mixing anything with NotoSans-CJK), multiple underlines can
+                    // appear. Using UnderlineSpan instead though causes the correct behaviour to
+                    // happen
+                    // (only a
+                    // single underline).
                     SpannableStringBuilder ssb = new SpannableStringBuilder();
                     ssb.append(t);
 
@@ -2684,13 +2715,6 @@
                         ssb.setSpan(new UnderlineSpan(), 0, ssb.length(), Spanned.SPAN_MARK_MARK);
                     }
 
-                    // When letter spacing, align and ellipsize are applied to text, the ellipsized
-                    // line is indented wrong. This adds the IndentationFixSpan in order to fix
-                    // the issue.
-                    if (shouldAttachIndentationFixSpan(text)) {
-                        attachIndentationFixSpan(ssb, /* layoutForMeasuring= */ null);
-                    }
-
                     textView.setText(ssb);
                 },
                 posId,
@@ -2713,7 +2737,7 @@
 
         if (overflow.getValue() == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE
                 && !text.getText().hasDynamicValue()) {
-            adjustMaxLinesForEllipsize(textView, shouldAttachIndentationFixSpan(text));
+            adjustMaxLinesForEllipsize(textView);
         }
 
         // Text auto size is not supported for dynamic text.
@@ -2804,78 +2828,6 @@
     }
 
     /**
-     * Checks whether the {@link IndentationFixSpan} needs to be attached to fix the alignment on
-     * text.
-     */
-    private static boolean shouldAttachIndentationFixSpan(@NonNull Text text) {
-        boolean hasLetterSpacing =
-                text.hasFontStyle()
-                        && text.getFontStyle().hasLetterSpacing()
-                        && text.getFontStyle().getLetterSpacing().getValue() != 0;
-        boolean hasEllipsize =
-                text.hasOverflow()
-                        && (text.getOverflow().getValue() == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE
-                        || text.getOverflow().getValue()
-                        == TextOverflow.TEXT_OVERFLOW_ELLIPSIZE_END);
-        // Since default align is center, we need fix when either alignment is not set or it's set
-        // to center.
-        boolean isCenterAligned =
-                !text.hasMultilineAlignment()
-                        || text.getMultilineAlignment().getValue()
-                        == TextAlignment.TEXT_ALIGN_CENTER;
-        return hasLetterSpacing && hasEllipsize && isCenterAligned;
-    }
-
-    /**
-     * This fixes that issue by correctly indenting the ellipsized line by translating the canvas on
-     * the opposite direction.
-     *
-     * <p>When letter spacing, center alignment and ellipsize are all set to a TextView, depending
-     * on a length of overflow text, the last, ellipsized line starts getting cut of from the
-     * start side.
-     *
-     * <p>It should be applied to a text only when those three attributes are set.
-     */
-    private static void attachIndentationFixSpan(
-            @NonNull SpannableStringBuilder ssb, @Nullable StaticLayout layoutForMeasuring) {
-        if (ssb.length() == 0) {
-            return;
-        }
-
-        // Add additional span that accounts for the extra space that TextView adds when ellipsizing
-        // text.
-        IndentationFixSpan fixSpan =
-                layoutForMeasuring == null
-                        ? new IndentationFixSpan()
-                        : new IndentationFixSpan(layoutForMeasuring);
-        ssb.setSpan(fixSpan, ssb.length() - 1, ssb.length() - 1, /* flags= */ 0);
-    }
-
-    /**
-     * See {@link #attachIndentationFixSpan(SpannableStringBuilder, StaticLayout)}. This method uses
-     * {@link StaticLayout} for measurements.
-     */
-    private static void attachIndentationFixSpan(@NonNull TextView textView) {
-        // This is needed to be passed in as the original Layout would have ellipsize on
-        // a maxLines and only be updated after it's drawn, so we need to calculate
-        // padding based on the StaticLayout.
-        StaticLayout layoutForMeasuring =
-                StaticLayout.Builder.obtain(
-                                /* source= */ textView.getText(),
-                                /* start= */ 0,
-                                /* end= */ textView.getText().length(),
-                                /* paint= */ textView.getPaint(),
-                                /* width= */ textView.getMeasuredWidth())
-                        .setMaxLines(textView.getMaxLines())
-                        .setEllipsize(TruncateAt.END)
-                        .setIncludePad(false)
-                        .build();
-        SpannableStringBuilder ssb = new SpannableStringBuilder(textView.getText());
-        attachIndentationFixSpan(ssb, layoutForMeasuring);
-        textView.setText(ssb);
-    }
-
-    /**
      * Sorts out what maxLines should be if the text could possibly be truncated before maxLines is
      * reached.
      *
@@ -2884,17 +2836,12 @@
      * different than what TEXT_OVERFLOW_ELLIPSIZE_END does, as that option just ellipsizes the last
      * line of text.
      */
-    private void adjustMaxLinesForEllipsize(
-            @NonNull TextView textView, boolean shouldAttachIndentationFixSpan) {
+    private void adjustMaxLinesForEllipsize(@NonNull TextView textView) {
         textView.getViewTreeObserver()
                 .addOnPreDrawListener(
                         new OnPreDrawListener() {
                             @Override
                             public boolean onPreDraw() {
-                                if (textView.getText().length() == 0) {
-                                    return true;
-                                }
-
                                 ViewParent maybeParent = textView.getParent();
                                 if (!(maybeParent instanceof View)) {
                                     Log.d(
@@ -2917,10 +2864,6 @@
                                 // Update only if changed.
                                 if (availableLines < maxMaxLines) {
                                     textView.setMaxLines(availableLines);
-
-                                    if (shouldAttachIndentationFixSpan) {
-                                        attachIndentationFixSpan(textView);
-                                    }
                                 }
 
                                 // Cancel the current drawing pass.
@@ -3238,7 +3181,7 @@
      *     to the image view; otherwise returns null to indicate the failure of setting drawable.
      */
     @Nullable
-    private static Drawable setImageDrawable(
+    private Drawable setImageDrawable(
             ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
         try {
             return setImageDrawable(imageView, drawableFuture.get(), protoResId);
@@ -3255,8 +3198,10 @@
      *     null to indicate the failure of setting drawable.
      */
     @Nullable
-    private static Drawable setImageDrawable(
-            ImageView imageView, Drawable drawable, String protoResId) {
+    private Drawable setImageDrawable(ImageView imageView, Drawable drawable, String protoResId) {
+        if (drawable != null) {
+            mInflaterStatsLogger.logDrawableUsage(drawable);
+        }
         if (drawable instanceof BitmapDrawable
                 && ((BitmapDrawable) drawable).getBitmap().getByteCount()
                         > DEFAULT_MAX_BITMAP_RAW_SIZE) {
@@ -3386,7 +3331,7 @@
                     Log.w(
                             TAG,
                             "ArcLine length's value_for_layout is not a positive value. Element"
-                                + " won't be visible.");
+                                    + " won't be visible.");
                 }
                 sizeWrapper.setSweepAngleDegrees(sizeForLayout);
                 sizedLp.setAngularAlignment(
@@ -4165,8 +4110,9 @@
             Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
         if (dpProp.hasDynamicValue() && pipelineMaker.isPresent()) {
             try {
-                pipelineMaker.get().addPipelineFor(dpProp, dpProp.getValue(), posId,
-                        dynamicValueConsumer);
+                pipelineMaker
+                        .get()
+                        .addPipelineFor(dpProp, dpProp.getValue(), posId, dynamicValueConsumer);
             } catch (RuntimeException ex) {
                 Log.e(TAG, "Error building pipeline", ex);
                 staticValueConsumer.accept(dpProp.getValue());
@@ -4229,7 +4175,9 @@
                 pipelineMaker
                         .get()
                         .addPipelineFor(
-                                floatProp.getDynamicValue(), floatProp.getValue(), posId,
+                                floatProp.getDynamicValue(),
+                                floatProp.getValue(),
+                                posId,
                                 dynamicValueconsumer);
             } catch (RuntimeException ex) {
                 Log.e(TAG, "Error building pipeline", ex);
@@ -4634,7 +4582,7 @@
     /** Apply the mutation that was previously computed with {@link #computeMutation}. */
     @UiThread
     @NonNull
-    public ListenableFuture<Void> applyMutation(
+    public ListenableFuture<RenderingArtifact> applyMutation(
             @NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
         RenderedMetadata prevRenderedMetadata = getRenderedMetadata(prevInflatedParent);
         if (prevRenderedMetadata != null
@@ -4647,11 +4595,11 @@
         }
         if (groupMutation.isNoOp()) {
             // Nothing to do.
-            return immediateVoidFuture();
+            return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
         }
 
         if (groupMutation.mPipelineMaker.isPresent()) {
-            SettableFuture<Void> result = SettableFuture.create();
+            SettableFuture<RenderingArtifact> result = SettableFuture.create();
             groupMutation
                     .mPipelineMaker
                     .get()
@@ -4661,7 +4609,7 @@
                             () -> {
                                 try {
                                     applyMutationInternal(prevInflatedParent, groupMutation);
-                                    result.set(null);
+                                    result.set(RenderingArtifact.create(mInflaterStatsLogger));
                                 } catch (ViewMutationException ex) {
                                     result.setException(ex);
                                 }
@@ -4670,7 +4618,7 @@
         } else {
             try {
                 applyMutationInternal(prevInflatedParent, groupMutation);
-                return immediateVoidFuture();
+                return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
             } catch (ViewMutationException ex) {
                 return immediateFailedFuture(ex);
             }
@@ -4679,6 +4627,7 @@
 
     private void applyMutationInternal(
             @NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
+        mInflaterStatsLogger.logMutationChangedNodes(groupMutation.mInflatedViews.size());
         for (InflatedView inflatedView : groupMutation.mInflatedViews) {
             String posId = inflatedView.getTag();
             if (posId == null) {
@@ -4706,15 +4655,21 @@
             }
             // Remove the touch delegate to the view to be updated
             if (immediateParent.getTouchDelegate() != null) {
-                ((TouchDelegateComposite) immediateParent.getTouchDelegate())
-                        .removeDelegate(viewToUpdate);
+                TouchDelegateComposite delegateComposite =
+                        (TouchDelegateComposite) immediateParent.getTouchDelegate();
+                delegateComposite.removeDelegate(viewToUpdate);
 
                 // Make sure to remove the touch delegate when the actual clickable view is wrapped,
                 // for example ImageView inside the RatioViewWrapper
                 if (viewToUpdate instanceof ViewGroup
                         && ((ViewGroup) viewToUpdate).getChildCount() > 0) {
-                    ((TouchDelegateComposite) immediateParent.getTouchDelegate())
-                            .removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+                    delegateComposite.removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+                }
+
+                // If no more touch delegate left in the composite, remove it completely from the
+                // parent
+                if (delegateComposite.isEmpty()) {
+                    immediateParent.setTouchDelegate(null);
                 }
             }
             immediateParent.removeViewAt(childIndex);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index e6bf97c..6184ac8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -83,6 +83,10 @@
         mDelegates.remove(delegateView);
     }
 
+    boolean isEmpty() {
+        return mDelegates.isEmpty();
+    }
+
     @Override
     public boolean onTouchEvent(@NonNull MotionEvent event) {
         boolean eventForwarded = false;
@@ -125,7 +129,7 @@
     @Override
     @NonNull
     public AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo() {
-        if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+        if (VERSION.SDK_INT >= VERSION_CODES.Q && !mDelegates.isEmpty()) {
             Map<Region, View> targetMap = new ArrayMap<>(mDelegates.size());
             for (Map.Entry<View, DelegateInfo> entry : mDelegates.entrySet()) {
                 AccessibilityNodeInfo.TouchDelegateInfo info =
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
index 38d616d..a52e426 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/PositionIdTreeTest.java
@@ -161,7 +161,7 @@
     }
 
     @Test
-    public void findAncestor_onlySearchesNodesAboveTheNode() {
+    public void findAncestorValues_onlySearchesNodesAboveTheNode() {
         List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
 
         assertThat(mTree.findAncestorsFor(NODE_2_1, nodesOfInterest::contains))
@@ -169,7 +169,15 @@
     }
 
     @Test
-    public void findAncestor_disjointTree_searchesAllAboveNodes() {
+    public void findAncestorIds_onlySearchesNodesAboveTheNode() {
+        List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
+
+        assertThat(mTree.findAncestorsNodesFor(NODE_2_1, nodesOfInterest::contains))
+                .containsExactly(NODE_2, NODE_ROOT);
+    }
+
+    @Test
+    public void findAncestorValues_disjointTree_searchesAllAboveNodes() {
         // Missing NODE_3_1
         mTree.addOrReplace(NODE_3_1_1, mNode3Child1Child1);
         mTree.addOrReplace(NODE_3_1_1_1, mNode3Child1Child1Child1);
@@ -181,7 +189,19 @@
     }
 
     @Test
-    public void findAncestor_emptyTree_returnsNothing() {
+    public void findAncestorIds_disjointTree_searchesAllAboveNodes() {
+        // Missing NODE_3_1
+        mTree.addOrReplace(NODE_3_1_1, mNode3Child1Child1);
+        mTree.addOrReplace(NODE_3_1_1_1, mNode3Child1Child1Child1);
+        List<TreeNode> nodesOfInterest =
+                Arrays.asList(mNodeRoot, mNode1, mNode3Child1Child1, mNode3);
+
+        assertThat(mTree.findAncestorsNodesFor(NODE_3_1_1_1, nodesOfInterest::contains))
+                .containsExactly(ROOT_NODE_ID, NODE_3_1_1, NODE_3);
+    }
+
+    @Test
+    public void findAncestorValues_emptyTree_returnsNothing() {
         mTree.clear();
         List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
 
@@ -189,11 +209,24 @@
     }
 
     @Test
-    public void findAncestor_noMatch_returnsNothing() {
+    public void findAncestorIds_emptyTree_returnsNothing() {
+        mTree.clear();
+        List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
+
+        assertThat(mTree.findAncestorsNodesFor(NODE_2_1, nodesOfInterest::contains)).isEmpty();
+    }
+
+    @Test
+    public void findAncestorValues_noMatch_returnsNothing() {
         assertThat(mTree.findAncestorsFor(NODE_2_1, treeNode -> false)).isEmpty();
     }
 
     @Test
+    public void findAncestorIds_noMatch_returnsNothing() {
+        assertThat(mTree.findAncestorsNodesFor(NODE_2_1, treeNode -> false)).isEmpty();
+    }
+
+    @Test
     public void findChildren_onlySearchesBelowTheNode() {
         List<TreeNode> nodesOfInterest = Arrays.asList(mNodeRoot, mNode2, mNode2Child1, mNode1);
 
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
index de6cd5a..0f8d2d3 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinter.java
@@ -16,6 +16,8 @@
 
 package androidx.wear.protolayout.renderer.helper;
 
+import static androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase.ADAPTER;
+
 import androidx.annotation.Nullable;
 import androidx.wear.protolayout.proto.FingerprintProto.NodeFingerprint;
 import androidx.wear.protolayout.proto.FingerprintProto.TreeFingerprint;
@@ -96,6 +98,7 @@
         for (LayoutElementProto.ArcLayoutElement child : getArcChildren(element)) {
             addNodeToParent(child, currentFingerprintBuilder);
         }
+
         NodeFingerprint currentFingerprint = currentFingerprintBuilder.build();
         if (parentFingerprintBuilder != null) {
             addNodeToParent(currentFingerprint, parentFingerprintBuilder);
@@ -106,12 +109,14 @@
     private void addNodeToParent(
             LayoutElementProto.ArcLayoutElement element,
             NodeFingerprint.Builder parentFingerprintBuilder) {
-        addNodeToParent(
+        NodeFingerprint.Builder currentFingerprint =
                 NodeFingerprint.newBuilder()
                         .setSelfTypeValue(getSelfTypeFingerprint(element))
-                        .setSelfPropsValue(getSelfPropsFingerprint(element))
-                        .build(),
-                parentFingerprintBuilder);
+                        .setSelfPropsValue(getSelfPropsFingerprint(element));
+        if (element.getInnerCase() == ADAPTER) {
+            addNodeToParent(element.getAdapter().getContent(), currentFingerprint);
+        }
+        addNodeToParent(currentFingerprint.build(), parentFingerprintBuilder);
     }
 
     private void addNodeToParent(
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
index 15f0481..b0d7c9c 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/helper/TestFingerprinterTest.java
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package androidx.wear.protolayout.renderer.helper;
 
 import static androidx.wear.protolayout.renderer.helper.TestDsl.arc;
+import static androidx.wear.protolayout.renderer.helper.TestDsl.arcAdapter;
 import static androidx.wear.protolayout.renderer.helper.TestDsl.arcText;
 import static androidx.wear.protolayout.renderer.helper.TestDsl.column;
 import static androidx.wear.protolayout.renderer.helper.TestDsl.layout;
@@ -43,57 +43,62 @@
                 TestFingerprinter.getDefault()
                         .buildLayoutWithFingerprints(referenceLayout().getRoot());
         NodeFingerprint root = layout.getFingerprint().getRoot();
-
         Set<Integer> selfPropsFingerprints = new HashSet<>();
         Set<Integer> childFingerprints = new HashSet<>();
-
         // 1
         NodeFingerprint node = root;
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesCount()).isEqualTo(4);
         assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
         // 1.1
         node = root.getChildNodes(0);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesCount()).isEqualTo(2);
         assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
         // 1.1.1
         node = root.getChildNodes(0).getChildNodes(0);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesList()).isEmpty();
         assertThat(node.getChildNodesValue()).isEqualTo(0);
-
         // 1.1.2
         node = root.getChildNodes(0).getChildNodes(1);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesList()).isEmpty();
         assertThat(node.getChildNodesValue()).isEqualTo(0);
-
         // 1.2
         node = root.getChildNodes(1);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesCount()).isEqualTo(1);
         assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
-
         // 1.2.1
         node = root.getChildNodes(1).getChildNodes(0);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesList()).isEmpty();
         assertThat(node.getChildNodesValue()).isEqualTo(0);
-
         // 1.3
         node = root.getChildNodes(2);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesList()).isEmpty();
         assertThat(node.getChildNodesValue()).isEqualTo(0);
-
         // 1.4
         node = root.getChildNodes(3);
         assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+        assertThat(node.getChildNodesCount()).isEqualTo(2);
+        assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+        // 1.4.1
+        node = root.getChildNodes(3).getChildNodes(0);
+        assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+        assertThat(node.getChildNodesCount()).isEqualTo(0);
+        assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+        // 1.4.2
+        node = root.getChildNodes(3).getChildNodes(1);
+        assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
         assertThat(node.getChildNodesCount()).isEqualTo(1);
         assertThat(childFingerprints.add(node.getChildNodesValue())).isTrue();
+        // 1.4.2.1
+        node = root.getChildNodes(3).getChildNodes(1).getChildNodes(0);
+        assertThat(selfPropsFingerprints.add(node.getSelfPropsValue())).isTrue();
+        assertThat(node.getChildNodesCount()).isEqualTo(0);
     }
 
     @Test
@@ -102,12 +107,10 @@
                 TestFingerprinter.getDefault()
                         .buildLayoutWithFingerprints(referenceLayout().getRoot());
         NodeFingerprint refRoot = refLayout.getFingerprint().getRoot();
-
         Layout layout =
                 TestFingerprinter.getDefault()
                         .buildLayoutWithFingerprints(layoutWithDifferentColumnHeight().getRoot());
         NodeFingerprint root = layout.getFingerprint().getRoot();
-
         // 1
         NodeFingerprint refNode = refRoot;
         NodeFingerprint node = root;
@@ -116,7 +119,6 @@
                 .isNotEqualTo(refNode.getSelfPropsValue()); // Only difference
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1
         refNode = refRoot.getChildNodes(0);
         node = root.getChildNodes(0);
@@ -124,7 +126,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1.1
         refNode = refRoot.getChildNodes(0).getChildNodes(0);
         node = root.getChildNodes(0).getChildNodes(0);
@@ -132,7 +133,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1.2
         refNode = refRoot.getChildNodes(0).getChildNodes(1);
         node = root.getChildNodes(0).getChildNodes(1);
@@ -140,7 +140,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.2
         refNode = refRoot.getChildNodes(1);
         node = root.getChildNodes(1);
@@ -148,7 +147,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.2.1
         refNode = refRoot.getChildNodes(1).getChildNodes(0);
         node = root.getChildNodes(1).getChildNodes(0);
@@ -156,7 +154,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.3
         refNode = refRoot.getChildNodes(2);
         node = root.getChildNodes(2);
@@ -164,7 +161,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.4
         refNode = refRoot.getChildNodes(3);
         node = root.getChildNodes(3);
@@ -180,12 +176,10 @@
                 TestFingerprinter.getDefault()
                         .buildLayoutWithFingerprints(referenceLayout().getRoot());
         NodeFingerprint refRoot = refLayout.getFingerprint().getRoot();
-
         Layout layout =
                 TestFingerprinter.getDefault()
                         .buildLayoutWithFingerprints(layoutWithDifferentText().getRoot());
         NodeFingerprint root = layout.getFingerprint().getRoot();
-
         // 1
         NodeFingerprint refNode = refRoot;
         NodeFingerprint node = root;
@@ -193,7 +187,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isNotEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1
         refNode = refRoot.getChildNodes(0);
         node = root.getChildNodes(0);
@@ -201,7 +194,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1.1
         refNode = refRoot.getChildNodes(0).getChildNodes(0);
         node = root.getChildNodes(0).getChildNodes(0);
@@ -209,7 +201,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.1.2
         refNode = refRoot.getChildNodes(0).getChildNodes(1);
         node = root.getChildNodes(0).getChildNodes(1);
@@ -217,7 +208,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.2
         refNode = refRoot.getChildNodes(1);
         node = root.getChildNodes(1);
@@ -225,7 +215,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isNotEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.2.1
         refNode = refRoot.getChildNodes(1).getChildNodes(0);
         node = root.getChildNodes(1).getChildNodes(0);
@@ -234,7 +223,6 @@
                 .isNotEqualTo(refNode.getSelfPropsValue()); // Updated text
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.3
         refNode = refRoot.getChildNodes(2);
         node = root.getChildNodes(2);
@@ -242,7 +230,6 @@
         assertThat(node.getSelfPropsValue()).isEqualTo(refNode.getSelfPropsValue());
         assertThat(node.getChildNodesValue()).isEqualTo(refNode.getChildNodesValue());
         assertThat(node.getChildNodesCount()).isEqualTo(refNode.getChildNodesCount());
-
         // 1.4
         refNode = refRoot.getChildNodes(3);
         node = root.getChildNodes(3);
@@ -273,8 +260,10 @@
                         text("blah blah"), // 1.3
                         arc( // 1.4
                                 props -> props.anchorAngleDegrees = 15, // arc props
-                                arcText("arctext") // 1.4.1
-                                )));
+                                arcText("arctext"), // 1.4.1
+                                arcAdapter( // 1.4.2
+                                        text("text") // 1.4.2.1
+                                        ))));
     }
 
     private static Layout layoutWithDifferentColumnHeight() {
@@ -298,8 +287,10 @@
                         text("blah blah"), // 1.3
                         arc( // 1.4
                                 props -> props.anchorAngleDegrees = 15, // arc props
-                                arcText("arctext") // 1.4.1
-                                )));
+                                arcText("arctext"), // 1.4.1
+                                arcAdapter( // 1.4.2
+                                        text("text") // 1.4.2.1
+                                        ))));
     }
 
     private static Layout layoutWithDifferentText() {
@@ -323,7 +314,9 @@
                         text("blah blah"), // 1.3
                         arc( // 1.4
                                 props -> props.anchorAngleDegrees = 15, // arc props
-                                arcText("arctext") // 1.4.1
-                                )));
+                                arcText("arctext"), // 1.4.1
+                                arcAdapter( // 1.4.2
+                                        text("text") // 1.4.2.1
+                                        ))));
     }
 }
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 6db2dfb..d095d58 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -53,6 +53,7 @@
 import androidx.wear.protolayout.expression.pipeline.StateStore;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.ResourceProto.Resources;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.helper.TestDsl.LayoutNode;
 import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config;
 
@@ -97,8 +98,8 @@
     @Test
     public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -108,7 +109,7 @@
         assertThat(layout1).hasSize(1);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -120,8 +121,8 @@
     @Test
     public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -131,7 +132,7 @@
         assertThat(layout1).hasSize(1);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -144,8 +145,8 @@
     @Test
     public void reattach_usesCachedLayoutForDiffUpdate() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -156,7 +157,7 @@
         mInstanceUnderTest.detach(mRootContainer);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -172,8 +173,8 @@
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
 
         // First one that does the full layout update.
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -184,7 +185,7 @@
 
         // Second one that applies mutation only.
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         // Detach so it can't apply update.
         mInstanceUnderTest.detach(mRootContainer);
@@ -200,9 +201,8 @@
 
         // Render the first layout.
         Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
-                        layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -227,9 +227,7 @@
         // not changed part of the layout was also changed in inflated View.
         Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
 
-        result =
-                mInstanceUnderTest.renderAndAttach(
-                        layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
 
         // Make sure future is computing result.
         assertThat(result.isDone()).isFalse();
@@ -252,8 +250,8 @@
 
         // Render the first layout.
         Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -263,7 +261,7 @@
         assertThat(findViewsWithText(mRootContainer, TEXT2)).hasSize(1);
 
         Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
         // Make sure future is computing result.
         assertThat(result.isDone()).isFalse();
         shadowOf(Looper.getMainLooper()).idle();
@@ -279,13 +277,13 @@
     public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsPreviousLayout() {
         FrameLayout container = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result1 =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result1 =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
         assertThat(result1.isDone()).isFalse();
 
-        ListenableFuture<Void> result2 =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result2 =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -301,14 +299,14 @@
         FrameLayout container1 = new FrameLayout(mApplicationContext);
         FrameLayout container2 = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
 
         assertThrows(
                 IllegalStateException.class,
                 () ->
-                        mInstanceUnderTest.renderAndAttach(
+                        mInstanceUnderTest.renderLayoutAndAttach(
                                 layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container2));
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -321,12 +319,14 @@
         FrameLayout container1 = new FrameLayout(mApplicationContext);
         FrameLayout container2 = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result1 =
-                mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
+        ListenableFuture<RenderingArtifact> result1 =
+                mInstanceUnderTest.renderLayoutAndAttach(
+                        layout(text(TEXT1)), RESOURCES, container1);
         mInstanceUnderTest.detach(container1);
 
-        ListenableFuture<Void> result2 =
-                mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
+        ListenableFuture<RenderingArtifact> result2 =
+                mInstanceUnderTest.renderLayoutAndAttach(
+                        layout(text(TEXT1)), RESOURCES, container2);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(result1.isCancelled()).isTrue();
@@ -341,13 +341,13 @@
             throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
 
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
@@ -360,14 +360,14 @@
         Layout layout1 = layout(text(TEXT1));
         Layout layout2 = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
 
         // Make sure we have an UnchangedRenderResult
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -377,7 +377,7 @@
         assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
         shadowOf(Looper.getMainLooper()).idle();
 
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
 
         assertThat(result.isDone()).isTrue();
         assertNoException(result);
@@ -388,8 +388,8 @@
     public void fullInflationResultCanBeReused() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
         Layout layout = layout(text(TEXT1));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -397,7 +397,7 @@
         ListenableFuture<?> renderFuture = mInstanceUnderTest.mRenderFuture;
 
         mInstanceUnderTest.detach(mRootContainer);
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
@@ -409,15 +409,15 @@
             throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         List<View> textViews1 = findViewsWithText(mRootContainer, TEXT1);
         assertThat(textViews1).hasSize(1);
 
         mInstanceUnderTest.close();
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
         shadowOf(Looper.getMainLooper()).idle();
@@ -431,8 +431,8 @@
     public void detach_clearsHostView() throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -449,14 +449,14 @@
         Layout layout2 = layout(text(TEXT1));
         Resources resources2 = Resources.newBuilder().setVersion("2").build();
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
         View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
 
-        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -472,15 +472,15 @@
         Layout layout2 = layout(text(TEXT1));
         Resources resources2 = Resources.newBuilder().setVersion("1").build();
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
         View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
 
         mInstanceUnderTest.invalidateCache();
-        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -490,13 +490,28 @@
     }
 
     @Test
+    public void invalidateCache_ongoingInflation_oldInflationGetsCancelled() throws Exception {
+        Layout layout1 = layout(text(TEXT1));
+        Resources resources1 = Resources.newBuilder().setVersion("1").build();
+        setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
+
+        mInstanceUnderTest.invalidateCache();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(result.isCancelled()).isTrue();
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
+    }
+
+    @Test
     public void adaptiveUpdateRatesEnabled_rootElementdiff_keepsElementCentered() throws Exception {
         int dimension = 50;
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
 
         // Full inflation.
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(
                                 column(
                                         props -> {
@@ -518,7 +533,7 @@
 
         // Diff update only for the root element.
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(
                                 column(
                                         props -> {
@@ -546,8 +561,8 @@
     public void close_clearsHostView() throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -562,7 +577,9 @@
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
         assertThrows(
                 ExecutionException.class,
-                () -> renderAndAttachLayout(layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
+                () ->
+                        renderLayoutAndAttachLayout(
+                                layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
     }
 
     @Test
@@ -573,8 +590,8 @@
         for (int i = 0; i < children.length; i++) {
             children[i] = recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1);
         }
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // MAX_LAYOUT_ELEMENT_DEPTH branches of depth MAX_LAYOUT_ELEMENT_DEPTH - 1.
                         // Total depth is MAX_LAYOUT_ELEMENT_DEPTH (if we count the head).
                         layout(box(children)), RESOURCES, mRootContainer);
@@ -591,7 +608,7 @@
         assertThrows(
                 ExecutionException.class,
                 () ->
-                        renderAndAttachLayout(
+                        renderLayoutAndAttachLayout(
                                 // Total number of views is = MAX_LAYOUT_ELEMENT_DEPTH  + 1 (span
                                 // text)
                                 layout(
@@ -599,8 +616,8 @@
                                                 MAX_LAYOUT_ELEMENT_DEPTH,
                                                 spannable(spanText("Hello"))))));
 
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // Total number of views is = (MAX_LAYOUT_ELEMENT_DEPTH -1)  + 1 (span text)
                         layout(
                                 recursiveBox(
@@ -620,12 +637,12 @@
         assertThrows(
                 ExecutionException.class,
                 () ->
-                        renderAndAttachLayout(
+                        renderLayoutAndAttachLayout(
                                 // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH)
                                 layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH))))));
 
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH - 1)
                         // = MAX_LAYOUT_ELEMENT_DEPTH
                         layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1)))),
@@ -637,9 +654,9 @@
         assertThat(mRootContainer.getChildCount()).isEqualTo(1);
     }
 
-    private void renderAndAttachLayout(Layout layout) throws Exception {
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+    private void renderLayoutAndAttachLayout(Layout layout) throws Exception {
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
     }
@@ -690,7 +707,8 @@
         return views;
     }
 
-    private static void assertNoException(ListenableFuture<Void> result) throws Exception {
+    private static void assertNoException(ListenableFuture<RenderingArtifact> result)
+            throws Exception {
         // Assert that result hasn't thrown exception.
         result.get();
     }
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java
deleted file mode 100644
index e795c5c..0000000
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/IndentationFixSpanTest.java
+++ /dev/null
@@ -1,324 +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.wear.protolayout.renderer.inflater;
-
-import static androidx.wear.protolayout.renderer.inflater.IndentationFixSpan.ELLIPSIS_CHAR;
-import static androidx.wear.protolayout.renderer.inflater.IndentationFixSpan.calculatePadding;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.text.Layout;
-import android.text.Layout.Alignment;
-import android.text.StaticLayout;
-import android.text.TextPaint;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.google.common.truth.Expect;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.Objects;
-
-/**
- * Tests that the actual padding is correctly calculated. The translation of canvas is tested with
- * screenshot tests.
- */
-@RunWith(AndroidJUnit4.class)
-public class IndentationFixSpanTest {
-
-    private static final String TEST_TEXT = "Test";
-    private static final int DEFAULT_ELLIPSIZE_START = 100;
-    private static final TestPaint PAINT = new TestPaint();
-
-    @Rule public final Expect expect = Expect.create();
-
-    @Test
-    public void test_givenLayout_correctObjectIsUsed() {
-        StaticLayout layout = mock(StaticLayout.class);
-        IndentationFixSpan span = new IndentationFixSpan(layout);
-        Layout givenLayout = mock(Layout.class);
-
-        span.drawLeadingMargin(
-                mock(Canvas.class),
-                mock(Paint.class),
-                /* x= */ 0,
-                /* dir= */ 0,
-                /* top= */ 0,
-                /* baseline= */ 0,
-                /* bottom= */ 0,
-                "Text",
-                /* start= */ 0,
-                /* end= */ 0,
-                false,
-                givenLayout);
-
-        verifyNoInteractions(givenLayout);
-
-        verify(layout).getLineCount();
-        verify(layout).getLineForOffset(/* offset= */ 0);
-    }
-
-    @Test
-    public void test_calculatedPadding_onRtl_centerAlign_correctValue() {
-        TestLayoutRtl layout =
-                new TestLayoutRtl(
-                        TEST_TEXT,
-                        /* width= */ 288,
-                        Alignment.ALIGN_CENTER,
-                        /* mainLineIndex= */ 2);
-
-        // On ellipsized line there is padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(-8.5f);
-    }
-
-    @Test
-    public void test_calculatedPadding_onRtl_normalAlign_correctValue() {
-        TestLayoutRtl layout =
-                new TestLayoutRtl(
-                        TEST_TEXT,
-                        /* width= */ 288,
-                        Alignment.ALIGN_NORMAL,
-                        /* mainLineIndex= */ 2);
-
-        // On ellipsized line there is padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(-9f);
-    }
-
-    @Test
-    public void test_calculatedPadding_onLtr_centerAlign_correctValue() {
-        TestLayoutLtr layout =
-                new TestLayoutLtr(
-                        TEST_TEXT,
-                        /* width= */ 300,
-                        Alignment.ALIGN_CENTER,
-                        /* mainLineIndex= */ 2);
-
-        // On ellipsized line there is padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(13f);
-    }
-
-    @Test
-    public void test_calculatedPadding_onLtr_normalAlign_correctValue() {
-        TestLayoutLtr layout =
-                new TestLayoutLtr(
-                        TEST_TEXT,
-                        /* width= */ 300,
-                        Alignment.ALIGN_NORMAL,
-                        /* mainLineIndex= */ 2);
-
-        // On ellipsized line there is padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(19f);
-    }
-
-    @Test
-    public void test_calculatePadding_lastLineNotEllipsize_returnsZero() {
-        // Number of lines so that notEllipsizedLineIndex is the last one.
-        TestLayoutLtr layout =
-                new TestLayoutLtr(
-                        TEST_TEXT,
-                        /* width= */ 300,
-                        Alignment.ALIGN_CENTER,
-                        /* mainLineIndex= */ 2);
-        // But not ellipsized.
-        layout.removeEllipsisCount();
-
-        // On not ellipsized line there is no padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(0);
-    }
-
-    @Test
-    public void test_calculatePadding_notLastLine_returnsZero() {
-        // Number of lines so that notEllipsizedLineIndex is the last one.
-        TestLayoutLtr layout =
-                new TestLayoutLtr(
-                        TEST_TEXT,
-                        /* width= */ 300,
-                        Alignment.ALIGN_CENTER,
-                        /* mainLineIndex= */ 2);
-        // Number of lines so that lineIndex is NOT the last one.
-        layout.increaseLineCount();
-
-        // On not last line there is no padding.
-        expect.that(calculatePadding(PAINT, DEFAULT_ELLIPSIZE_START, layout)).isEqualTo(0);
-    }
-
-    private static class TestPaint extends TextPaint {
-
-        @Override
-        public float measureText(String text) {
-            if (Objects.equals(text, ELLIPSIS_CHAR)) {
-                return 23f;
-            }
-            return super.measureText(text);
-        }
-    }
-
-    /**
-     * Test only implementation of {@link Layout} with numbers so we can test padding correctly.
-     */
-    private abstract static class TestLayout extends Layout {
-
-        private static final int DEFAULT_ELLIPSIS_COUNT = 3;
-        protected final int mMainLineIndex;
-
-        // Overridable values for the mainLineIndex.
-        private int mLineCount;
-        private int mEllipsisCount = DEFAULT_ELLIPSIS_COUNT;
-
-        protected TestLayout(CharSequence text, int width, Alignment align, int mainLineIndex) {
-            super(text, PAINT, width, align, /* spacingMult= */ 0, /* spacingAdd= */ 0);
-            this.mMainLineIndex = mainLineIndex;
-            mLineCount = mainLineIndex + 1;
-        }
-
-        void increaseLineCount() {
-            mLineCount = mLineCount + 3;
-        }
-
-        void removeEllipsisCount() {
-            this.mEllipsisCount = 0;
-        }
-
-        @Override
-        public int getLineCount() {
-            return mLineCount;
-        }
-
-        @Override
-        public int getLineTop(int line) {
-            // N/A
-            return 0;
-        }
-
-        @Override
-        public int getLineDescent(int line) {
-            // N/A
-            return 0;
-        }
-
-        @Override
-        public int getLineStart(int line) {
-            return 0;
-        }
-
-        @Override
-        public boolean getLineContainsTab(int line) {
-            // N/A
-            return false;
-        }
-
-        @Override
-        public Directions getLineDirections(int line) {
-            // N/A
-            return null;
-        }
-
-        @Override
-        public int getTopPadding() {
-            // N/A
-            return 0;
-        }
-
-        @Override
-        public int getBottomPadding() {
-            // N/A
-            return 0;
-        }
-
-        @Override
-        public int getEllipsisCount(int line) {
-            return line == mMainLineIndex ? /* non zero */ mEllipsisCount : 0;
-        }
-
-        @Override
-        public int getLineForOffset(int offset) {
-            return offset == DEFAULT_ELLIPSIZE_START ? mMainLineIndex : 0;
-        }
-    }
-
-    /**
-     * Specific implementation of {@link Layout} that returns numbers for LTR testing of padding.
-     */
-    private static class TestLayoutLtr extends TestLayout {
-
-        protected TestLayoutLtr(CharSequence text, int width, Alignment align, int mainLineIndex) {
-            super(text, width, align, mainLineIndex);
-        }
-
-        @Override
-        public float getPrimaryHorizontal(int offset) {
-            return 258f;
-        }
-
-        @Override
-        public float getLineLeft(int line) {
-            return line == mMainLineIndex ? -7f : super.getLineLeft(line);
-        }
-
-        @Override
-        public int getEllipsisStart(int line) {
-            return 20;
-        }
-
-        @Override
-        public int getParagraphDirection(int line) {
-            return Layout.DIR_LEFT_TO_RIGHT;
-        }
-    }
-
-    /**
-     * Specific implementation of {@link Layout} that returns numbers for RTL testing of padding.
-     */
-    private static class TestLayoutRtl extends TestLayout {
-
-        protected TestLayoutRtl(CharSequence text, int width, Alignment align, int mainLineIndex) {
-            super(text, width, align, mainLineIndex);
-        }
-
-        @Override
-        public float getPrimaryHorizontal(int offset) {
-            return 32f;
-        }
-
-        @Override
-        public float getLineLeft(int line) {
-            return line == mMainLineIndex ? -7f : super.getLineLeft(line);
-        }
-
-        @Override
-        public int getEllipsisStart(int line) {
-            return 20;
-        }
-
-        @Override
-        public int getParagraphDirection(int line) {
-            return Layout.DIR_RIGHT_TO_LEFT;
-        }
-
-        @Override
-        public float getLineRight(int line) {
-            return line == mMainLineIndex ? 296f : super.getLineRight(line);
-        }
-    }
-}
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index da07985..1a46140 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -83,7 +83,6 @@
 import androidx.core.content.ContextCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -203,6 +202,8 @@
 import androidx.wear.protolayout.proto.TypesProto.StringProp;
 import androidx.wear.protolayout.protobuf.ByteString;
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.helper.TestFingerprinter;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -222,6 +223,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowChoreographer;
 import org.robolectric.shadows.ShadowLooper;
 import org.robolectric.shadows.ShadowPackageManager;
@@ -543,37 +545,41 @@
         int width = 10;
         int height = 12;
         byte[] payload = "Hello World".getBytes(UTF_8);
+        LayoutElement extension =
+                LayoutElement.newBuilder()
+                        .setExtension(
+                                ExtensionLayoutElement.newBuilder()
+                                        .setExtensionId("foo")
+                                        .setPayload(ByteString.copyFrom(payload))
+                                        .setWidth(
+                                                ExtensionDimension.newBuilder()
+                                                        .setLinearDimension(dp(width))
+                                                        .build())
+                                        .setHeight(
+                                                ExtensionDimension.newBuilder()
+                                                        .setLinearDimension(dp(height))
+                                                        .build()))
+                        .build();
         LayoutElement root =
                 LayoutElement.newBuilder()
-                    .setBox(
-                        Box.newBuilder()
-                            // Outer box's width and height left at default value of "wrap"
-                            .addContents(
-                                LayoutElement.newBuilder()
-                                    .setExtension(
-                                        ExtensionLayoutElement.newBuilder()
-                                            .setExtensionId("foo")
-                                            .setPayload(ByteString.copyFrom(payload))
-                                            .setWidth(
-                                                ExtensionDimension.newBuilder()
-                                                    .setLinearDimension(dp(width))
-                                                    .build())
-                                            .setHeight(
-                                                ExtensionDimension.newBuilder()
-                                                    .setLinearDimension(dp(height))
-                                                    .build()))))
+                        .setBox(
+                                Box.newBuilder()
+                                        // Outer box's width and height left at default value of
+                                        // "wrap"
+                                        .addContents(extension))
                         .build();
 
         FrameLayout rootLayout =
                 renderer(
-                    newRendererConfigBuilder(fingerprintedLayout(root))
-                        .setExtensionViewProvider(
-                            (extensionPayload, id) -> {
-                                TextView returnedView = new TextView(getApplicationContext());
-                                returnedView.setText("testing");
+                                newRendererConfigBuilder(fingerprintedLayout(root))
+                                        .setExtensionViewProvider(
+                                                (extensionPayload, id) -> {
+                                                    TextView returnedView =
+                                                            new TextView(getApplicationContext());
+                                                    returnedView.setText("testing");
 
-                                return returnedView;
-                            }))
+                                                    return returnedView;
+                                                }))
                         .inflate();
 
         // Check that the outer box is displayed and it has a child.
@@ -976,6 +982,23 @@
 
         // A column with a row (Spacer + Spacer) and Spacer, everything has weighted expand
         // dimension.
+
+        Row rowWithSpacers =
+                Row.newBuilder()
+                        .setWidth(expand())
+                        .setHeight(
+                                ContainerDimension.newBuilder()
+                                        .setExpandedDimension(expandWithWeight(heightWeight1))
+                                        .build())
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setSpacer(
+                                                buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setSpacer(
+                                                buildExpandedSpacer(widthWeight2, DEFAULT_WEIGHT)))
+                        .build();
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setColumn(
@@ -983,26 +1006,13 @@
                                         .setWidth(expand())
                                         .setHeight(expand())
                                         .addContents(
-                                                LayoutElement.newBuilder()
-                                                        .setRow(
-                                                                Row.newBuilder()
-                                                                        .setWidth(expand())
-                                                                        .setHeight(
-                                                                                ContainerDimension.newBuilder()
-                                                                                        .setExpandedDimension(expandWithWeight(heightWeight1))
-                                                                                        .build())
-                                                                        .addContents(
-                                                                                LayoutElement.newBuilder()
-                                                                                        .setSpacer(
-                                                                                                buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
-                                                                        .addContents(
-                                                                                LayoutElement.newBuilder()
-                                                                                        .setSpacer(
-                                                                                                buildExpandedSpacer(
-                                                                                                        widthWeight2, DEFAULT_WEIGHT)))))
+                                                LayoutElement.newBuilder().setRow(rowWithSpacers))
                                         .addContents(
                                                 LayoutElement.newBuilder()
-                                                        .setSpacer(buildExpandedSpacer(DEFAULT_WEIGHT, heightWeight2)))
+                                                        .setSpacer(
+                                                                buildExpandedSpacer(
+                                                                        DEFAULT_WEIGHT,
+                                                                        heightWeight2)))
                                         .build())
                         .build();
 
@@ -1034,9 +1044,9 @@
                                                         .setExpandedDimension(
                                                                 ExpandedDimensionProp
                                                                         .getDefaultInstance()))
-                                        .setWidth(SpacerDimension
-                                                .newBuilder()
-                                                .setLinearDimension(dp(width))))
+                                        .setWidth(
+                                                SpacerDimension.newBuilder()
+                                                        .setLinearDimension(dp(width))))
                         .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
@@ -1543,17 +1553,17 @@
                 ContainerDimension.newBuilder().setLinearDimension(dp(childSize)).build();
 
         LayoutElement childBox =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(childBoxSize)
-                                .setHeight(childBoxSize)
-                                .setModifiers(
-                                        Modifiers.newBuilder()
-                                                .setClickable(
-                                                        Clickable.newBuilder()
-                                                                .setId("foo")
-                                                                .setOnClick(
-                                                                        action))))
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(childBoxSize)
+                                        .setHeight(childBoxSize)
+                                        .setModifiers(
+                                                Modifiers.newBuilder()
+                                                        .setClickable(
+                                                                Clickable.newBuilder()
+                                                                        .setId("foo")
+                                                                        .setOnClick(action))))
                         .build();
 
         LayoutElement root =
@@ -1563,13 +1573,14 @@
                                         .setWidth(parentBoxSize)
                                         .setHeight(parentBoxSize)
                                         .addContents(childBox))
-                                        .build();
+                        .build();
 
         State.Builder receivedState = State.newBuilder();
         FrameLayout rootLayout =
                 renderer(
-                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
-                                .setLoadActionListener(receivedState::mergeFrom))
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
                         .inflate();
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -1627,8 +1638,7 @@
                                         .setWidth(
                                                 SpacerDimension.newBuilder()
                                                         .setLinearDimension(dp(spacerSize))
-                                                        .build()
-                                        ));
+                                                        .build()));
 
         //           |--clickable area child box 1 (5 - 35)--|
         //                                          |---clickable area child box 2 (30-60)--|
@@ -1647,8 +1657,7 @@
                                                                                 dp(clickTargetSize))
                                                                         .setMinimumClickableHeight(
                                                                                 dp(clickTargetSize))
-                                                                        .setOnClick(
-                                                                                action)
+                                                                        .setOnClick(action)
                                                                         .setId("foo1"))))
                         .build();
 
@@ -1666,8 +1675,7 @@
                                                                                 dp(clickTargetSize))
                                                                         .setMinimumClickableHeight(
                                                                                 dp(clickTargetSize))
-                                                                        .setOnClick(
-                                                                                action)
+                                                                        .setOnClick(action)
                                                                         .setId("foo2"))))
                         .build();
 
@@ -1692,8 +1700,9 @@
         State.Builder receivedState = State.newBuilder();
         FrameLayout rootLayout =
                 renderer(
-                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
-                                .setLoadActionListener(receivedState::mergeFrom))
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
                         .inflate();
 
         ShadowLooper.runUiThreadTasks();
@@ -1812,8 +1821,8 @@
 
         // Compute the mutation
         ViewGroupMutation mutation =
-                renderer.computeMutation(getRenderedMetadata(rootLayout),
-                        fingerprintedLayout(root2));
+                renderer.computeMutation(
+                        getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
         assertThat(mutation).isNotNull();
         assertThat(mutation.isNoOp()).isFalse();
 
@@ -1848,8 +1857,9 @@
                         .setSpacer(
                                 Spacer.newBuilder()
                                         .setWidth(
-                                                SpacerDimension.newBuilder().setLinearDimension(
-                                                        dp(spacerSize)).build()));
+                                                SpacerDimension.newBuilder()
+                                                        .setLinearDimension(dp(spacerSize))
+                                                        .build()));
 
         int parentHeight = 45;
         int parentWidth = 125;
@@ -1916,13 +1926,13 @@
 
         // Compute the mutation
         ViewGroupMutation mutation =
-                renderer.computeMutation(getRenderedMetadata(rootLayout),
-                        fingerprintedLayout(root2));
+                renderer.computeMutation(
+                        getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
         assertThat(mutation).isNotNull();
         assertThat(mutation.isNoOp()).isFalse();
 
         // Apply the mutation
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(rootLayout, mutation);
         shadowOf(getMainLooper()).idle();
         try {
@@ -1958,6 +1968,108 @@
     }
 
     @Test
+    @Config(minSdk = VERSION_CODES.Q)
+    public void inflateThenMutate_withClickableSizeChange_clickableModifier_extendClickTargetSize()
+    {
+        Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+        int parentSize = 50;
+        ContainerDimension parentBoxSize =
+                ContainerDimension.newBuilder().setLinearDimension(dp(parentSize)).build();
+        ContainerDimension childBoxSize =
+                ContainerDimension.newBuilder().setLinearDimension(dp(parentSize / 2f)).build();
+
+        Modifiers testModifiers1 =
+                Modifiers.newBuilder()
+                        .setClickable(Clickable.newBuilder().setOnClick(action).setId("foo1"))
+                        .build();
+
+        // Child box has a size smaller than the minimum clickable size, touch delegation is
+        // required.
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(parentBoxSize)
+                                        .setHeight(parentBoxSize)
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setBox(
+                                                                Box.newBuilder()
+                                                                        .setWidth(childBoxSize)
+                                                                        .setHeight(childBoxSize)
+                                                                        .setModifiers(
+                                                                                testModifiers1))))
+                        .build();
+
+        State.Builder receivedState = State.newBuilder();
+        Renderer renderer =
+                renderer(
+                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
+                                .setLoadActionListener(receivedState::mergeFrom));
+        FrameLayout rootLayout = renderer.inflate();
+        ViewGroup parent = (ViewGroup) rootLayout.getChildAt(0);
+        // Confirm the touch delegation has happened.
+        assertThat(parent.getTouchDelegate()).isNotNull();
+        // Dispatch a click event to the parent View within the expanded clickable area;
+        // it should trigger the LoadAction...
+        receivedState.clearLastClickableId();
+        dispatchTouchEvent(parent, 5, 5);
+        expect.that(receivedState.getLastClickableId()).isEqualTo("foo1");
+
+        // Produce a new layout with child box specifies its minimum clickable size, NO touch
+        // delegation is required.
+        Modifiers testModifiers2 =
+                Modifiers.newBuilder()
+                        .setClickable(
+                                Clickable.newBuilder()
+                                        .setOnClick(action)
+                                        .setId("foo2")
+                                        .setMinimumClickableWidth(dp(parentSize / 2f))
+                                        .setMinimumClickableHeight(dp(parentSize / 2f)))
+                        .build();
+        LayoutElement root2 =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(parentBoxSize)
+                                        .setHeight(parentBoxSize)
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setBox(
+                                                                Box.newBuilder()
+                                                                        .setWidth(childBoxSize)
+                                                                        .setHeight(childBoxSize)
+                                                                        .setModifiers(
+                                                                                testModifiers2))))
+                        .build();
+
+        // Compute the mutation
+        ViewGroupMutation mutation =
+                renderer.computeMutation(getRenderedMetadata(rootLayout),
+                        fingerprintedLayout(root2));
+        assertThat(mutation).isNotNull();
+        assertThat(mutation.isNoOp()).isFalse();
+
+        // Apply the mutation
+        boolean mutationResult = renderer.applyMutation(rootLayout, mutation);
+        assertThat(mutationResult).isTrue();
+
+        // Verify that the parent removed the touch delegation.
+        // Keep an empty touch delegate composite will lead to failure when calling
+        // {@link TouchDelegateComposite#getTouchDelegateInfo}
+        assertThat(parent.getTouchDelegate()).isNull();
+
+        // Dispatch a click event to the parent View within the expanded clickable area;
+        // it should no longer trigger the LoadAction.
+        receivedState.clearLastClickableId();
+        dispatchTouchEvent(parent, 5, 5);
+        expect.that(receivedState.getLastClickableId()).isEmpty();
+        View box = parent.getChildAt(0);
+        dispatchTouchEvent(box, 1, 1);
+        expect.that(receivedState.getLastClickableId()).isEqualTo("foo2");
+    }
+
+    @Test
     public void inflate_clickable_withoutRippleEffect_rippleDrawableNotAdded() throws IOException {
         final String textContentsWithRipple = "clickable with ripple";
         final String textContentsWithoutRipple = "clickable without ripple";
@@ -1993,15 +2105,15 @@
 
         FrameLayout rootLayout =
                 renderer(
-                        fingerprintedLayout(
-                                LayoutElement.newBuilder()
-                                        .setColumn(
-                                                Column.newBuilder()
-                                                        .addContents(textElementWithRipple)
-                                                        .addContents(
-                                                                textElementWithoutRipple)
-                                                        .build())
-                                        .build()))
+                                fingerprintedLayout(
+                                        LayoutElement.newBuilder()
+                                                .setColumn(
+                                                        Column.newBuilder()
+                                                                .addContents(textElementWithRipple)
+                                                                .addContents(
+                                                                        textElementWithoutRipple)
+                                                                .build())
+                                                .build()))
                         .inflate();
 
         // Column
@@ -2025,6 +2137,49 @@
     }
 
     @Test
+    public void inflate_hiddenModifier_inhibitsClicks() {
+        final String textContents = "I am a clickable";
+
+        Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setText(
+                                                                createTextWithVisibility(
+                                                                        textContents,
+                                                                        "back",
+                                                                        action,
+                                                                        true)))
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setText(
+                                                                createTextWithVisibility(
+                                                                        textContents,
+                                                                        "front",
+                                                                        action,
+                                                                        false))))
+                        .build();
+
+        State.Builder receivedState = State.newBuilder();
+        FrameLayout rootLayout =
+                renderer(
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
+                        .inflate();
+
+        // Try to tap the stacked clickables.
+        dispatchTouchEvent(rootLayout, 5f, 5f);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        expect.that(receivedState.getLastClickableId()).isEqualTo("back");
+    }
+
+    @Test
     public void inflate_arc_withLineDrawnWithArcTo() {
         LayoutElement root =
                 LayoutElement.newBuilder()
@@ -2151,8 +2306,8 @@
     @Test
     public void inflate_arc_withText_autoSize_notSet() {
         int lastSize = 12;
-        FontStyle.Builder style = FontStyle.newBuilder()
-                .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
+        FontStyle.Builder style =
+                FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {10, 20, lastSize}));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setArc(
@@ -2889,12 +3044,13 @@
                         .setFontStyle(FontStyle.newBuilder().addSize(sp(16)))
                         .setMaxLines(Int32Prop.newBuilder().setValue(6))
                         .setOverflow(
-                                TextOverflowProp.newBuilder().setValue(
-                                        TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+                                TextOverflowProp.newBuilder()
+                                        .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
         Layout layout1 =
                 fingerprintedLayout(
                         LayoutElement.newBuilder()
-                                .setBox(buildFixedSizeBoxWIthText(text1)).build());
+                                .setBox(buildFixedSizeBoxWIthText(text1))
+                                .build());
 
         Text.Builder text2 =
                 Text.newBuilder()
@@ -2904,18 +3060,19 @@
                         .setFontStyle(FontStyle.newBuilder().addSize(sp(4)))
                         .setMaxLines(Int32Prop.newBuilder().setValue(6))
                         .setOverflow(
-                                TextOverflowProp.newBuilder().setValue(
-                                        TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+                                TextOverflowProp.newBuilder()
+                                        .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
         Layout layout2 =
                 fingerprintedLayout(
                         LayoutElement.newBuilder()
-                                .setBox(buildFixedSizeBoxWIthText(text2)).build());
+                                .setBox(buildFixedSizeBoxWIthText(text2))
+                                .build());
 
         // Initial layout.
         Renderer renderer = renderer(layout1);
         ViewGroup inflatedViewParent = renderer.inflate();
-        TextView textView1 = (TextView) ((ViewGroup) inflatedViewParent
-                .getChildAt(0)).getChildAt(0);
+        TextView textView1 =
+                (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
 
         // Apply the mutation.
         ViewGroupMutation mutation =
@@ -2926,8 +3083,8 @@
         assertThat(mutationResult).isTrue();
 
         // This contains layout after the mutation.
-        TextView textView2 = (TextView) ((ViewGroup) inflatedViewParent
-                .getChildAt(0)).getChildAt(0);
+        TextView textView2 =
+                (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
 
         expect.that(textView1.getEllipsize()).isEqualTo(TruncateAt.END);
         expect.that(textView1.getMaxLines()).isEqualTo(2);
@@ -3028,7 +3185,7 @@
     @Test
     public void inflate_textView_autosize_set() {
         String text = "Test text";
-        int[] presetSizes = new int[]{12, 20, 10};
+        int[] presetSizes = new int[] {12, 20, 10};
         List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
 
         LayoutElement textElement =
@@ -3036,16 +3193,16 @@
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3076,16 +3233,16 @@
                                 Text.newBuilder()
                                         .setText(string(text))
                                         .setMaxLines(Int32Prop.newBuilder().setValue(4))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3099,23 +3256,23 @@
     public void inflate_textView_autosize_notSet() {
         String text = "Test text";
         int size = 24;
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {size});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3129,23 +3286,23 @@
     public void inflate_textView_autosize_setDynamic_noop() {
         String text = "Test text";
         int lastSize = 24;
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {10, 30, lastSize});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(dynamicString(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ArrayList<View> textChildren = new ArrayList<>();
@@ -3159,23 +3316,23 @@
     @Test
     public void inflate_textView_autosize_wrongSizes_noop() {
         String text = "Test text";
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {0, -2, 0});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ArrayList<View> textChildren = new ArrayList<>();
@@ -3219,8 +3376,8 @@
     public void inflate_spantext_ignoresMultipleSizes() {
         String text = "Test text";
         int firstSize = 12;
-        FontStyle.Builder style = FontStyle.newBuilder()
-                .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
+        FontStyle.Builder style =
+                FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {firstSize, 10, 20}));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setSpannable(
@@ -4780,7 +4937,7 @@
 
         boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) {
             try {
-                ListenableFuture<Void> applyMutationFuture =
+                ListenableFuture<RenderingArtifact> applyMutationFuture =
                         mRenderer.applyMutation(parent, mutation);
                 shadowOf(Looper.getMainLooper()).idle();
                 applyMutationFuture.get();
@@ -5000,33 +5157,25 @@
 
         LayoutElement image = buildImage(protoResId, 30, 30);
 
-
-        BoolProp.Builder stateBoolPropBuilder = BoolProp
-                .newBuilder()
-                .setValue(
-                        true)
-                .setDynamicValue(
-                        DynamicBool
-                                .newBuilder()
-                                .setStateSource(
-                                        StateBoolSource
-                                                .newBuilder()
-                                                .setSourceKey(
-                                                        boolKey)));
-        LayoutElement.Builder boxBuilder = LayoutElement.newBuilder()
-                .setBox(
-                        Box.newBuilder()
-                                .addContents(image)
-                                .setModifiers(
-                                        Modifiers
-                                                .newBuilder()
-                                                .setHidden(stateBoolPropBuilder)));
+        BoolProp.Builder stateBoolPropBuilder =
+                BoolProp.newBuilder()
+                        .setValue(true)
+                        .setDynamicValue(
+                                DynamicBool.newBuilder()
+                                        .setStateSource(
+                                                StateBoolSource.newBuilder()
+                                                        .setSourceKey(boolKey)));
+        LayoutElement.Builder boxBuilder =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .addContents(image)
+                                        .setModifiers(
+                                                Modifiers.newBuilder()
+                                                        .setHidden(stateBoolPropBuilder)));
         LayoutElement root =
                 LayoutElement.newBuilder()
-                        .setRow(
-                                Row.newBuilder()
-                                        .addContents(boxBuilder)
-                                        .addContents(image))
+                        .setRow(Row.newBuilder().addContents(boxBuilder).addContents(image))
                         .build();
 
         FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
@@ -5059,7 +5208,8 @@
         assertThat(secondImage.getLeft()).isEqualTo(secondImageLeft);
     }
 
-    @Test   public void inflate_box_withVisibleModifier() {
+    @Test
+    public void inflate_box_withVisibleModifier() {
         final String protoResId = "android";
         final String boolKey = "bool-key";
 
@@ -5226,20 +5376,18 @@
                 ContainerDimension.newBuilder().setLinearDimension(dp(100.f).build()).build();
         ContainerDimension innerBoxSize =
                 ContainerDimension.newBuilder().setLinearDimension(dp(60.f).build()).build();
-        Box.Builder boxBuilder = Box.newBuilder()
-                .setWidth(expand())
-                .setHeight(wrap())
-                .setModifiers(
-                        Modifiers.newBuilder()
-                                .setTransformation(
-                                        transformation)
-                                .build())
-                .addContents(
-                        LayoutElement.newBuilder()
-                                .setBox(
-                                        Box.newBuilder()
-                                                .setWidth(innerBoxSize)
-                                                .setHeight(innerBoxSize)));
+        Box.Builder boxBuilder =
+                Box.newBuilder()
+                        .setWidth(expand())
+                        .setHeight(wrap())
+                        .setModifiers(
+                                Modifiers.newBuilder().setTransformation(transformation).build())
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setBox(
+                                                Box.newBuilder()
+                                                        .setWidth(innerBoxSize)
+                                                        .setHeight(innerBoxSize)));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setBox(
@@ -5657,7 +5805,7 @@
                 renderer.computeMutation(
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(textFadeIn("World")));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         // Idle for running code for starting animations.
@@ -5689,7 +5837,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 0)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5715,7 +5863,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5738,7 +5886,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5764,7 +5912,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5792,7 +5940,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
         shadowOf(getMainLooper()).idle();
 
@@ -5821,7 +5969,7 @@
                         fingerprintedLayout(
                                 getMultipleTextElementWithExitAnimation(
                                         Arrays.asList("Hello"), /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5850,7 +5998,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5863,7 +6011,7 @@
                                 getTextElementWithExitAnimation(
                                         "Second mutation", /* iterations= */ 10)));
 
-        ListenableFuture<Void> applySecondMutationFuture =
+        ListenableFuture<RenderingArtifact> applySecondMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
 
         // the previous mutation should be finished
@@ -6222,9 +6370,12 @@
 
     private static Spacer.Builder buildExpandedSpacer(int widthWeight, int heightWeight) {
         return Spacer.newBuilder()
-                .setWidth(SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(widthWeight)))
+                .setWidth(
+                        SpacerDimension.newBuilder()
+                                .setExpandedDimension(expandWithWeight(widthWeight)))
                 .setHeight(
-                        SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(heightWeight)));
+                        SpacerDimension.newBuilder()
+                                .setExpandedDimension(expandWithWeight(heightWeight)));
     }
 
     private static ExpandedDimensionProp expandWithWeight(int weight) {
@@ -6255,4 +6406,15 @@
                                         .addContents(LayoutElement.newBuilder().setSpacer(spacer)))
                         .build());
     }
+
+    private static Text createTextWithVisibility(
+            String text, String id, Action action, boolean visibility) {
+        return Text.newBuilder()
+                .setText(string(text))
+                .setModifiers(
+                        Modifiers.newBuilder()
+                                .setVisible(BoolProp.newBuilder().setValue(visibility))
+                                .setClickable(Clickable.newBuilder().setId(id).setOnClick(action)))
+                .build();
+    }
 }
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 4a563e0..2d88c40 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -49,12 +49,13 @@
  *
  * To modify the user style, you should call [toMutableUserStyle] and construct a new [UserStyle]
  * instance with [MutableUserStyle.toUserStyle].
- *
+ */
+public class UserStyle
+/**
  * @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
  * @param copySelectedOptions Whether to create a copy of the provided [selectedOptions]. If
  *   `false`, no mutable copy of the [selectedOptions] map should be retained outside this class.
  */
-public class UserStyle
 private constructor(
     selectedOptions: Map<UserStyleSetting, UserStyleSetting.Option>,
     copySelectedOptions: Boolean
@@ -70,6 +71,8 @@
      *
      * A copy of the [selectedOptions] map will be created, so that changed to the map will not be
      * reflected by this object.
+     *
+     * @param selectedOptions The [UserStyleSetting.Option] selected for each [UserStyleSetting]
      */
     public constructor(
         selectedOptions: Map<UserStyleSetting, UserStyleSetting.Option>
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
index 5d3c6df..93e468f 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlot.kt
@@ -131,6 +131,7 @@
      * @param boundsType The [ComplicationSlotBoundsTypeIntDef] of the complication
      * @param zonedDateTime The [ZonedDateTime] to render the highlight with
      * @param color The color to render the highlight with
+     * @param boundingArc Optional [BoundingArc] defining the geometry of an edge complication
      */
     @ComplicationExperimental
     public fun drawHighlight(
@@ -633,8 +634,12 @@
          * @param defaultDataSourcePolicy The [DefaultComplicationDataSourcePolicy] used to select
          *   the initial complication data source when the watch is first installed.
          * @param bounds The complication's [ComplicationSlotBounds]. Its likely the bounding rect
-         *   will be much larger than the complication and shouldn't directly be used for hit
+         *   will have a much larger area than [boundingArc] and shouldn't directly be used for hit
          *   testing.
+         * @param boundingArc The [BoundingArc] defining the geometry of the edge complication.
+         * @param complicationTapFilter The [ComplicationTapFilter] used to determine whether or not
+         *   a tap hit the complication. The default [ComplicationTapFilter] uses [boundingArc] to
+         *   perform hit testing.
          */
         @JvmStatic
         @JvmOverloads
diff --git a/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java b/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
index 3aaad88..cb31832 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/CookieManagerCompat.java
@@ -39,6 +39,7 @@
      * <a href="https://httpwg.org/specs/rfc6265.html#sane-set-cookie-syntax">the RFC6265 spec.</a>
      *  eg. "name=value; domain=.example.com; path=/"
      *
+     * @param cookieManager The CookieManager instance to get info from.
      * @param url the URL for which the API retrieves all available cookies.
      * @return the cookies as a list of strings.
      */
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 46d466f..5c15f3a 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -119,6 +119,7 @@
      * {@link WebViewFeature#isFeatureSupported(String)}
      * returns true for {@link WebViewFeature#SAFE_BROWSING_ENABLE}.
      *
+     * @param settings The WebSettings object to update.
      * @param enabled Whether Safe Browsing is enabled.
      */
     @RequiresFeature(name = WebViewFeature.SAFE_BROWSING_ENABLE,
@@ -179,6 +180,7 @@
      * {@link WebViewFeature#isFeatureSupported(String)}
      * returns true for {@link WebViewFeature#DISABLED_ACTION_MODE_MENU_ITEMS}.
      *
+     * @param settings The WebSettings object to update.
      * @param menuItems an integer field flag for the menu items to be disabled.
      */
     @RequiresFeature(name = WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS,
@@ -425,6 +427,7 @@
      * is created.
      *
      * <p>
+     * @param settings The WebSettings object to update.
      * @param allow allow algorithmic darkening or not.
      *
      */
@@ -593,6 +596,7 @@
      * {@link WebViewFeature#isFeatureSupported(String)}
      * returns true for {@link WebViewFeature#ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY}.
      *
+     * @param settings The WebSettings object to update.
      * @param enabled Whether EnterpriseAuthenticationAppLinkPolicy should be enabled.
      */
     @RequiresFeature(name = WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
index d3f6250..de2c728 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewCompat.java
@@ -160,6 +160,7 @@
      * {@link WebViewFeature#isFeatureSupported(String)}
      * returns true for {@link WebViewFeature#VISUAL_STATE_CALLBACK}.
      *
+     * @param webview The WebView to post to.
      * @param requestId An id that will be returned in the callback to allow callers to match
      *                  requests with callbacks.
      * @param callback  The callback to be invoked.
@@ -497,6 +498,7 @@
      * }
      * </pre
      *
+     * @param webview The WebView to post to.
      * @param message the WebMessage
      * @param targetOrigin the target origin.
      */
@@ -760,6 +762,7 @@
      * This method should only be called if {@link WebViewFeature#isFeatureSupported(String)}
      * returns true for {@link WebViewFeature#WEB_MESSAGE_LISTENER}.
      *
+     * @param webview The WebView object to remove from.
      * @param jsObjectName The JavaScript object's name that was previously passed to {@link
      *         #addWebMessageListener(WebView, String, Set, WebMessageListener)}.
      *