Merge "Add helpers to CallStats to map CallTypes to AppSearchManagerService APIs" into androidx-platform-dev
diff --git a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
index a58ae85..b1fcd9c 100644
--- a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
+++ b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateCall.kt
@@ -38,47 +38,7 @@
 
 private const val CAPABILITY_NAME: String = "actions.intent.CREATE_CALL"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(CreateCall.Arguments::class.java, CreateCall.Arguments::Builder)
-        .setOutput(CreateCall.Output::class.java)
-        .bindOptionalParameter(
-            "call.callFormat",
-            { properties ->
-                Optional.ofNullable(
-                    properties[CreateCall.PropertyMapStrings.CALL_FORMAT.key]
-                        as Property<Call.CanonicalValue.CallFormat>
-                )
-            },
-            CreateCall.Arguments.Builder::setCallFormat,
-            TypeConverters.CALL_FORMAT_PARAM_VALUE_CONVERTER,
-            TypeConverters.CALL_FORMAT_ENTITY_CONVERTER
-        )
-        .bindRepeatedParameter(
-            "call.participant",
-            { properties ->
-                Optional.ofNullable(
-                    properties[CreateCall.PropertyMapStrings.PARTICIPANT.key]
-                        as Property<Participant>
-                )
-            },
-            CreateCall.Arguments.Builder::setParticipantList,
-            ParticipantValue.PARAM_VALUE_CONVERTER,
-            EntityConverter.of(PARTICIPANT_TYPE_SPEC)
-        )
-        .bindOptionalOutput(
-            "call",
-            { output -> Optional.ofNullable(output.call) },
-            ParamValueConverter.of(CALL_TYPE_SPEC)::toParamValue
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            CreateCall.ExecutionStatus::toParamValue
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.CREATE_CALL */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class CreateCall private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -216,4 +176,47 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "call.callFormat",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.CALL_FORMAT.key]
+                                as Property<Call.CanonicalValue.CallFormat>
+                        )
+                    },
+                    Arguments.Builder::setCallFormat,
+                    TypeConverters.CALL_FORMAT_PARAM_VALUE_CONVERTER,
+                    TypeConverters.CALL_FORMAT_ENTITY_CONVERTER
+                )
+                .bindRepeatedParameter(
+                    "call.participant",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.PARTICIPANT.key]
+                                as Property<Participant>
+                        )
+                    },
+                    Arguments.Builder::setParticipantList,
+                    ParticipantValue.PARAM_VALUE_CONVERTER,
+                    EntityConverter.of(PARTICIPANT_TYPE_SPEC)
+                )
+                .bindOptionalOutput(
+                    "call",
+                    { output -> Optional.ofNullable(output.call) },
+                    ParamValueConverter.of(CALL_TYPE_SPEC)::toParamValue
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
index 806e5ac..61ceb00 100644
--- a/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
+++ b/appactions/interaction/interaction-capabilities-communication/src/main/java/androidx/appactions/interaction/capabilities/communication/CreateMessage.kt
@@ -39,47 +39,7 @@
 
 private const val CAPABILITY_NAME: String = "actions.intent.CREATE_MESSAGE"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(CreateMessage.Arguments::class.java, CreateMessage.Arguments::Builder)
-        .setOutput(CreateMessage.Output::class.java)
-        .bindRepeatedParameter(
-            "message.recipient",
-            { properties ->
-                Optional.ofNullable(
-                    properties[CreateMessage.PropertyMapStrings.RECIPIENT.key]
-                        as Property<Recipient>
-                )
-            },
-            CreateMessage.Arguments.Builder::setRecipientList,
-            RecipientValue.PARAM_VALUE_CONVERTER,
-            EntityConverter.of(RECIPIENT_TYPE_SPEC)
-        )
-        .bindOptionalParameter(
-            "message.text",
-            { properties ->
-                Optional.ofNullable(
-                    properties[CreateMessage.PropertyMapStrings.MESSAGE_TEXT.key]
-                        as Property<StringValue>
-                )
-            },
-            CreateMessage.Arguments.Builder::setMessageText,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "message",
-            { output -> Optional.ofNullable(output.message) },
-            ParamValueConverter.of(MESSAGE_TYPE_SPEC)::toParamValue
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            CreateMessage.ExecutionStatus::toParamValue
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.CREATE_MESSAGE */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class CreateMessage private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -218,4 +178,47 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindRepeatedParameter(
+                    "message.recipient",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.RECIPIENT.key]
+                                as Property<Recipient>
+                        )
+                    },
+                    Arguments.Builder::setRecipientList,
+                    RecipientValue.PARAM_VALUE_CONVERTER,
+                    EntityConverter.of(RECIPIENT_TYPE_SPEC)
+                )
+                .bindOptionalParameter(
+                    "message.text",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.MESSAGE_TEXT.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setMessageText,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "message",
+                    { output -> Optional.ofNullable(output.message) },
+                    ParamValueConverter.of(MESSAGE_TYPE_SPEC)::toParamValue
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
index 3b6f3d9..d983859 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/ErrorStatusInternal.java
@@ -31,7 +31,8 @@
     SYNC_REQUEST_FAILURE(6),
     CONFIRMATION_REQUEST_FAILURE(7),
     TOUCH_EVENT_REQUEST_FAILURE(8),
-    EXTERNAL_EXCEPTION(9);
+    EXTERNAL_EXCEPTION(9),
+    SESSION_ALREADY_DESTROYED(10);
 
     private final int mCode;
 
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
index 56cf334..1f577f56 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskCapabilitySession.kt
@@ -51,7 +51,6 @@
     // single-turn capability does not have status
     override val isActive: Boolean
         get() = when (sessionOrchestrator.status) {
-            TaskOrchestrator.Status.COMPLETED,
             TaskOrchestrator.Status.DESTROYED -> false
             else -> true
         }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
index 8b71429..790a750 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskHandler.kt
@@ -50,7 +50,7 @@
             mutableTaskParamMap[paramName] =
                 TaskParamBinding(
                     paramName,
-                    GROUND_IF_NO_IDENTIFIER,
+                    GROUND_NEVER,
                     GenericResolverInternal.fromInventoryListener(listener),
                     converter,
                     null,
@@ -66,7 +66,7 @@
             mutableTaskParamMap[paramName] =
                 TaskParamBinding(
                     paramName,
-                    GROUND_IF_NO_IDENTIFIER,
+                    GROUND_NEVER,
                     GenericResolverInternal.fromInventoryListListener(listener),
                     converter,
                     null,
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
index 48e73b6..5b51ac3 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/task/TaskOrchestrator.kt
@@ -68,7 +68,6 @@
     internal enum class Status {
         UNINITIATED,
         IN_PROGRESS,
-        COMPLETED,
         DESTROYED,
     }
     /**
@@ -147,7 +146,14 @@
             inProgress = true
         }
         try {
-            if (updateRequest.assistantRequest != null) {
+            if (status == Status.DESTROYED) {
+                if (updateRequest.assistantRequest != null) {
+                    FulfillmentResult(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+                        .applyToCallback(updateRequest.assistantRequest.callbackInternal)
+                } else if (updateRequest.touchEventRequest != null && touchEventCallback != null) {
+                    touchEventCallback!!.onError(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+                }
+            } else if (updateRequest.assistantRequest != null) {
                 processAssistantUpdateRequest(updateRequest.assistantRequest)
             } else if (updateRequest.touchEventRequest != null) {
                 processTouchEventUpdateRequest(updateRequest.touchEventRequest)
@@ -187,12 +193,12 @@
 
                 FulfillmentRequest.Fulfillment.Type.SYNC -> handleSync(argumentsWrapper)
                 FulfillmentRequest.Fulfillment.Type.CONFIRM -> handleConfirm()
-                FulfillmentRequest.Fulfillment.Type.CANCEL,
-                FulfillmentRequest.Fulfillment.Type.TERMINATE,
+                FulfillmentRequest.Fulfillment.Type.CANCEL
                 -> {
                     terminate()
                     FulfillmentResult(FulfillmentResponse.getDefaultInstance())
                 }
+                else -> FulfillmentResult(ErrorStatusInternal.INVALID_REQUEST)
             }
         }
         fulfillmentResult.applyToCallback(callback)
@@ -254,7 +260,6 @@
         }
     }
 
-    // TODO: add cleanup logic if any
     internal fun terminate() {
         externalSession.onDestroy()
         status = Status.DESTROYED
@@ -471,7 +476,7 @@
         val result = invokeExternalSuspendBlock("onExecute") {
             externalSession.onExecute(actionSpec.buildArguments(finalArguments))
         }
-        status = Status.COMPLETED
+        terminate()
         val fulfillmentResponse =
             FulfillmentResponse.newBuilder().setStartDictation(result.shouldStartDictation)
         convertToExecutionOutput(result)?.let { fulfillmentResponse.executionOutput = it }
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
index ba5b322..18c1cab 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.kt
@@ -19,6 +19,7 @@
 import androidx.appactions.builtintypes.experimental.types.ListItem
 import androidx.appactions.interaction.capabilities.core.AppEntityListener
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.ConfirmationOutput
 import androidx.appactions.interaction.capabilities.core.EntitySearchResult
 import androidx.appactions.interaction.capabilities.core.ExecutionResult
 import androidx.appactions.interaction.capabilities.core.HostProperties
@@ -58,6 +59,7 @@
 import androidx.appactions.interaction.proto.DisambiguationData
 import androidx.appactions.interaction.proto.Entity
 import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.CANCEL
+import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type
 import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.SYNC
 import androidx.appactions.interaction.proto.FulfillmentRequest.Fulfillment.Type.UNKNOWN_TYPE
 import androidx.appactions.interaction.proto.FulfillmentResponse.StructuredOutput
@@ -431,7 +433,6 @@
             buildRequestArgs(CANCEL),
             callback3
         )
-        assertThat(callback3.receiveResponse().fulfillmentResponse).isNotNull()
         assertThat(session.isActive).isFalse()
     }
 
@@ -959,6 +960,158 @@
         assertThat(callback.receiveResponse().fulfillmentResponse!!.startDictation).isTrue()
     }
 
+    @Test
+    @kotlin.Throws(Exception::class)
+    fun fulfillmentType_finalSync_stateCleared() {
+        val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+            { _ ->
+                object : ExecutionSession {
+                    override suspend fun onExecute(arguments: Arguments) =
+                        ExecutionResult.Builder<Output>().build()
+                }
+            }
+        val property = mapOf(
+            "required" to Property.Builder<StringValue>().setRequired(true).build()
+        )
+        val capability: Capability =
+            createCapability(
+                property,
+                sessionFactory = sessionFactory,
+                sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+                sessionUpdaterSupplier = ::EmptyTaskUpdater,
+            )
+        val session = capability.createSession(fakeSessionId, hostProperties)
+
+        // TURN 1. Not providing all the required slots in the SYNC Request
+        val callback = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(SYNC),
+            callback,
+        )
+        assertThat(callback.receiveResponse()).isNotNull()
+        assertThat(getCurrentValues("required", session.state!!)).isEmpty()
+        assertThat(session.isActive).isEqualTo(true)
+
+        // TURN 2. Providing the required slots so that the task completes and the state gets cleared
+        val callback2 = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(SYNC,
+                "required",
+                ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+            ),
+            callback2,
+        )
+        assertThat(callback2.receiveResponse().fulfillmentResponse).isNotNull()
+        assertThat(session.isActive).isEqualTo(false)
+    }
+
+    @Test
+    @kotlin.Throws(Exception::class)
+    @Suppress("DEPRECATION") // TODO(b/279830425) implement tryExecute (INTENT_CONFIRMED can be used instead)
+    fun fulfillmentType_syncWithConfirmation_stateClearedAfterConfirmation() {
+        val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+            { _ ->
+                object : ExecutionSession {
+                    override suspend fun onExecute(arguments: Arguments) =
+                        ExecutionResult.Builder<Output>().build()
+                }
+            }
+        var onReadyToConfirm =
+             object : OnReadyToConfirmListenerInternal<Confirmation> {
+                override suspend fun onReadyToConfirm(args: Map<String, List<ParamValue>>):
+                    ConfirmationOutput<Confirmation> {
+                    return ConfirmationOutput.Builder<Confirmation>()
+                            .setConfirmation(Confirmation.builder().setOptionalStringField("bar")
+                                .build())
+                            .build()
+                }
+            }
+        val property = mapOf(
+            "required" to Property.Builder<StringValue>().setRequired(true).build()
+        )
+        val capability: Capability =
+            createCapability(
+                property,
+                sessionFactory = sessionFactory,
+                sessionBridge = SessionBridge {
+                                    TaskHandler.Builder<Confirmation>()
+                                        .setOnReadyToConfirmListenerInternal(onReadyToConfirm)
+                                        .build() },
+                sessionUpdaterSupplier = ::EmptyTaskUpdater,
+            )
+        val session = capability.createSession(fakeSessionId, hostProperties)
+
+        // TURN 1. Providing all the required slots in the SYNC Request
+        val callback = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(SYNC,
+                "required",
+                ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+            ),
+            callback,
+        )
+        assertThat(callback.receiveResponse()).isNotNull()
+        assertThat(session.isActive).isEqualTo(true)
+
+        // Sending the confirmation request. After the confirm request, the session should not be
+        // active
+        val callback2 = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(Type.CONFIRM),
+            callback2
+        )
+
+        assertThat(callback2.receiveResponse().fulfillmentResponse).isNotNull()
+        assertThat(session.isActive).isEqualTo(false)
+    }
+
+    @Test
+    fun fulfillmentRequest_whenStatusDestroyed_errorReported() {
+        val sessionFactory: (hostProperties: HostProperties?) -> ExecutionSession =
+            { _ ->
+                object : ExecutionSession {
+                    override suspend fun onExecute(arguments: Arguments) =
+                        ExecutionResult.Builder<Output>().build()
+                }
+            }
+        val property = mapOf(
+            "required" to Property.Builder<StringValue>().setRequired(true).build()
+        )
+        val capability: Capability =
+            createCapability(
+                property,
+                sessionFactory = sessionFactory,
+                sessionBridge = SessionBridge { TaskHandler.Builder<Confirmation>().build() },
+                sessionUpdaterSupplier = ::EmptyTaskUpdater,
+            )
+        val session = capability.createSession(fakeSessionId, hostProperties)
+
+        // TURN 1. Providing the required slots so that the task completes and the state gets cleared
+        val callback = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(SYNC,
+                "required",
+                ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+            ),
+            callback,
+        )
+        assertThat(callback.receiveResponse().fulfillmentResponse).isNotNull()
+        assertThat(session.isActive).isEqualTo(false)
+
+        // TURN 2. Trying to sync after the session is destroyed
+        val callback2 = FakeCallbackInternal()
+        session.execute(
+            buildRequestArgs(SYNC,
+                "required",
+                ParamValue.newBuilder().setIdentifier("foo").setStringValue("foo").build()
+            ),
+            callback2,
+        )
+        assertThat(session.isActive).isEqualTo(false)
+        assertThat(callback2.receiveResponse().errorStatus)
+            .isEqualTo(ErrorStatusInternal.SESSION_ALREADY_DESTROYED)
+    }
+
     /**
      * an implementation of Capability.Builder using Argument. Output, etc. defined under
      * testing/spec
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
index d530c21..eeae92b 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetExerciseObservation.kt
@@ -26,44 +26,9 @@
 import java.time.LocalTime
 import java.util.Optional
 
-/** GetExerciseObservation.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.GET_EXERCISE_OBSERVATION"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(
-            GetExerciseObservation.Arguments::class.java,
-            GetExerciseObservation.Arguments::Builder
-        )
-        .setOutput(GetExerciseObservation.Output::class.java)
-        .bindOptionalParameter(
-            "exerciseObservation.startTime",
-            { properties ->
-                Optional.ofNullable(
-                    properties[GetExerciseObservation.PropertyMapStrings.START_TIME.key]
-                        as Property<LocalTime>
-                )
-            },
-            GetExerciseObservation.Arguments.Builder::setStartTime,
-            TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
-            TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
-        )
-        .bindOptionalParameter(
-            "exerciseObservation.endTime",
-            { properties ->
-                Optional.ofNullable(
-                    properties[GetExerciseObservation.PropertyMapStrings.END_TIME.key]
-                        as Property<LocalTime>
-                )
-            },
-            GetExerciseObservation.Arguments.Builder::setEndTime,
-            TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
-            TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.GET_EXERCISE_OBSERVATION */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class GetExerciseObservation private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -134,4 +99,41 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(
+                    Arguments::class.java,
+                    Arguments::Builder
+                )
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "exerciseObservation.startTime",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.START_TIME.key]
+                                as Property<LocalTime>
+                        )
+                    },
+                    Arguments.Builder::setStartTime,
+                    TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+                    TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+                )
+                .bindOptionalParameter(
+                    "exerciseObservation.endTime",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.END_TIME.key]
+                                as Property<LocalTime>
+                        )
+                    },
+                    Arguments.Builder::setEndTime,
+                    TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+                    TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
index c7482e1..cfacf6e 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/GetHealthObservation.kt
@@ -26,46 +26,12 @@
 import java.time.LocalTime
 import java.util.Optional
 
-/** GetHealthObservation.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.GET_HEALTH_OBSERVATION"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(
-            GetHealthObservation.Arguments::class.java,
-            GetHealthObservation.Arguments::Builder
-        )
-        .setOutput(GetHealthObservation.Output::class.java)
-        .bindOptionalParameter(
-            "healthObservation.startTime",
-            { properties ->
-                Optional.ofNullable(
-                    properties[GetHealthObservation.PropertyMapStrings.START_TIME.key]
-                        as Property<LocalTime>
-                )
-            },
-            GetHealthObservation.Arguments.Builder::setStartTime,
-            TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
-            TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
-        )
-        .bindOptionalParameter(
-            "healthObservation.endTime",
-            { properties ->
-                Optional.ofNullable(
-                    properties[GetHealthObservation.PropertyMapStrings.END_TIME.key]
-                        as Property<LocalTime>
-                )
-            },
-            GetHealthObservation.Arguments.Builder::setEndTime,
-            TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
-            TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.GET_HEALTH_OBSERVATION */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class GetHealthObservation private constructor() {
+
     internal enum class PropertyMapStrings(val key: String) {
         START_TIME("healthObservation.startTime"),
         END_TIME("healthObservation.endTime"),
@@ -138,4 +104,41 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(
+                    Arguments::class.java,
+                    Arguments::Builder
+                )
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "healthObservation.startTime",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.START_TIME.key]
+                                as Property<LocalTime>
+                        )
+                    },
+                    Arguments.Builder::setStartTime,
+                    TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+                    TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+                )
+                .bindOptionalParameter(
+                    "healthObservation.endTime",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.END_TIME.key]
+                                as Property<LocalTime>
+                        )
+                    },
+                    Arguments.Builder::setEndTime,
+                    TypeConverters.LOCAL_TIME_PARAM_VALUE_CONVERTER,
+                    TypeConverters.LOCAL_TIME_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
index f04ceee..f8b68c1 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/PauseExercise.kt
@@ -26,29 +26,9 @@
 import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import java.util.Optional
 
-/** PauseExercise.kt in interaction-capabilities-fitness */
 private const val CAPABILITY_NAME = "actions.intent.PAUSE_EXERCISE"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(PauseExercise.Arguments::class.java, PauseExercise.Arguments::Builder)
-        .setOutput(PauseExercise.Output::class.java)
-        .bindOptionalParameter(
-            "exercise.name",
-            { properties ->
-                Optional.ofNullable(
-                    properties[PauseExercise.PropertyMapStrings.NAME.key]
-                        as Property<StringValue>
-                )
-            },
-            PauseExercise.Arguments.Builder::setName,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.PAUSE_EXERCISE */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class PauseExercise private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -110,4 +90,26 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "exercise.name",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.NAME.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setName,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
index 1881a7c..a154fc2 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/ResumeExercise.kt
@@ -26,29 +26,9 @@
 import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import java.util.Optional
 
-/** ResumeExercise.kt in interaction-capabilities-fitness */
 private const val CAPABILITY_NAME = "actions.intent.RESUME_EXERCISE"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(ResumeExercise.Arguments::class.java, ResumeExercise.Arguments::Builder)
-        .setOutput(ResumeExercise.Output::class.java)
-        .bindOptionalParameter(
-            "exercise.name",
-            { properties ->
-                Optional.ofNullable(
-                    properties[ResumeExercise.PropertyMapStrings.NAME.key]
-                        as Property<StringValue>
-                )
-            },
-            ResumeExercise.Arguments.Builder::setName,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.RESUME_EXERCISE */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class ResumeExercise private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -110,4 +90,26 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "exercise.name",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.NAME.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setName,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
index 0847d33..adb2749 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StartExercise.kt
@@ -27,41 +27,9 @@
 import java.time.Duration
 import java.util.Optional
 
-/** StartExercise.kt in interaction-capabilities-fitness */
 private const val CAPABILITY_NAME = "actions.intent.START_EXERCISE"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StartExercise.Arguments::class.java, StartExercise.Arguments::Builder)
-        .setOutput(StartExercise.Output::class.java)
-        .bindOptionalParameter(
-            "exercise.duration",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartExercise.PropertyMapStrings.DURATION.key]
-                        as Property<Duration>
-                )
-            },
-            StartExercise.Arguments.Builder::setDuration,
-            TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
-            TypeConverters.DURATION_ENTITY_CONVERTER
-        )
-        .bindOptionalParameter(
-            "exercise.name",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartExercise.PropertyMapStrings.NAME.key]
-                        as Property<StringValue>
-                )
-            },
-            StartExercise.Arguments.Builder::setName,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.START_EXERCISE */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class StartExercise private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -136,4 +104,38 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(StartExercise.Arguments::class.java, StartExercise.Arguments::Builder)
+                .setOutput(StartExercise.Output::class.java)
+                .bindOptionalParameter(
+                    "exercise.duration",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[StartExercise.PropertyMapStrings.DURATION.key]
+                                as Property<Duration>
+                        )
+                    },
+                    StartExercise.Arguments.Builder::setDuration,
+                    TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+                    TypeConverters.DURATION_ENTITY_CONVERTER
+                )
+                .bindOptionalParameter(
+                    "exercise.name",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[StartExercise.PropertyMapStrings.NAME.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    StartExercise.Arguments.Builder::setName,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
index 18efde0..5bbc5eb7 100644
--- a/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
+++ b/appactions/interaction/interaction-capabilities-fitness/src/main/java/androidx/appactions/interaction/capabilities/fitness/fitness/StopExercise.kt
@@ -26,29 +26,9 @@
 import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import java.util.Optional
 
-/** StopExercise.kt in interaction-capabilities-fitness */
-private const val CAPABILITY_NAME = "actions.intent.PAUSE_EXERCISE"
+private const val CAPABILITY_NAME = "actions.intent.STOP_EXERCISE"
 
-// TODO(b/273602015): Update to use Name property from builtintype library.
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StopExercise.Arguments::class.java, StopExercise.Arguments::Builder)
-        .setOutput(StopExercise.Output::class.java)
-        .bindOptionalParameter(
-            "exercise.name",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StopExercise.PropertyMapStrings.NAME.key]
-                        as Property<StringValue>
-                )
-            },
-            StopExercise.Arguments.Builder::setName,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER
-        )
-        .build()
-
+/** A capability corresponding to actions.intent.STOP_EXERCISE */
 @CapabilityFactory(name = CAPABILITY_NAME)
 class StopExercise private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
@@ -111,4 +91,26 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        // TODO(b/273602015): Update to use Name property from builtintype library.
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "exercise.name",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.NAME.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setName,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
index 528a497..59ed86a 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/PauseTimer.kt
@@ -20,6 +20,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** PauseTimer.kt in interaction-capabilities-productivity */
 private const val CAPABILITY_NAME = "actions.intent.PAUSE_TIMER"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(PauseTimer.Arguments::class.java, PauseTimer.Arguments::Builder)
-        .setOutput(PauseTimer.Output::class.java)
-        .bindRepeatedParameter(
-            "timer",
-            { properties ->
-                Optional.ofNullable(
-                    properties[PauseTimer.PropertyMapStrings.TIMER_LIST.key]
-                        as Property<TimerValue>
-                )
-            },
-            PauseTimer.Arguments.Builder::setTimerList,
-            TimerValue.PARAM_VALUE_CONVERTER,
-            TimerValue.ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            PauseTimer.ExecutionStatus::toParamValue,
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.PAUSE_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class PauseTimer private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         TIMER_LIST("timer.timerList"),
@@ -178,4 +155,30 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindRepeatedParameter(
+                    "timer",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.TIMER_LIST.key]
+                                as Property<TimerValue>
+                        )
+                    },
+                    Arguments.Builder::setTimerList,
+                    TimerValue.PARAM_VALUE_CONVERTER,
+                    TimerValue.ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue,
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
index 94d203b..e174668 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResetTimer.kt
@@ -20,6 +20,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** ResetTimer.kt in interaction-capabilities-productivity */
 private const val CAPABILITY_NAME = "actions.intent.RESET_TIMER"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(ResetTimer.Arguments::class.java, ResetTimer.Arguments::Builder)
-        .setOutput(ResetTimer.Output::class.java)
-        .bindRepeatedParameter(
-            "timer",
-            { properties ->
-                Optional.ofNullable(
-                    properties[ResetTimer.PropertyMapStrings.TIMER_LIST.key]
-                        as Property<TimerValue>
-                )
-            },
-            ResetTimer.Arguments.Builder::setTimerList,
-            TimerValue.PARAM_VALUE_CONVERTER,
-            TimerValue.ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            ResetTimer.ExecutionStatus::toParamValue
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.RESET_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class ResetTimer private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindRepeatedParameter(
+                    "timer",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.TIMER_LIST.key]
+                                as Property<TimerValue>
+                        )
+                    },
+                    Arguments.Builder::setTimerList,
+                    TimerValue.PARAM_VALUE_CONVERTER,
+                    TimerValue.ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
index d967e45..a190cc8 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/ResumeTimer.kt
@@ -20,6 +20,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** ResumeTimer.kt in interaction-capabilities-productivity */
 private const val CAPABILITY_NAME = "actions.intent.RESUME_TIMER"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(ResumeTimer.Arguments::class.java, ResumeTimer.Arguments::Builder)
-        .setOutput(ResumeTimer.Output::class.java)
-        .bindRepeatedParameter(
-            "timer",
-            { properties ->
-                Optional.ofNullable(
-                    properties[ResumeTimer.PropertyMapStrings.TIMER_LIST.key]
-                        as Property<TimerValue>
-                )
-            },
-            ResumeTimer.Arguments.Builder::setTimerList,
-            TimerValue.PARAM_VALUE_CONVERTER,
-            TimerValue.ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            ResumeTimer.ExecutionStatus::toParamValue
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.RESUME_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class ResumeTimer private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindRepeatedParameter(
+                    "timer",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.TIMER_LIST.key]
+                                as Property<TimerValue>
+                        )
+                    },
+                    Arguments.Builder::setTimerList,
+                    TimerValue.PARAM_VALUE_CONVERTER,
+                    TimerValue.ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
index fcd211c..7d71c22 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StartTimer.kt
@@ -20,6 +20,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.HostProperties
 import androidx.appactions.interaction.capabilities.core.ValueListener
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
@@ -35,78 +36,10 @@
 import java.time.Duration
 import java.util.Optional
 
-/** StartTimer.kt in interaction-capabilities-productivity */
 private const val CAPABILITY_NAME = "actions.intent.START_TIMER"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StartTimer.Arguments::class.java, StartTimer.Arguments::Builder)
-        .setOutput(StartTimer.Output::class.java)
-        .bindOptionalParameter(
-            "timer.identifier",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartTimer.PropertyMapStrings.IDENTIFIER.key]
-                        as Property<StringValue>
-                )
-            },
-            StartTimer.Arguments.Builder::setIdentifier,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
-        )
-        .bindOptionalParameter(
-            "timer.name",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartTimer.PropertyMapStrings.NAME.key]
-                        as Property<StringValue>
-                )
-            },
-            StartTimer.Arguments.Builder::setName,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-            TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
-        )
-        .bindOptionalParameter(
-            "timer.duration",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartTimer.PropertyMapStrings.DURATION.key]
-                        as Property<Duration>
-                )
-            },
-            StartTimer.Arguments.Builder::setDuration,
-            TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
-            TypeConverters.DURATION_ENTITY_CONVERTER,
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StartTimer.ExecutionStatus::toParamValue,
-        )
-        .build()
-
-private val SESSION_BRIDGE = SessionBridge<StartTimer.ExecutionSession, StartTimer.Confirmation> {
-        session ->
-    val taskHandlerBuilder = TaskHandler.Builder<StartTimer.Confirmation>()
-    session.nameListener?.let {
-        taskHandlerBuilder.registerValueTaskParam(
-            "timer.name",
-            it,
-            TypeConverters.STRING_PARAM_VALUE_CONVERTER,
-        )
-    }
-    session.durationListener?.let {
-        taskHandlerBuilder.registerValueTaskParam(
-            "timer.duration",
-            it,
-            TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
-        )
-    }
-    taskHandlerBuilder.build()
-}
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StartTimer private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         TIMER_LIST("timer.timerList"),
@@ -265,4 +198,74 @@
     }
 
     class Confirmation internal constructor()
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "timer.identifier",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.IDENTIFIER.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setIdentifier,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+                )
+                .bindOptionalParameter(
+                    "timer.name",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.NAME.key]
+                                as Property<StringValue>
+                        )
+                    },
+                    Arguments.Builder::setName,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                    TypeConverters.STRING_VALUE_ENTITY_CONVERTER,
+                )
+                .bindOptionalParameter(
+                    "timer.duration",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.DURATION.key]
+                                as Property<Duration>
+                        )
+                    },
+                    Arguments.Builder::setDuration,
+                    TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+                    TypeConverters.DURATION_ENTITY_CONVERTER,
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue,
+                )
+                .build()
+
+        private val SESSION_BRIDGE = SessionBridge<ExecutionSession, Confirmation> {
+                session ->
+            val taskHandlerBuilder = TaskHandler.Builder<Confirmation>()
+            session.nameListener?.let {
+                taskHandlerBuilder.registerValueTaskParam(
+                    "timer.name",
+                    it,
+                    TypeConverters.STRING_PARAM_VALUE_CONVERTER,
+                )
+            }
+            session.durationListener?.let {
+                taskHandlerBuilder.registerValueTaskParam(
+                    "timer.duration",
+                    it,
+                    TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+                )
+            }
+            taskHandlerBuilder.build()
+        }
     }
+}
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
index 93f5b75..394935b 100644
--- a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/StopTimer.kt
@@ -20,6 +20,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -29,34 +30,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** StopTimer.kt in interaction-capabilities-productivity */
 private const val CAPABILITY_NAME = "actions.intent.STOP_TIMER"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StopTimer.Arguments::class.java, StopTimer.Arguments::Builder)
-        .setOutput(StopTimer.Output::class.java)
-        .bindRepeatedParameter(
-            "timer",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StopTimer.PropertyMapStrings.TIMER_LIST.key]
-                        as Property<TimerValue>
-                )
-            },
-            StopTimer.Arguments.Builder::setTimerList,
-            TimerValue.PARAM_VALUE_CONVERTER,
-            TimerValue.ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StopTimer.ExecutionStatus::toParamValue
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_TIMER */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StopTimer private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         TIMER_LIST("timer.timerList"),
@@ -175,4 +152,30 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindRepeatedParameter(
+                    "timer",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.TIMER_LIST.key]
+                                as Property<TimerValue>
+                        )
+                    },
+                    Arguments.Builder::setTimerList,
+                    TimerValue.PARAM_VALUE_CONVERTER,
+                    TimerValue.ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
index 11e0682..9152d36 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartEmergencySharing.kt
@@ -21,6 +21,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,24 +34,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** StartEmergencySharing.kt in interaction-capabilities-safety */
 private const val CAPABILITY_NAME = "actions.intent.START_EMERGENCY_SHARING"
 
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(
-            StartEmergencySharing.Arguments::class.java,
-            StartEmergencySharing.Arguments::Builder
-        )
-        .setOutput(StartEmergencySharing.Output::class.java)
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StartEmergencySharing.ExecutionStatus::toParamValue,
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_EMERGENCY_SHARING */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StartEmergencySharing private constructor() {
     // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
     class CapabilityBuilder :
@@ -167,4 +154,20 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(
+                    Arguments::class.java,
+                    Arguments::Builder
+                )
+                .setOutput(Output::class.java)
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue,
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
index 9bb6f01..133dfde 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StartSafetyCheck.kt
@@ -23,6 +23,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
@@ -39,51 +40,10 @@
 import java.time.ZonedDateTime
 import java.util.Optional
 
-/** StartSafetyCheck.kt in interaction-capabilities-safety */
 private const val CAPABILITY_NAME = "actions.intent.START_SAFETY_CHECK"
 
-@Suppress("UNCHECKED_CAST")
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StartSafetyCheck.Arguments::class.java, StartSafetyCheck.Arguments::Builder)
-        .setOutput(StartSafetyCheck.Output::class.java)
-        .bindOptionalParameter(
-            "safetyCheck.duration",
-            { properties ->
-                Optional.ofNullable(
-                    properties[StartSafetyCheck.PropertyMapStrings.DURATION.key]
-                        as Property<Duration>
-                )
-            },
-            StartSafetyCheck.Arguments.Builder::setDuration,
-            TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
-            TypeConverters.DURATION_ENTITY_CONVERTER
-        )
-        .bindOptionalParameter(
-            "safetyCheck.checkInTime",
-            { property ->
-                Optional.ofNullable(
-                    property[StartSafetyCheck.PropertyMapStrings.CHECK_IN_TIME.key]
-                        as Property<ZonedDateTime>
-                )
-            },
-            StartSafetyCheck.Arguments.Builder::setCheckInTime,
-            TypeConverters.ZONED_DATETIME_PARAM_VALUE_CONVERTER,
-            TypeConverters.ZONED_DATETIME_ENTITY_CONVERTER
-        )
-        .bindOptionalOutput(
-            "safetyCheck",
-            { output -> Optional.ofNullable(output.safetyCheck) },
-            ParamValueConverter.of(SAFETY_CHECK_TYPE_SPEC)::toParamValue
-        )
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StartSafetyCheck.ExecutionStatus::toParamValue
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.START_SAFETY_CHECK */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StartSafetyCheck private constructor() {
     internal enum class PropertyMapStrings(val key: String) {
         DURATION("safetycheck.duration"),
@@ -265,4 +225,47 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        @Suppress("UNCHECKED_CAST")
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalParameter(
+                    "safetyCheck.duration",
+                    { properties ->
+                        Optional.ofNullable(
+                            properties[PropertyMapStrings.DURATION.key]
+                                as Property<Duration>
+                        )
+                    },
+                    Arguments.Builder::setDuration,
+                    TypeConverters.DURATION_PARAM_VALUE_CONVERTER,
+                    TypeConverters.DURATION_ENTITY_CONVERTER
+                )
+                .bindOptionalParameter(
+                    "safetyCheck.checkInTime",
+                    { property ->
+                        Optional.ofNullable(
+                            property[PropertyMapStrings.CHECK_IN_TIME.key]
+                                as Property<ZonedDateTime>
+                        )
+                    },
+                    Arguments.Builder::setCheckInTime,
+                    TypeConverters.ZONED_DATETIME_PARAM_VALUE_CONVERTER,
+                    TypeConverters.ZONED_DATETIME_ENTITY_CONVERTER
+                )
+                .bindOptionalOutput(
+                    "safetyCheck",
+                    { output -> Optional.ofNullable(output.safetyCheck) },
+                    ParamValueConverter.of(SAFETY_CHECK_TYPE_SPEC)::toParamValue
+                )
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
index 731aff5..88fe5f2 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopEmergencySharing.kt
@@ -22,6 +22,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,24 +34,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** StopEmergencySharing.kt in interaction-capabilities-safety */
 private const val CAPABILITY_NAME = "actions.intent.STOP_EMERGENCY_SHARING"
 
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(
-            StopEmergencySharing.Arguments::class.java,
-            StopEmergencySharing.Arguments::Builder
-        )
-        .setOutput(StopEmergencySharing.Output::class.java)
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StopEmergencySharing.ExecutionStatus::toParamValue,
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_EMERGENCY_SHARING */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StopEmergencySharing private constructor() {
     // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
     class CapabilityBuilder :
@@ -168,4 +155,20 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(
+                    Arguments::class.java,
+                    Arguments::Builder
+                )
+                .setOutput(Output::class.java)
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue,
+                )
+                .build()
+    }
 }
diff --git a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
index 3d3ffed..c1d9aef 100644
--- a/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
+++ b/appactions/interaction/interaction-capabilities-safety/src/main/java/androidx/appactions/interaction/capabilities/safety/StopSafetyCheck.kt
@@ -22,6 +22,7 @@
 import androidx.appactions.builtintypes.experimental.types.SuccessStatus
 import androidx.appactions.interaction.capabilities.core.BaseExecutionSession
 import androidx.appactions.interaction.capabilities.core.Capability
+import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
@@ -33,21 +34,10 @@
 import androidx.appactions.interaction.protobuf.Value
 import java.util.Optional
 
-/** StopSafetyCheck.kt in interaction-capabilities-safety */
 private const val CAPABILITY_NAME = "actions.intent.STOP_SAFETY_CHECK"
 
-private val ACTION_SPEC =
-    ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
-        .setArguments(StopSafetyCheck.Arguments::class.java, StopSafetyCheck.Arguments::Builder)
-        .setOutput(StopSafetyCheck.Output::class.java)
-        .bindOptionalOutput(
-            "executionStatus",
-            { output -> Optional.ofNullable(output.executionStatus) },
-            StopSafetyCheck.ExecutionStatus::toParamValue
-        )
-        .build()
-
-// TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
+/** A capability corresponding to actions.intent.STOP_SAFETY_CHECK */
+@CapabilityFactory(name = CAPABILITY_NAME)
 class StopSafetyCheck private constructor() {
     // TODO(b/267805819): Update to include the SessionFactory once Session API is ready.
     class CapabilityBuilder :
@@ -164,4 +154,17 @@
     class Confirmation internal constructor()
 
     sealed interface ExecutionSession : BaseExecutionSession<Arguments, Output>
+
+    companion object {
+        private val ACTION_SPEC =
+            ActionSpecBuilder.ofCapabilityNamed(CAPABILITY_NAME)
+                .setArguments(Arguments::class.java, Arguments::Builder)
+                .setOutput(Output::class.java)
+                .bindOptionalOutput(
+                    "executionStatus",
+                    { output -> Optional.ofNullable(output.executionStatus) },
+                    ExecutionStatus::toParamValue
+                )
+                .build()
+    }
 }
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index 32f63ba..f60a494 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -161,6 +161,7 @@
                     error.add("ModifierNodeInspectableProperties")
                     error.add("ModifierParameter")
                     error.add("MutableCollectionMutableState")
+                    error.add("OpaqueUnitKey")
                     error.add("UnnecessaryComposedModifier")
                     error.add("FrequentlyChangedStateReadInComposition")
                     error.add("ReturnFromAwaitPointerEventScope")
diff --git a/buildSrc/repos.gradle b/buildSrc/repos.gradle
index 9b5077d..4ae93fa 100644
--- a/buildSrc/repos.gradle
+++ b/buildSrc/repos.gradle
@@ -65,6 +65,13 @@
                url("https://maven.pkg.jetbrains.space/public/p/compose/dev")
         }
         handler.mavenLocal()
+        // TODO(b/280646217): Remove after official release to gmaven.
+        handler.maven {
+            url("https://storage.googleapis.com/r8-releases/raw")
+            content {
+                includeModule("com.android.tools", "r8")
+            }
+        }
     }
     // Ordering appears to be important: b/229733266
     def androidPluginRepoOverride = System.getenv("GRADLE_PLUGIN_REPO")
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
index 288929c..5a0fb78 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/internal/sessionprocessor/BasicExtenderSessionProcessorTest.kt
@@ -180,6 +180,10 @@
                 cameraProvider.shutdown()[10, TimeUnit.SECONDS]
             }
         }
+
+        if (::basicExtenderSessionProcessor.isInitialized) {
+            basicExtenderSessionProcessor.deInitSession()
+        }
     }
 
     @Test
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
index 6bbe1fe..8366234 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt
@@ -847,29 +847,6 @@
     }
 
     @Test
-    fun mute_defaultToNotMuted() {
-        // Arrange.
-        val recorder = createRecorder()
-        val recording = createRecordingProcess(recorder = recorder)
-        val recording2 = createRecordingProcess(recorder = recorder)
-
-        // Act.
-        recording.startAndVerify()
-        recording.mute(true)
-        recording.stopAndVerify()
-
-        recording2.startAndVerify()
-        recording2.verifyStatus(5) { statusList ->
-            // Assert.
-            statusList.forEach {
-                assertThat(it.recordingStats.audioStats.audioState)
-                    .isEqualTo(AudioStats.AUDIO_STATE_ACTIVE)
-            }
-        }
-        recording2.stopAndVerify()
-    }
-
-    @Test
     fun optionsOverridesDefaults() {
         val qualitySelector = QualitySelector.from(Quality.HIGHEST)
         val recorder = createRecorder(qualitySelector = qualitySelector)
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 2a79dbd..7e62cf4 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
@@ -846,6 +846,73 @@
         file2.delete()
     }
 
+    @Test
+    fun mute_defaultToNotMuted() {
+        assumeTrue("Audio stream is not available", audioStreamAvailable)
+
+        // Arrange.
+        val recorder = Recorder.Builder().build()
+        val videoCaptureLocal = VideoCapture.withOutput(recorder)
+        instrumentation.runOnMainSync {
+            cameraProvider.bindToLifecycle(
+                lifecycleOwner,
+                cameraSelector,
+                preview,
+                videoCaptureLocal
+            )
+        }
+        val file1 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+
+        recorder.prepareRecording(context, FileOutputOptions.Builder(file1).build())
+            .withAudioEnabled()
+            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).use {
+                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+                // Keep the first recording muted.
+                it.mute(true)
+            }
+
+        mockVideoRecordEventConsumer.verifyAcceptCall(
+            VideoRecordEvent.Finalize::class.java,
+            false,
+            GENERAL_TIMEOUT
+        )
+        file1.delete()
+
+        mockVideoRecordEventConsumer.clearAcceptCalls()
+
+        val file2 = File.createTempFile("CameraX", ".tmp").apply { deleteOnExit() }
+
+        // Act.
+        recorder.prepareRecording(context, FileOutputOptions.Builder(file2).build())
+            .withAudioEnabled()
+            .start(CameraXExecutors.directExecutor(), mockVideoRecordEventConsumer).use {
+                mockVideoRecordEventConsumer.verifyRecordingStartSuccessfully()
+                val captor = ArgumentCaptorCameraX<VideoRecordEvent> { argument ->
+                    VideoRecordEvent::class.java.isInstance(
+                        argument
+                    )
+                }
+                mockVideoRecordEventConsumer.verifyAcceptCall(
+                    VideoRecordEvent::class.java,
+                    false,
+                    CallTimesAtLeast(1),
+                    captor
+                )
+                assertThat(captor.value).isInstanceOf(VideoRecordEvent.Status::class.java)
+                val status = captor.value as VideoRecordEvent.Status
+                // Assert: The second recording should not be muted.
+                assertThat(status.recordingStats.audioStats.audioState)
+                    .isEqualTo(AudioStats.AUDIO_STATE_ACTIVE)
+            }
+
+        mockVideoRecordEventConsumer.verifyAcceptCall(
+            VideoRecordEvent.Finalize::class.java,
+            false,
+            GENERAL_TIMEOUT
+        )
+        file2.delete()
+    }
+
     private fun performRecording(
         videoCapture: VideoCapture<Recorder>,
         file: File,
diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
index af3ed21..58e7d60 100644
--- a/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
+++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/internal/SharedByteBufferTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.video.internal
 
+import android.os.Build
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -35,6 +36,7 @@
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
+import org.junit.Assume.assumeFalse
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -289,6 +291,11 @@
     @Test
     @LargeTest
     fun finalizeClosesUnclosedInstances() = runBlocking {
+        assumeFalse(
+            "Ignore devices that get flaky result. See b/278842333",
+            isModel("moto c") || isModel("rne-l23")
+        )
+
         val buf = ByteBuffer.allocate(0)
         val closeActionDeferred = CompletableDeferred<Unit>()
         val origBuf = SharedByteBuffer.newSharedInstance(buf, CameraXExecutors.directExecutor()) {
@@ -327,4 +334,6 @@
             phantomReferences.forEach { it.clear() }
         }
     }
+
+    private fun isModel(model: String) = model.equals(Build.MODEL, true)
 }
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
index 26d932d..2fb1a60 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BasicUITest.kt
@@ -19,6 +19,7 @@
 import android.Manifest
 import android.content.Context
 import android.content.Intent
+import android.os.Build
 import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.lifecycle.ProcessCameraProvider
@@ -38,7 +39,7 @@
 import java.util.concurrent.TimeUnit
 import leakcanary.DetectLeaksAfterTestSuccess
 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
@@ -94,7 +95,12 @@
 
     @Before
     fun setUp() {
-        Assume.assumeTrue(CameraUtil.deviceHasCamera())
+        assumeTrue(CameraUtil.deviceHasCamera())
+        assumeFalse(
+            "See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+                " the test failure.",
+            Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30
+        )
         CoreAppTestUtil.assumeCompatibleDevice()
         // Use the natural orientation throughout these tests to ensure the activity isn't
         // recreated unexpectedly. This will also freeze the sensors until
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
index 056a07e..8aceeb4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/FocusMeteringDeviceTest.kt
@@ -44,7 +44,6 @@
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.hamcrest.CoreMatchers.equalTo
@@ -140,11 +139,6 @@
                 cameraProvider.shutdown()[10, TimeUnit.SECONDS]
             }
         }
-
-        if (selectorName == "front" && implName == CameraPipeConfig::class.simpleName) {
-            // TODO(b/264332446): Replace this delay with some API like closeAll() once available
-            delay(5000)
-        }
     }
 
     @Test
@@ -381,8 +375,8 @@
             )
             cameraCharacteristics?.run {
                 (if (flags.hasFlag(FLAG_AF)) (get(CONTROL_MAX_REGIONS_AF)!! > 0) else false) ||
-                (if (flags.hasFlag(FLAG_AE)) (get(CONTROL_MAX_REGIONS_AE)!! > 0) else false) ||
-                (if (flags.hasFlag(FLAG_AWB)) (get(CONTROL_MAX_REGIONS_AWB)!! > 0) else false)
+                    (if (flags.hasFlag(FLAG_AE)) (get(CONTROL_MAX_REGIONS_AE)!! > 0) else false) ||
+                    (if (flags.hasFlag(FLAG_AWB)) (get(CONTROL_MAX_REGIONS_AWB)!! > 0) else false)
             } ?: false
         } catch (e: Exception) {
             false
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
index 1b89972..7ed59c5 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ZoomControlDeviceTest.kt
@@ -56,7 +56,6 @@
 import kotlinx.atomicfu.atomic
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeout
@@ -134,11 +133,6 @@
                 cameraProvider.shutdown()[10, TimeUnit.SECONDS]
             }
         }
-
-        if (selectorName == "front" && implName == CameraPipeConfig::class.simpleName) {
-            // TODO(b/264332446): Replace this delay with some API like closeAll() once available
-            delay(5000)
-        }
     }
 
     @Test
@@ -186,7 +180,8 @@
          */
         try {
             cameraControl.setZoomRatio(maxZoomRatio + 1.0f)[5, TimeUnit.SECONDS]
-        } catch (_: ExecutionException) {}
+        } catch (_: ExecutionException) {
+        }
 
         assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
     }
@@ -213,7 +208,8 @@
          */
         try {
             cameraControl.setZoomRatio(minZoomRatio - 1.0f)[5, TimeUnit.SECONDS]
-        } catch (_: ExecutionException) {}
+        } catch (_: ExecutionException) {
+        }
 
         assertThat(cameraInfo.zoomState.value?.zoomRatio).isEqualTo(2.0f)
     }
@@ -425,7 +421,8 @@
          */
         try {
             cameraControl.setLinearZoom(1.1f)[5, TimeUnit.SECONDS]
-        } catch (_: ExecutionException) {}
+        } catch (_: ExecutionException) {
+        }
 
         assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
     }
@@ -448,7 +445,8 @@
          */
         try {
             cameraControl.setLinearZoom(-0.1f)[5, TimeUnit.SECONDS]
-        } catch (_: ExecutionException) {}
+        } catch (_: ExecutionException) {
+        }
 
         assertThat(cameraInfo.zoomState.value?.linearZoom).isEqualTo(0.5f)
     }
@@ -761,8 +759,10 @@
         private val failureException =
             TimeoutException("Test doesn't complete after waiting for $captureCount frames.")
 
-        @Volatile private var startReceiving = false
-        @Volatile private var _verifyBlock: (
+        @Volatile
+        private var startReceiving = false
+        @Volatile
+        private var _verifyBlock: (
             captureRequest: CaptureRequest,
             captureResult: TotalCaptureResult
         ) -> Boolean = { _, _ -> false }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
index 7165a2e..b8d8429 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureLockedOrientationTest.kt
@@ -16,9 +16,11 @@
 
 package androidx.camera.integration.uiwidgets.rotations
 
+import android.os.Build
 import androidx.test.core.app.ActivityScenario
 import androidx.test.filters.LargeTest
 import org.junit.After
+import org.junit.Assume
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -54,6 +56,11 @@
 
     @Before
     fun before() {
+        Assume.assumeFalse(
+            "See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+                " the test failure.",
+            Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30
+        )
         setUp(lensFacing)
     }
 
diff --git a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
index a0204db..aaedb05 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/androidTest/java/androidx/camera/integration/uiwidgets/rotations/ImageCaptureUnlockedOrientationTest.kt
@@ -17,6 +17,7 @@
 package androidx.camera.integration.uiwidgets.rotations
 
 import android.app.Instrumentation
+import android.os.Build
 import androidx.camera.core.CameraSelector
 import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.IMAGE_CAPTURE_MODE_FILE
 import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.IMAGE_CAPTURE_MODE_IN_MEMORY
@@ -25,6 +26,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import org.junit.After
+import org.junit.Assume.assumeFalse
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -87,6 +89,9 @@
 
     @Before
     fun before() {
+        assumeFalse("See b/152082918, Wembley Api30 has a libjpeg issue which causes" +
+            " the test failure.",
+            Build.MODEL.equals("wembley", ignoreCase = true) && Build.VERSION.SDK_INT <= 30)
         setUp(lensFacing)
     }
 
diff --git a/compose/animation/animation-core/build.gradle b/compose/animation/animation-core/build.gradle
index 2482074..4777d20 100644
--- a/compose/animation/animation-core/build.gradle
+++ b/compose/animation/animation-core/build.gradle
@@ -15,9 +15,8 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,65 +24,15 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
-    if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.1.0")
-
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation("androidx.compose.ui:ui:1.2.1")
-        implementation("androidx.compose.ui:ui-unit:1.2.1")
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-        implementation(libs.kotlinStdlib)
-        api(libs.kotlinCoroutinesCore)
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinCoroutinesCore)
-
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(project(":compose:animation:animation"))
-        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
-        androidTestImplementation(project(":compose:test-utils"))
-
-        lintPublish project(":compose:animation:animation-core-lint")
-
-        samples(project(":compose:animation:animation-core:animation-core-samples"))
-    }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-
-            jvmMain {
-                dependencies {
-                    implementation(libs.kotlinStdlib)
-                    api(libs.kotlinCoroutinesCore)
-                }
-            }
-
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(project(":compose:runtime:runtime"))
                 implementation(project(":compose:ui:ui"))
                 implementation(project(":compose:ui:ui-unit"))
@@ -91,45 +40,86 @@
                 implementation(libs.kotlinStdlibCommon)
                 api(libs.kotlinCoroutinesCore)
             }
+        }
 
-            androidMain {
-                dependencies {
-                    api("androidx.annotation:annotation:1.1.0")
-                    implementation(libs.kotlinStdlib)
-                }
+        commonTest {
+            dependencies {
             }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+                implementation(libs.kotlinStdlib)
+                api(libs.kotlinCoroutinesCore)
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+                implementation(libs.kotlinStdlib)
+            }
+        }
+
+        if (desktopEnabled) {
             desktopMain {
+                dependsOn(jvmMain)
                 dependencies {
                     implementation(libs.kotlinStdlib)
+                    implementation(project(":compose:runtime:runtime"))
+                    implementation(project(":compose:ui:ui"))
+                    implementation(project(":compose:ui:ui-unit"))
+                    implementation(project(":compose:ui:ui-util"))
                 }
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.testCore)
+                implementation(libs.junit)
+                implementation(project(":compose:animation:animation"))
+                implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
+                implementation(project(":compose:test-utils"))
+            }
+        }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
                 implementation(libs.truth)
                 implementation(libs.kotlinCoroutinesCore)
             }
+        }
 
-            androidAndroidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.testCore)
-                implementation(libs.junit)
-                implementation(project(":compose:animation:animation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
-                implementation(project(":compose:test-utils"))
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
             }
         }
     }
-    dependencies {
-        samples(project(":compose:animation:animation-core:animation-core-samples"))
-    }
+}
+
+dependencies {
+    lintPublish project(":compose:animation:animation-core-lint")
 }
 
 androidx {
diff --git a/compose/animation/animation-graphics/build.gradle b/compose/animation/animation-graphics/build.gradle
index c6597a7..bb4c4c1 100644
--- a/compose/animation/animation-graphics/build.gradle
+++ b/compose/animation/animation-graphics/build.gradle
@@ -15,7 +15,7 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -25,55 +25,15 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.1.0")
-        api(project(":compose:animation:animation"))
-        api("androidx.compose.foundation:foundation-layout:1.2.1")
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api("androidx.compose.ui:ui:1.2.1")
-        api("androidx.compose.ui:ui-geometry:1.2.1")
-
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-        implementation(libs.kotlinStdlibCommon)
-        implementation("androidx.core:core-ktx:1.5.0")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-
-        androidTestImplementation("androidx.compose.foundation:foundation:1.2.1")
-        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-
-        samples(project(":compose:animation:animation-graphics:animation-graphics-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api(project(":compose:animation:animation"))
@@ -84,39 +44,84 @@
 
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
+        androidMain.dependencies {
+            api("androidx.annotation:annotation:1.1.0")
+            implementation("androidx.core:core-ktx:1.5.0")
+        }
 
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
-                implementation("androidx.core:core-ktx:1.5.0")
+        commonTest {
+            dependencies {
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+                    api(project(":compose:foundation:foundation-layout"))
+                    api(project(":compose:runtime:runtime"))
+                    api(project(":compose:ui:ui"))
+                    api(project(":compose:ui:ui-geometry"))
+                    implementation(project(":compose:ui:ui-util"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
                 implementation(libs.truth)
-                implementation(project(":compose:foundation:foundation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation("androidx.compose.foundation:foundation:1.2.1")
+                implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
                 implementation(project(":compose:test-utils"))
             }
         }
-    }
-    dependencies {
-        samples(project(":compose:animation:animation-graphics:animation-graphics-samples"))
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
diff --git a/compose/animation/animation/build.gradle b/compose/animation/animation/build.gradle
index 8d6b39f..f2b4f811 100644
--- a/compose/animation/animation/build.gradle
+++ b/compose/animation/animation/build.gradle
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,56 +23,15 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.1.0")
-        api(project(":compose:animation:animation-core"))
-        api("androidx.compose.foundation:foundation-layout:1.2.1")
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api("androidx.compose.ui:ui:1.2.1")
-        api("androidx.compose.ui:ui-geometry:1.2.1")
-
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-        implementation(libs.kotlinStdlibCommon)
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-
-        androidTestImplementation("androidx.compose.foundation:foundation:1.2.1")
-        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-
-        lintPublish project(":compose:animation:animation-lint")
-
-        samples(project(":compose:animation:animation:animation-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api(project(":compose:animation:animation-core"))
@@ -85,39 +42,84 @@
 
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+
+                    api(project(":compose:foundation:foundation-layout"))
+                    api(project(":compose:runtime:runtime"))
+                    api(project(":compose:ui:ui"))
+                    api(project(":compose:ui:ui-geometry"))
+
+                    implementation(project(":compose:ui:ui-util"))
+                }
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
+        jvmTest {
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
                 implementation(libs.truth)
-                implementation(project(":compose:foundation:foundation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation("androidx.compose.foundation:foundation:1.2.1")
+                implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
                 implementation(project(":compose:test-utils"))
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
-    dependencies {
-        samples(project(":compose:animation:animation:animation-samples"))
-    }
+}
+
+dependencies {
+    lintPublish project(":compose:animation:animation-lint")
 }
 
 androidx {
diff --git a/compose/foundation/foundation-layout/api/current.txt b/compose/foundation/foundation-layout/api/current.txt
index 4339957..c8e0c04 100644
--- a/compose/foundation/foundation-layout/api/current.txt
+++ b/compose/foundation/foundation-layout/api/current.txt
@@ -111,12 +111,6 @@
     method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
-  }
-
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
-  }
-
   public final class IntrinsicKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index 3e7c323..3d72dbe 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -114,7 +114,7 @@
   @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
+  @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
   }
 
   public final class FlowLayoutKt {
@@ -122,7 +122,7 @@
     method @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.runtime.Composable public static inline void FlowRow(optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, optional androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, optional int maxItemsInEachRow, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.FlowRowScope,kotlin.Unit> content);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
+  @androidx.compose.foundation.layout.ExperimentalLayoutApi @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
   }
 
   public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/api/restricted_current.txt b/compose/foundation/foundation-layout/api/restricted_current.txt
index 2b75f19..03cdfb3 100644
--- a/compose/foundation/foundation-layout/api/restricted_current.txt
+++ b/compose/foundation/foundation-layout/api/restricted_current.txt
@@ -114,17 +114,11 @@
     method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowColumnScope extends androidx.compose.foundation.layout.ColumnScope {
-  }
-
   public final class FlowLayoutKt {
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy columnMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, int maxItemsInMainAxis);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static androidx.compose.ui.layout.MeasurePolicy rowMeasurementHelper(androidx.compose.foundation.layout.Arrangement.Horizontal horizontalArrangement, androidx.compose.foundation.layout.Arrangement.Vertical verticalArrangement, int maxItemsInMainAxis);
   }
 
-  @androidx.compose.foundation.layout.LayoutScopeMarker @androidx.compose.runtime.Immutable @kotlin.jvm.JvmDefaultWithCompatibility public interface FlowRowScope extends androidx.compose.foundation.layout.RowScope {
-  }
-
   public final class IntrinsicKt {
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier height(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier requiredHeight(androidx.compose.ui.Modifier, androidx.compose.foundation.layout.IntrinsicSize intrinsicSize);
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index a754486..872eef6 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,94 +23,66 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-
-        api("androidx.annotation:annotation:1.1.0")
-        api(project(":compose:ui:ui"))
-        api("androidx.compose.ui:ui-unit:1.2.1")
-
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-        implementation("androidx.core:core:1.7.0")
-        implementation("androidx.compose.animation:animation-core:1.2.1")
-        implementation(libs.kotlinStdlibCommon)
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.2.1")
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
-        // old version of common-java8 conflicts with newer version, because both have
-        // DefaultLifecycleEventObserver.
-        // Outside of androidx this is resolved via constraint added to lifecycle-common,
-        // but it doesn't work in androidx.
-        // See aosp/1804059
-        androidTestImplementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-
-        samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api(project(":compose:ui:ui"))
                 implementation(project(":compose:runtime:runtime"))
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation("androidx.core:core:1.7.0")
                 implementation("androidx.compose.animation:animation-core:1.2.1")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+
+                    implementation(project(":compose:runtime:runtime"))
+                    implementation(project(":compose:ui:ui-util"))
+                }
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        jvmTest {
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:foundation:foundation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation("androidx.compose.ui:ui-test-junit4:1.2.1")
                 implementation(project(":compose:test-utils"))
                 implementation("androidx.activity:activity-compose:1.3.1")
 
@@ -121,9 +92,26 @@
                 implementation(libs.truth)
             }
         }
-    }
-    dependencies {
-        samples(project(":compose:foundation:foundation-layout:foundation-layout-samples"))
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
 }
 
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
index 5d58064..cdec1d5 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/FlowLayout.kt
@@ -126,7 +126,7 @@
  */
 @LayoutScopeMarker
 @Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
 interface FlowRowScope : RowScope
 
 /**
@@ -134,11 +134,13 @@
  */
 @LayoutScopeMarker
 @Immutable
-@JvmDefaultWithCompatibility
+@ExperimentalLayoutApi
 interface FlowColumnScope : ColumnScope
 
+@OptIn(ExperimentalLayoutApi::class)
 internal object FlowRowScopeInstance : RowScope by RowScopeInstance, FlowRowScope
 
+@OptIn(ExperimentalLayoutApi::class)
 internal object FlowColumnScopeInstance : ColumnScope by ColumnScopeInstance, FlowColumnScope
 
 private fun getVerticalArrangement(verticalArrangement: Arrangement.Vertical):
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
index 78210e2..b0a80c7 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Size.kt
@@ -38,7 +38,6 @@
 import androidx.compose.ui.unit.constrain
 import androidx.compose.ui.unit.constrainHeight
 import androidx.compose.ui.unit.constrainWidth
-import androidx.compose.ui.unit.dp
 import kotlin.math.roundToInt
 
 /**
@@ -768,12 +767,12 @@
     private val Density.targetConstraints: Constraints
         get() {
             val maxWidth = if (maxWidth != Dp.Unspecified) {
-                maxWidth.coerceAtLeast(0.dp).roundToPx()
+                maxWidth.roundToPx().coerceAtLeast(0)
             } else {
                 Constraints.Infinity
             }
             val maxHeight = if (maxHeight != Dp.Unspecified) {
-                maxHeight.coerceAtLeast(0.dp).roundToPx()
+                maxHeight.roundToPx().coerceAtLeast(0)
             } else {
                 Constraints.Infinity
             }
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index e367382..a57dfef 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -15,9 +15,8 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -26,80 +25,15 @@
     id("AndroidXPaparazziPlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        api("androidx.annotation:annotation:1.1.0")
-        api("androidx.compose.animation:animation:1.2.1")
-        api(project(":compose:runtime:runtime"))
-        api(project(":compose:ui:ui"))
-
-        implementation(libs.kotlinStdlibCommon)
-        implementation(project(":compose:foundation:foundation-layout"))
-        implementation(project(':emoji2:emoji2'))
-        implementation(project(':core:core'))
-        implementation("androidx.compose.ui:ui-graphics:1.2.1")
-        implementation("androidx.compose.ui:ui-text:1.2.1")
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-
-        testImplementation(project(":compose:test-utils"))
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinCoroutinesTest)
-        testImplementation(libs.kotlinTest)
-        testImplementation(libs.mockitoCore4)
-        testImplementation(libs.kotlinReflect)
-        testImplementation(libs.mockitoKotlin4)
-
-        testImplementation(project(":constraintlayout:constraintlayout-compose"))
-
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":internal-testutils-fonts"))
-        androidTestImplementation(project(":test:screenshot:screenshot"))
-        androidTestImplementation(project(":internal-testutils-runtime"))
-        androidTestImplementation(libs.testUiautomator)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testMonitor)
-        androidTestImplementation "androidx.activity:activity-compose:1.3.1"
-        androidTestImplementation "androidx.lifecycle:lifecycle-runtime:2.6.1"
-        androidTestImplementation "androidx.savedstate:savedstate:1.2.1"
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.kotlinTest)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.mockitoKotlin)
-
-        lintChecks(project(":compose:foundation:foundation-lint"))
-        lintPublish(project(":compose:foundation:foundation-lint"))
-
-        samples(project(":compose:foundation:foundation:foundation-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(':compose:animation:animation'))
                 api(project(':compose:runtime:runtime'))
@@ -108,36 +42,57 @@
                 implementation(project(":compose:ui:ui-util"))
                 implementation(project(':compose:foundation:foundation-layout'))
             }
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
-                implementation(project(':emoji2:emoji2'))
-                implementation(project(":core:core"))
-            }
+        }
+        androidMain.dependencies {
+            api("androidx.annotation:annotation:1.1.0")
+            implementation(project(':emoji2:emoji2'))
+            implementation(project(":core:core"))
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.mockitoCore4)
-                implementation(libs.truth)
-                implementation(libs.kotlinReflect)
-                implementation(libs.mockitoKotlin4)
-            }
-
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(libs.kotlinTest)
                 implementation(libs.kotlinCoroutinesTest)
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+                implementation(project(':emoji2:emoji2'))
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+
+                    implementation(project(":compose:ui:ui-util"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+                implementation(project(":compose:ui:ui-test"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:test-utils"))
                 implementation(project(":internal-testutils-fonts"))
                 implementation(project(":test:screenshot:screenshot"))
@@ -157,20 +112,43 @@
                 implementation(libs.mockitoCore)
                 implementation(libs.mockitoKotlin)
             }
+        }
 
-            desktopTest.dependencies {
-                implementation(project(":compose:ui:ui-test-junit4"))
-                implementation(libs.truth)
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
                 implementation(libs.junit)
-                implementation(libs.skikoCurrentOs)
-                implementation(libs.mockitoCore)
-                implementation(libs.mockitoKotlin)
+                implementation(libs.truth)
+                implementation(libs.kotlinReflect)
+                implementation(project(":constraintlayout:constraintlayout-compose"))
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                    implementation(libs.truth)
+                    implementation(libs.junit)
+                    implementation(libs.skikoCurrentOs)
+                    implementation(libs.mockitoCore)
+                    implementation(libs.mockitoKotlin)
+                }
             }
         }
     }
-    dependencies {
-        samples(project(":compose:foundation:foundation:foundation-samples"))
-    }
+}
+
+dependencies {
+    lintChecks(project(":compose:foundation:foundation-lint"))
+    lintPublish(project(":compose:foundation:foundation-lint"))
 }
 
 // Screenshot tests related setup
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
index d0fc90e..603ac21 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutTest.kt
@@ -94,7 +94,10 @@
     @Test
     fun measureAndPlaceTwoItems() {
         val itemProvider = itemProvider({ 2 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         }
         rule.setContent {
             LazyLayout(itemProvider) {
@@ -118,8 +121,14 @@
     @Test
     fun measureAndPlaceMultipleLayoutsInOneItem() {
         val itemProvider = itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("${index}x0"))
-            Box(Modifier.fillMaxSize().testTag("${index}x1"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("${index}x0"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("${index}x1"))
         }
 
         rule.setContent {
@@ -143,7 +152,10 @@
     @Test
     fun updatingitemProvider() {
         var itemProvider by mutableStateOf(itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         })
 
         rule.setContent {
@@ -166,7 +178,10 @@
 
         rule.runOnIdle {
             itemProvider = itemProvider({ 2 }) { index ->
-                Box(Modifier.fillMaxSize().testTag("$index"))
+                Box(
+                    Modifier
+                        .fillMaxSize()
+                        .testTag("$index"))
             }
         }
 
@@ -178,7 +193,10 @@
     fun stateBaseditemProvider() {
         var itemCount by mutableStateOf(1)
         val itemProvider = itemProvider({ itemCount }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index"))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index"))
         }
 
         rule.setContent {
@@ -228,7 +246,11 @@
             }
         }
         val itemProvider = itemProvider({ 1 }) { index ->
-            Box(Modifier.fillMaxSize().testTag("$index").then(modifier))
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .testTag("$index")
+                    .then(modifier))
         }
         var needToCompose by mutableStateOf(false)
         val prefetchState = LazyLayoutPrefetchState()
@@ -335,13 +357,15 @@
     fun nodeIsReusedWithoutExtraRemeasure() {
         var indexToCompose by mutableStateOf<Int?>(0)
         var remeasuresCount = 0
-        val modifier = Modifier.layout { measurable, constraints ->
-            val placeable = measurable.measure(constraints)
-            remeasuresCount++
-            layout(placeable.width, placeable.height) {
-                placeable.place(0, 0)
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
             }
-        }.fillMaxSize()
+            .fillMaxSize()
         val itemProvider = itemProvider({ 2 }) {
             Box(modifier)
         }
@@ -376,6 +400,52 @@
     }
 
     @Test
+    fun nodeIsReusedWhenRemovedFirst() {
+        var itemCount by mutableStateOf(1)
+        var remeasuresCount = 0
+        val modifier = Modifier
+            .layout { measurable, constraints ->
+                val placeable = measurable.measure(constraints)
+                remeasuresCount++
+                layout(placeable.width, placeable.height) {
+                    placeable.place(0, 0)
+                }
+            }
+            .fillMaxSize()
+        val itemProvider = itemProvider({ itemCount }) {
+            Box(modifier)
+        }
+
+        rule.setContent {
+            LazyLayout(itemProvider) { constraints ->
+                val node = if (itemCount == 1) {
+                    measure(0, constraints).first()
+                } else {
+                    null
+                }
+                layout(10, 10) {
+                    node?.place(0, 0)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasuresCount).isEqualTo(1)
+            // node will be kept for reuse
+            itemCount = 0
+        }
+
+        rule.runOnIdle {
+            // node should be now reused
+            itemCount = 1
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasuresCount).isEqualTo(1)
+        }
+    }
+
+    @Test
     fun regularCompositionIsUsedInPrefetchTimeCalculation() {
         val itemProvider = itemProvider({ 1 }) {
             Box(Modifier.fillMaxSize())
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 e5d86d8..077a75f 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
@@ -20,8 +20,10 @@
 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
 import androidx.compose.runtime.Composable
 
+// TODO (b/269341173) remove inline once these composables are non-trivial
+
 @Composable
-internal actual fun ContextMenuArea(
+internal actual inline fun ContextMenuArea(
     manager: TextFieldSelectionManager,
     content: @Composable () -> Unit
 ) {
@@ -29,7 +31,7 @@
 }
 
 @Composable
-internal actual fun ContextMenuArea(
+internal actual inline fun ContextMenuArea(
     manager: SelectionManager,
     content: @Composable () -> Unit
 ) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 910e79f..748d6b1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -89,7 +89,7 @@
 )
 
 private class BackgroundElement(
-    private val color: Color? = null,
+    private val color: Color = Color.Unspecified,
     private val brush: Brush? = null,
     private val alpha: Float,
     private val shape: Shape,
@@ -116,7 +116,7 @@
     }
 
     override fun hashCode(): Int {
-        var result = color?.hashCode() ?: 0
+        var result = color.hashCode()
         result = 31 * result + (brush?.hashCode() ?: 0)
         result = 31 * result + alpha.hashCode()
         result = 31 * result + shape.hashCode()
@@ -133,7 +133,7 @@
 }
 
 private class BackgroundNode(
-    var color: Color?,
+    var color: Color,
     var brush: Brush?,
     var alpha: Float,
     var shape: Shape,
@@ -155,7 +155,7 @@
     }
 
     private fun ContentDrawScope.drawRect() {
-        color?.let { drawRect(color = it) }
+        if (color != Color.Unspecified) drawRect(color = color)
         brush?.let { drawRect(brush = it, alpha = alpha) }
     }
 
@@ -166,7 +166,7 @@
             } else {
                 shape.createOutline(size, layoutDirection, this)
             }
-        color?.let { drawOutline(outline, color = it) }
+        if (color != Color.Unspecified) drawOutline(outline, color = color)
         brush?.let { drawOutline(outline, brush = it, alpha = alpha) }
         lastOutline = outline
         lastSize = size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index 5ef5aa7..86fda0b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -46,7 +46,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.platform.inspectable
 import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.disabled
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.onLongClick
@@ -645,7 +645,7 @@
     private var onClickLabel: String?,
     private var role: Role?,
     private var onClick: () -> Unit
-) : DelegatingNode(), SemanticsModifierNode, PointerInputModifierNode, KeyInputModifierNode {
+) : DelegatingNode(), PointerInputModifierNode, KeyInputModifierNode {
     abstract val clickablePointerInputNode: AbstractClickablePointerInputNode
     abstract val clickableSemanticsNode: ClickableSemanticsNode
 
@@ -695,9 +695,6 @@
         interactionData.currentKeyPressInteractions.clear()
     }
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = clickableSemanticsNode.semanticsConfiguration
-
     override fun onPointerEvent(
         pointerEvent: PointerEvent,
         pass: PointerEventPass,
@@ -812,26 +809,26 @@
         this.onLongClick = onLongClick
     }
 
-    override val semanticsConfiguration
-        get() = SemanticsConfiguration().apply {
-            isMergingSemanticsOfDescendants = true
-            if (this@ClickableSemanticsNode.role != null) {
-                role = this@ClickableSemanticsNode.role!!
-            }
-            onClick(
-                action = { onClick(); true },
-                label = onClickLabel
-            )
-            if (onLongClick != null) {
-                onLongClick(
-                    action = { onLongClick?.invoke(); true },
-                    label = onLongClickLabel
-                )
-            }
-            if (!enabled) {
-                disabled()
-            }
+    override val shouldMergeDescendantSemantics: Boolean
+        get() = true
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        if (this@ClickableSemanticsNode.role != null) {
+            role = this@ClickableSemanticsNode.role!!
         }
+        onClick(
+            action = { onClick(); true },
+            label = onClickLabel
+        )
+        if (onLongClick != null) {
+            onLongClick(
+                action = { onLongClick?.invoke(); true },
+                label = onLongClickLabel
+            )
+        }
+        if (!enabled) {
+            disabled()
+        }
+    }
 }
 
 private sealed class AbstractClickablePointerInputNode(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
index 55ea39a..de22bba 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Focusable.kt
@@ -51,6 +51,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.platform.inspectable
 import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.requestFocus
 import kotlinx.coroutines.launch
@@ -246,9 +247,9 @@
     }
 
     // TODO(levima) Remove this once delegation can propagate this events on its own
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = focusableSemanticsNode.semanticsConfiguration
-
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        with(focusableSemanticsNode) { applySemantics() }
+    }
     // TODO(levima) Remove this once delegation can propagate this events on its own
     override fun onGloballyPositioned(coordinates: LayoutCoordinates) {
         focusedBoundsNode.onGloballyPositioned(coordinates)
@@ -362,11 +363,10 @@
         this.isFocused = focused
     }
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = semanticsConfigurationCache.apply {
-            focused = isFocused
-            requestFocus {
-                this@FocusableSemanticsNode.requestFocus()
-            }
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        focused = isFocused
+        requestFocus {
+            this@FocusableSemanticsNode.requestFocus()
         }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 4023a81..9b61189 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.ReusableContentHost
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -95,13 +96,11 @@
             val index = itemProvider.findIndexByKey(key, lastKnownIndex).also {
                 lastKnownIndex = it
             }
-
-            if (index < itemProvider.itemCount) {
-                val key = itemProvider.getKey(index)
-                if (key == this.key) {
-                    StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
-                        itemProvider.Item(index)
-                    }
+            val indexIsUpToDate =
+                index < itemProvider.itemCount && itemProvider.getKey(index) == key
+            ReusableContentHost(active = indexIsUpToDate) {
+                StableSaveProvider(StableValue(saveableStateHolder), StableValue(key)) {
+                    itemProvider.Item(index)
                 }
             }
             DisposableEffect(key) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
index cb376a7..23c078e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -181,7 +181,8 @@
  */
 @Deprecated(
     "Please use the overload without pageCount. pageCount should be provided " +
-        "through PagerState.", ReplaceWith(
+        "through PagerState.",
+    ReplaceWith(
         """HorizontalPager(
             modifier = modifier,
             state = state,
@@ -202,8 +203,9 @@
             "androidx.compose.foundation.layout.PaddingValues",
             "androidx.compose.foundation.pager.PageSize",
             "androidx.compose.foundation.pager.PagerDefaults"
-        )
-    )
+        ),
+    ),
+    level = DeprecationLevel.ERROR
 )
 @Composable
 @ExperimentalFoundationApi
@@ -387,7 +389,8 @@
             "androidx.compose.foundation.pager.PageSize",
             "androidx.compose.foundation.pager.PagerDefaults"
         )
-    )
+    ),
+    level = DeprecationLevel.ERROR
 )
 @Composable
 @ExperimentalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index 23e45b8..1e9132b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -103,7 +103,7 @@
             ){
                 // provide pageCount
             }"""
-    )
+    ), level = DeprecationLevel.ERROR
 )
 @ExperimentalFoundationApi
 @Composable
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
index f8831fe..3616033 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/SelectableTextAnnotatedStringNode.kt
@@ -30,9 +30,7 @@
 import androidx.compose.ui.node.DrawModifierNode
 import androidx.compose.ui.node.GlobalPositionAwareModifierNode
 import androidx.compose.ui.node.LayoutModifierNode
-import androidx.compose.ui.node.SemanticsModifierNode
 import androidx.compose.ui.node.invalidateMeasurement
-import androidx.compose.ui.semantics.SemanticsConfiguration
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.TextLayoutResult
@@ -59,8 +57,7 @@
     onPlaceholderLayout: ((List<Rect?>) -> Unit)? = null,
     private val selectionController: SelectionController? = null,
     overrideColor: ColorProducer? = null
-) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode,
-    SemanticsModifierNode {
+) : DelegatingNode(), LayoutModifierNode, DrawModifierNode, GlobalPositionAwareModifierNode {
 
     private val delegate = delegate(
         TextAnnotatedStringNode(
@@ -96,9 +93,6 @@
         constraints: Constraints
     ): MeasureResult = delegate.measureNonExtension(this, measurable, constraints)
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = delegate.semanticsConfiguration
-
     override fun IntrinsicMeasureScope.minIntrinsicWidth(
         measurable: IntrinsicMeasurable,
         height: Int
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
index 93b37e5..5068c23 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextAnnotatedStringNode.kt
@@ -43,7 +43,7 @@
 import androidx.compose.ui.node.invalidateLayer
 import androidx.compose.ui.node.invalidateMeasurement
 import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.text
 import androidx.compose.ui.text.AnnotatedString
@@ -207,7 +207,6 @@
         callbacksChanged: Boolean
     ) {
         if (textChanged) {
-            _semanticsConfiguration = null
             invalidateSemantics()
         }
 
@@ -230,11 +229,9 @@
         }
     }
 
-    private var _semanticsConfiguration: SemanticsConfiguration? = null
-
     private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
 
-    private fun generateSemantics(text: AnnotatedString): SemanticsConfiguration {
+    override fun SemanticsPropertyReceiver.applySemantics() {
         var localSemanticsTextLayoutResult = semanticsTextLayoutResult
         if (localSemanticsTextLayoutResult == null) {
             localSemanticsTextLayoutResult = { textLayoutResult ->
@@ -245,24 +242,10 @@
             }
             semanticsTextLayoutResult = localSemanticsTextLayoutResult
         }
-        return SemanticsConfiguration().also {
-            it.isMergingSemanticsOfDescendants = false
-            it.isClearingSemantics = false
-            it.text = text
-            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
-        }
+        text = this@TextAnnotatedStringNode.text
+        getTextLayoutResult(action = localSemanticsTextLayoutResult)
     }
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() {
-            var localSemantics = _semanticsConfiguration
-            if (localSemantics == null) {
-                localSemantics = generateSemantics(text)
-                _semanticsConfiguration = localSemantics
-            }
-            return localSemantics
-        }
-
     fun measureNonExtension(
         measureScope: MeasureScope,
         measurable: Measurable,
@@ -401,7 +384,7 @@
                         decoration = textDecoration
                     )
                 } else {
-                    val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
+                    val overrideColorVal = overrideColor?.produce() ?: Color.Unspecified
                     val color = if (overrideColorVal.isSpecified) {
                         overrideColorVal
                     } else if (style.color.isSpecified) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
index 09d2c75..e046446 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/TextStringSimpleNode.kt
@@ -43,7 +43,7 @@
 import androidx.compose.ui.node.invalidateLayer
 import androidx.compose.ui.node.invalidateMeasurement
 import androidx.compose.ui.node.invalidateSemantics
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.text
 import androidx.compose.ui.text.AnnotatedString
@@ -168,7 +168,6 @@
         layoutChanged: Boolean
     ) {
         if (textChanged) {
-            _semanticsConfiguration = null
             invalidateSemantics()
         }
 
@@ -190,11 +189,9 @@
         }
     }
 
-    private var _semanticsConfiguration: SemanticsConfiguration? = null
-
     private var semanticsTextLayoutResult: ((MutableList<TextLayoutResult>) -> Boolean)? = null
 
-    private fun generateSemantics(text: String): SemanticsConfiguration {
+    override fun SemanticsPropertyReceiver.applySemantics() {
         var localSemanticsTextLayoutResult = semanticsTextLayoutResult
         if (localSemanticsTextLayoutResult == null) {
             localSemanticsTextLayoutResult = { textLayoutResult ->
@@ -206,24 +203,10 @@
             }
             semanticsTextLayoutResult = localSemanticsTextLayoutResult
         }
-        return SemanticsConfiguration().also {
-            it.isMergingSemanticsOfDescendants = false
-            it.isClearingSemantics = false
-            it.text = AnnotatedString(text)
-            it.getTextLayoutResult(action = localSemanticsTextLayoutResult)
-        }
+        this.text = AnnotatedString(this@TextStringSimpleNode.text)
+        getTextLayoutResult(action = localSemanticsTextLayoutResult)
     }
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() {
-            var localSemantics = _semanticsConfiguration
-            if (localSemantics == null) {
-                localSemantics = generateSemantics(text)
-                _semanticsConfiguration = localSemantics
-            }
-            return localSemantics
-        }
-
     /**
      * Text layout happens here
      */
@@ -317,7 +300,7 @@
                         textDecoration = textDecoration
                     )
                 } else {
-                    val overrideColorVal = overrideColor?.invoke() ?: Color.Unspecified
+                    val overrideColorVal = overrideColor?.produce() ?: Color.Unspecified
                     val color = if (overrideColorVal.isSpecified) {
                         overrideColorVal
                     } else if (style.color.isSpecified) {
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
index b285b1c..fff404c 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/window/WindowDraggableArea.desktop.kt
@@ -36,6 +36,7 @@
  * WindowDraggableArea is a component that allows you to drag the window using the mouse.
  *
  * @param modifier The modifier to be applied to the layout.
+ * @param content The content lambda.
  */
 @Composable
 fun WindowScope.WindowDraggableArea(
diff --git a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
index dd46067..5c61695 100644
--- a/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
+++ b/compose/lint/common-test/src/main/java/androidx/compose/lint/test/Stubs.kt
@@ -219,6 +219,69 @@
         """
     )
 
+    val Composables: TestFile = bytecodeStub(
+        filename = "Composables.kt",
+        filepath = "androidx/compose/runtime",
+        checksum = 0x92d0959f,
+        source = """
+        package androidx.compose.runtime
+
+        @Composable
+        inline fun <T> key(
+            @Suppress("UNUSED_PARAMETER")
+            vararg keys: Any?,
+            block: @Composable () -> T
+        ) = block()
+
+        @Composable
+        inline fun ReusableContent(
+            key: Any?,
+            content: @Composable () -> Unit
+        ) {
+            content()
+        }
+
+        @Composable
+        inline fun ReusableContentHost(
+            active: Boolean,
+            crossinline content: @Composable () -> Unit
+        ) {
+            if (active) { content() }
+        }
+        """,
+        """
+        META-INF/main.kotlin_module:
+        H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuWSSMxLKcrPTKnQS87PLcgvTtUr
+        Ks0rycxNFeJ1BgskJuWkFnuXCHEFpeam5ialFnmXcPFxsZSkFpcIsYUASe8S
+        JQYtBgDjkUhNXwAAAA==
+        """,
+        """
+        androidx/compose/runtime/ComposablesKt.class:
+        H4sIAAAAAAAA/51VzXLbVBT+rvwnK04sy05xXEhT103tGCrHpfzUaaBNKfHg
+        BiY2gWlW17IaFNtSR5I9ZZdhwzN0yxPArrBgMmHHg/AUDMORLCdO7GkK49H5
+        P+d+55wr+c9/fvsdwPv4jGGVmx3bMjovVM3qP7ccXbUHpmv0dXXL13m7pztf
+        uDEwBvmQD7na4+aB+mX7UNfIGmIIdfXvGT4v7jcuumuNruX2DFM9HPbVZwNT
+        cw3LdNTHgVSplaZTGFobrXvT9s3/UX+j3GrVNmslogw3G2/QKcXdaFj2gXqo
+        u22bG1SNm6bl8lHlHcvdGfR6FBWmph0REsPyBAbDdHXb5D21bro2JRuaE0OC
+        YVH7Tte6QfZX3OZ9nQIZbhVn9HRmaXpFDmqlvQQWkJQwD5kh0u5ZWleEcv7o
+        Ge3HkGGIGubQ6uoMmeKMaSdwBW/NYRFZBrFgFJ4V/GWyOkN6xrwZVi5bKUNy
+        Vx/4w9yyaBymy3B3VpuXXY09hkf/PW9j7P/aNFxv91Sm8LqN0kKCxce0EV4R
+        eZqWP4ypTpRZM0lfCNu2HAotFJ++SY93Lg2b1VJ2FrzRuVFOaUNaOHvKkBrn
+        PtFd3uEuJ7hCfxiit595RPQIKLbrCQI5XxieVCGps87w1/HRmnR8JAmyMGJE
+        PJ4VRo9IjzwX8JTHcyWKyQkVVhVlIRfOskqompLDuXklrJC1Ejn5KSqI0e2T
+        H8U/XjEKLckxPzwqi8Tj1bQYfl34Q6oqbEv5sHh8JEvVK/JcLqGIIlP8oyqJ
+        /PjI+VGNbWmqhqfKCyc/CDEpIp68rFaY122VeYNQxgObvNGsxbAw8Tm83aU5
+        h7esDk052TBMfWfQb+t2y3N6JSyN9/a4bXh6YIw3jQOTuwOb5Ku7o+9O3Rwa
+        jkHuB2cXku7NRe/p9+Jc2HzT5Vr3CX8eHCA1rYGt6Y8NT1kKauxN1cc6BIS9
+        pSOEJUQQJW2DtCbZvc0vrilzr5AqK2miofvlX7HE8LN3OXCfaJRmtIA4Nkle
+        oYQFxJDDVfJSKiS87ZdehIJ3KPITPy+GT4NMkfgDeuYFUuL+zfPoEpZxLcCx
+        G+CQy8qNCQTf/HIKQSIuQkYSqVMYIv0KAQyZurrpw5CRn4CxMhvG9QkYq7gV
+        wGgHMDJjGLmXkCaghPBwVOxvpNkErCzSVOcMVgKlAFYGayj7sDLnYBWnYMWF
+        U0gCtnxawyPi35L1XeruvX2E6rhdh1pHBet1VHGnTn/nd/fBHHyAD/eRdLDs
+        4CMHEZ/mHXzsQHSw6mDNt9xzIPmC4iD6Lx9Lm/oRCAAA
+        """
+    )
+
     val Modifier: TestFile = bytecodeStub(
         filename = "Modifier.kt",
         filepath = "androidx/compose/ui",
@@ -382,7 +445,7 @@
     val Remember: TestFile = bytecodeStub(
         filename = "Remember.kt",
         filepath = "androidx/compose/runtime",
-        checksum = 0xc78323f1,
+        checksum = 0x736631c7,
         source = """
         package androidx.compose.runtime
 
@@ -392,72 +455,71 @@
         inline fun <T> remember(calculation: () -> T): T = calculation()
 
         @Composable
-        inline fun <T, V1> remember(
-            v1: V1,
-            calculation: () -> T
+        inline fun <T> remember(
+            key1: Any?,
+            crossinline calculation: () -> T
         ): T = calculation()
 
         @Composable
-        inline fun <T, V1, V2> remember(
-            v1: V1,
-            v2: V2,
-            calculation: () -> T
+        inline fun <T> remember(
+            key1: Any?,
+            key2: Any?,
+            crossinline calculation: () -> T
         ): T = calculation()
 
         @Composable
-        inline fun <T, V1, V2, V3> remember(
-            v1: V1,
-            v2: V2,
-            v3: V3,
-            calculation: () -> T
+        inline fun <T> remember(
+            key1: Any?,
+            key2: Any?,
+            key3: Any?,
+            crossinline calculation: () -> T
         ): T = calculation()
 
         @Composable
         inline fun <V> remember(
             vararg inputs: Any?,
-            calculation: () -> V
+            crossinline calculation: () -> V
         ): V = calculation()
         """,
-"""
-        META-INF/main.kotlin_module:
-        H4sIAAAAAAAAAGNgYGBmYGBgBGJWKM3ApcYlkZiXUpSfmVKhl5yfW5BfnKpX
-        VJpXkpmbKsQVlJqbmpuUWuRdwqXJJYyhrjRTSMgZwk7xzU/JTMsEK+XjYilJ
-        LS4RYgsBkt4lSgxaDACMRj6sewAAAA==
-        """,
         """
-        androidx/compose/runtime/RememberKt.class:
-        H4sIAAAAAAAAAK1WXVPbRhQ9K38J29jCfJSYJiFgyndkG5q2MZBSWhpPCekE
-        j9opT8JWqMCWMlrZk0emL33pH+hrf0Ef0z50GPrWH9XpXVlgg4UJTTzW3qvr
-        u+ecu3el9T///vkXgFV8xTCtWzXHNmuv1ardeGVzQ3Walms2DPWF0TAaB4bz
-        jRsDY1CO9Jau1nXrUH1+cGRUKRpikB0/i2F1bufYduumpR61GurLplV1Tdvi
-        6rbv5UvzO1cxSgyba5XHvfGNm8DWFiuV0kZpnkaGmZ1rq9jy7vWDukF50zu2
-        c6geGe6Bo5uEpluW7ept5F3b3W3W65SVqOr1arPuxWXEGe51STEt13Asva6W
-        LdchDLPKY0gyjFZ/NKrHPsi3uqM3DFesyuxcb3FdkT0Bclia15JIIR3HIJTL
-        fAGlx5BhiJpWyz42GEbmApY1iRGMJjCMMYbBnJl7mev0iZUZJm9qFcN2kPD/
-        0+AfAhusFQK7XtEKN7Fc6rzUKjBkgmi/77/w71IRf/uKtOK1ZVa04i1LLTIc
-        vV1V76fOX96xTm2lb/EVbeWWC7DC8PXc/nuqrrKmBcq7PT6p1DyVWsl7MF81
-        XS5jlmE4AIth6BzumeHqNd3VRW2NVoheyUwMEXpGj4UjUfy1Kbw8ebUCY+7p
-        yVhckkNxaVwim4xLyhBdpyfx05PsDNms9JRNheXTE4UVk4qUlTPhDIXyoadn
-        P8t/v2GnJ2e/RSUlnF25nEyGKZFiVIlSMNJvaiy7GTSVRkmRLwCiygBZuR9Q
-        PPv8eiAaQ0qiBy6qJMkm+sEOZtfbsKk2bKo4pqSzyYwss0x4nOWH8sqUZ7tA
-        UldBMmc/SbF4RD77tZhnYu3p0WMV8abxW9f9ppS0ghiKYqAdyjRGjUTi/Px8
-        eOwyhLfsGr2s0zumZew2RbgiziSBaNNxo+mOKe794MCeeWjpbtMhf+JF+yQr
-        Wy2Tm/TzZufQYshd/fXi6LmUFt+zm07V2DYF+h1/jtaDhwIkhCE+EdyhK0p3
-        63S3RXGJbHohk3iDodDawh/4gOF3sUmxQWOUKpYRwxPyxygm/HGCYGIS4siS
-        /dzLjmHzIh/4gq4YrRkGyBGMEz7jM0oVOz+92GZcXwxkHPQYJyn1nNGTibu4
-        55XR5mY+94c93AMivcN+32f/zl+H9FKbfWMpkH3EY1+g1HP20BX2B+R11kDy
-        dUz26EiEvAkdJVO+kgNKj5BVlttKHoWXA6QMUGlPvL90EfLbUoR+5UKKciFF
-        QY48yfOEqJAvarpH1GB7K3TLmvFl7fntGV3IzJGsfk1KUS3nTUp1NWkUs5j3
-        4EcvNemjXh2Sr6A9SrQdxbiGL8nWKLpAyhb3ESpjqYxlGvGwDBX5Mm3o4j4Y
-        xwpW9zHMEeH4mCPO8YgjyvEJx12OCY5POR5w3Of4jCPHMcXxmGOWo+R9Z/4D
-        oSNh5zILAAA=
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgUuOSSMxLKcrPTKnQS87PLcgvTtUr
+                Ks0rycxNFeIKSs1NzU1KLfIu4eLjYilJLS4RYgsBkt4lSgxaDAC9VMzjUAAA
+                AA==
+                """,
+        """
+                androidx/compose/runtime/RememberKt.class:
+                H4sIAAAAAAAA/61WWXPbVBT+rrzJjhfFWUgcCCFL62yV44YCdeoSAqEe3MDU
+                HsNMnmRbTeVF6kiyKW8ZXvgNvPILeCw8MJnwxo9iOFeWHW9JXdIHXZ17dO53
+                vnO/u+iff//8C8A+vmRYU/SqaWjVV3LFaL40LFU2W7qtNVX5mdpUm2XV/MYO
+                gDFINaWtyA1FP5O/LdfUCnk9DKLpRjHsJ/N1w25oulxrN+XnLb1ia4Zuyceu
+                lcps5ocxMgyHB8WHo/7sm8AOtovFTDazSS3DRv7aKo6cvlJuqBS3ljfMM7mm
+                2mVT0QhN0XXDVjrIJ4Z90mo0KGqqojQqrYbjFxFiWO6joum2aupKQ87ptkkY
+                WsUKIMwwV3mhVuouyHeKqTRVm8/K3eRocX2eAgc5y2yWwogiFkIE0mC+MaUH
+                EGfwa3rbqKsMs8kx0xrGLOamMIN5hsi6tv58/UonlmNYeZNUDMfjiP8fgQvX
+                Cfy28AOSr98kJUngKu6tqz/tMcTHEfvhZmluU7M6ec23mgVeXpqhNhnwu6nt
+                x9vUdutq7zN8nTx9R6UUD0pjS3l7fOJZcniWMs7efNmyLRFJhpkxWAzTXbin
+                qq1UFVshn9Bse+hUZrwReQPaq3VuCPTxlcatFFnVPcZeXJwnQoLoCQkLAr3p
+                kS7OyQiQMR2ijxvUTwhP2KpXvDiXWDosCQkx7o2TK+V5cvmL+PdrdnF++Ztf
+                kLyJvaFgUfIlvAss5U9HRe/1AwOJzLUDRUmcBCKY+GoCCFEKTQI2lXjcAQt3
+                wMLpeSmSCMdFkcWd4ano6ihMeBgmdvmzEAj5xMtf0ynG55s2GCvyM8TVrP+U
+                5BqV6MroXpb36jat0yOjSidzLK/p6kmLu4v8OOIQBt0tJcXUeN91Bgvama7Y
+                LZPspWedayuntzVLo8+HV8canXnDX3v3zEBYqGC0zIp6rHH0RXdMaQQPexDg
+                5esMPizS46feAfWOyC/QO7YVn3qNac/B1h94j+F3vhLxiFo/VS0igCzZ8+Tj
+                9gJBMD4IISTo/diJDuDzXjxwSE+AJgxBMnjGJTfjUwrlyzu23cn4aHtsxoiT
+                cYVCuxkdmljGh04ZndzMzf3+SO6IQJ0PnI3VZbDiMvjenYvYTodBdmcsg1mH
+                wRaFdhl4hhisknU1D4LL5aMRLjOeHpdBRmsuozIN89Fb2u0weuDdHUMpSGVm
+                nX85H9kdSrwOqUdJ6lGSsEGW4FicnMcltz5CbtE7RG6Q4h2XYsGVbW4rvkkU
+                bxIvSiy64kX7xJtDkqYTjtUv3t3x4gV7PAR84bQZWq7AKXm3idnOKTw57OZw
+                LwcZqRyt8XQO97FPARY+xoNTSBZ8Fj6xELLwqQW/hWULn1lYsrBqYcXChoU1
+                Cw8tJB3/nf8A7cabHS4LAAA=
         """
     )
 
     val SnapshotState: TestFile = bytecodeStub(
         filename = "SnapshotState.kt",
         filepath = "androidx/compose/runtime",
-        checksum = 0x9907976f,
+        checksum = 0x3a5656cc,
         source = """
         package androidx.compose.runtime
 
@@ -500,6 +562,29 @@
             }
         }
 
+        @Composable
+        fun <T> produceState(
+            initialValue: T,
+            key1: Any?,
+            key2: Any?,
+            producer: suspend ProduceStateScope<T>.() -> Unit
+        ): State<T> {
+            return object : State<T> {
+                override val value = initialValue
+            }
+        }
+
+        @Composable
+        fun <T> produceState(
+            initialValue: T,
+            vararg keys: Any?,
+            producer: suspend ProduceStateScope<T>.() -> Unit
+        ): State<T> {
+            return object : State<T> {
+                override val value = initialValue
+            }
+        }
+
         interface ProduceStateScope<T> : MutableState<T> {
             suspend fun awaitDispose(onDispose: () -> Unit): Nothing
         }
@@ -516,181 +601,487 @@
         """,
         """
                 META-INF/main.kotlin_module:
-                H4sIAAAAAAAAAGNgYGBmYGBgBGJ2KM3Apc8ln5iXUpSfmVKhl5yfW5BfnKqX
-                mJeZm1iSmZ8HFClKFeJxBPMTk3JSvUu4zLkkMDQUleaVZOamCnEFpeam5ial
-                FnmXCPEH5yUWFGfklwSXJJaANCpg0ViaqVeal1kixOJS4F2ixKDFAAA4Rqdc
-                pAAAAA==
+                H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMueSSMxLKcrPTKnQS87PLcgvTtUr
+                Ks0rycxNFeIKSs1NzU1KLfIuEeIPzkssKM7ILwkuSSxJ9S7h4uNiKUktLhFi
+                CwGS3iVKDFoMAJF5eAthAAAA
                 """,
                 """
                 androidx/compose/runtime/DerivedState.class:
-                H4sIAAAAAAAAAIVRTW/TQBB9Yzuxk4bghhbSAKVCQk044FJxQKSqhPgQkVIh
-                NVGElNM2XtJtHLvybqIe81s48CM4IKtHfhRinKIKERUuM/Nm37zdffPj57fv
-                AF5gh/BExGGaqPAiGCXT80TLIJ3FRk1l8Famai7DnhFGuiDC0UH/VfdMzEUQ
-                iXgcfDw5kyPTPlxtdW/UXIod9PvtwzbB/3vQhUPY/vewiyLBG0szENFMEjaa
-                rdUHEArNFt/CTH3N3GyuElsDQrHJzLxY704SE6k4OJJGhMIInremc5utojy4
-                BJpw60LlaI+r8DnhXbaolq26Vc4Wy2R5Be9zPVs8dbxs4dO+V3Nq1gfas47r
-                vt2wXmaLT5dfq5dfipWG4zl+4bHjFX03F9sn7N5s3Z/r4IdRn7DzH6NzH+ZX
-                n/d7sTjXp4lZHjybGEKpp8axMLOUj8u9ZJaO5HsVMdg6vhIZKK1OIvk6jhMe
-                Ukms2XoLBRBcNsDiZXkoMXqYI5QZr6FyjW/B/l3Z2F7mB3jE+Q0zqqxyewi7
-                A7+DdY6o5eFOBxvYHII07uLekLeHusaWRkPjvs5hSWNNo/ILV3pR08ICAAA=
+                H4sIAAAAAAAA/4VRTW/TQBB9Yzuxk4bghhbSAG1BQiQccKk4IFJVQnyISKmQ
+                mihCymkbL2Ebx668m6jH/BYO/AgOyOqRH4UYp6hCRIXLzLzZN29Hb378/PYd
+                wHM8IDwScZgmKjwPRsn0LNEySGexUVMZvJGpmsuwZ4SRLohwdNB/2T0VcxFE
+                Ih4HH05O5ci0D1db3Ws1l2IH/X77sE3w/x504RC2/z3sokjwxtIMRDSThI1m
+                a3UBQqHZ4l+Yqa+Ym81VYmtAKDaZmRfr3UliIhUHR9KIUBjB89Z0brNVlAcv
+                DyDQhPvnKkd7XIXPCG+zRbVs1a1ytlgmyyt4n+rZ4onjZQuf9r2aU7Pe0551
+                XPfthvUiW3y8+Fq9+FKsNBzP8QsPHa/ou7nYPuHx9f79eRPejvqE3f+4nZsx
+                v3TA78XiTH9OzPLh6cQQSj01joWZpfxc7iWzdCTfqYjB1vGlyEBpdRLJV3Gc
+                8JBKYs3+WyiwDy4bYPHFPJQYbecIZcZrqFzhG7B/VzZ2lvk+djm/ZkaVVW4O
+                YXfgd7DeQQ23uMRGB5u4PQRp3EF9yCfElkZD467GPZ3DksaaRuUXZ2oGC8cC
+                AAA=
                 """,
                 """
                 androidx/compose/runtime/DerivedStateImpl.class:
-                H4sIAAAAAAAAAI1R227TQBA9u3Yc16Spm6ZXyq1QmqQUl4oH1EZBXFQ1UgCp
-                iSJEn7aJ1W6T2JXXifqYr+AD+AKQQEg8oKiPfBRi7EQVECTy4Lkcn5kzs/Pj
-                57fvAB5jiyEvvGbgy+aF0/A7575ynaDrhbLjOi/dQPbcZjUUoVvunLeTYAy1
-                Ym23ciZ6wmkL78R5c3zmNsK90jhUmahvsVbbK+0x2H/XJ6EzrE/UIwmDwShK
-                T4Ylhvnc+Cz5OhFyJBUFWi5fT8HENQsJpBgSPdHuugyZ8boU0piZAofNoIen
-                UjFsTrZX9F60lnnihvVh+2wuPy5A6rk8zUVMdcVMFineekq7aPGv2UrLD9vS
-                c165oWiKUBDGOz2NLsgik2RgLYIuZJRtU9R8xPBu0E9bfIlbg37suGmYfGnQ
-                L+jmoG+zHTOjZ/gB2+bPpzOGra3wJ4P+5QeD2/rh6ih9e/k+TZBtcdtc0c2E
-                bazpZtLWI4UdEq0xbEz2HNGFq544V6d+GAMPWyHDVFWeeCLsBrS0/sJvkpup
-                SM993e0cu0FNHLfjs/gN0a6LQEb5CLSqfjdouPsySpYPh4p1qST9feZ5PklI
-                31PYptslQE9KH4+OSX6DHoljGRrFJqLr5ggpkefkrcJXTBc2v2D2U8zLkzWI
-                CaoukF0YspDBHBBHv3e1KMpiftTToSyqTBQ+Y/bjP9ulhoRRu2GTBcIWrwbb
-                HQ1m/Hco42ooA0t/DKWNIg2bsb+PB+T3ibFC2tePoJWxWsYNsrgZmVtl3Mad
-                IzCFNdw9wpTCnMI9hXWFtIrSrMK8wqLCzC+1g+gKTQQAAA==
+                H4sIAAAAAAAA/41S204TURRd58x0OoylDOWOeEORtgiDxAcDTY2XEJtUTWjT
+                GHk6tBM40M6QOacNj/0KP8Av0ERj4oNpePSjjHvahqg1sS97r71mn732ZX78
+                /PYdwCNsMeRE0IhC2bjw6mHrPFS+F7UDLVu+98KPZMdvVLTQfql13kyCMVQL
+                1d3yqegIrymCY+/N0alf13vFUao8Vt1CtbpX3GNw/36fhMmwNlaNJCwGqyAD
+                qYsMc9nRXnI1SsiSVAyMbK6Wgo1rDhJIMSQ6otn2GTKj71JIY2oCHC6DqU+k
+                YtgYb654XzSWfezr2qD8bDY3KkDq2Rz1RZnqKjNZILz5hGYx+p+my2ehbsrA
+                e+Vr0RBaEMdbHYMuyGJjxwYM7Iz4CxlH24QaDxne9bpphy9yp9ftO25bNl/s
+                dfOm3eu6bMfOmBn+km3zZ5MZyzWW+eNe9/KDxV3zYGUYvr18nybKdbhrL5t2
+                wrVWTTvpmrHCDolWGdbH20l85kogztVJqPvE1plmmKjI40DodkSTm8/DBrmp
+                sgz81+3WkR9VxVGzf5uwLpo1Eck4HpJOJWxHdX9fxsHSwUCxJpWkr0+DICQJ
+                GQYK23TABO2H/l9CdFHyWVoSxxIMwjbiE+eIKZLn5J38V0zmN75g+lM/L0/W
+                okzQ6w2y84MsZDATr57Q71UdQrOYG9b04suQT+Q/Y/rjP8ulBgnDcoMi88Qt
+                XDW2O2zM+m9T1lVTFhb/aMoYIgMP+n4dm+T3KWOZtK8fwihhpYQbJdzELYK4
+                XcIdrB6CKdzFvUNMKMworCncV0irOJxVmFNYUJj6BfKpA8lSBAAA
                 """,
                 """
                 androidx/compose/runtime/MutableState.class:
-                H4sIAAAAAAAAAIVRwW7TQBB9s3ZiJw3BDS2kAUqFhEg44FJxQKSqhBCISImQ
-                miiqlNM2McGNs66y66hHfwsHPoIDsnrkoxDjFFWIqHCZmbf75s3umx8/v30H
-                8BJ7hCdSTRZxOLnwx/H8PNaBv0iUCeeB30uMPI2CvpEmcECE3uHgdfdMLqUf
-                STX1P56eBWPTPlo/6t6ouRI7HAzaR22C93ejA5uw++9mB0WCOw3MUEZJQNhq
-                ttYfQCg0WzyFmfqaud1cJ7aGhGKTmXmx2Z3FJgqV3wuMnEgjuV/MlxZbRXlw
-                CDTjo4swR/tcTV4Q3mVptSzqopylqyTcgvupnqXPbDdLPTpwa3ZNfKB9cVz3
-                rIZ4laUnl1+rl1+KlYbt2l7hse0WPScXOyA8vdm6P9fBD6MBYe8/Ruc+LK8+
-                7/WVPNefY7O6eD4zhFI/nCppkgVfl/txshgH78OIwc7xlcgw1CFPfKNUzE1h
-                rDRbL1AAwWEDBC/LRYnRwxyhzHgDlWt8C9bvysLuKj/AI85vmVFlldsjWB14
-                HWxyRC0PdzrYwvYIpHEX90a8PdQ1djQaGvd1DksaGxqVX2k2HnjCAgAA
+                H4sIAAAAAAAA/4VR0WoTURA9c3eT3aQxbmOradRaBTHxwa3FBzGlIKIYSBCa
+                EIQ83SZrvM1mt+TeDX3cb/HBj/BBlj76UeJsKkUM1Zc7c+aeOTOc+fHz23cA
+                L/CQ8FhGk0WsJuf+OJ6fxTrwF0lk1Dzwe4mRJ2HQN9IEDojQOxy86p7KpfRD
+                GU39Dyenwdi0j9ZL3Ws1V2KHg0H7qE3w/m50YBN2/93soEhwp4EZyjAJCFvN
+                1voChEKzxVOYqa+Y2811YmtIKDaZmSeb3VlsQhX5vcDIiTSS+8V8abFVlD9u
+                /oBAM66fqxztczZ5TnibpdWyqItylq6CcAvup3qWPrXdLPXowK3ZNfGe9sVx
+                3bMa4mWWfrz4Wr34Uqw0bNf2Co9st+g5udgB4cn1/v15E96OBoS9/7idm7G8
+                dMDrR/JMf47N6uPZzBBKfTWNpEkW/F3ux8liHLxTIYOd40uRodKKJ76Oopib
+                VBxp9l+gwD44bIDgi7koMdrNEcqMN1C5wjdg/c4sPFjF+9jj+IYZVVa5OYLV
+                gdfBZgc13OIUWx1s4/YIpHEH9RGfEDsaDY27Gvd0DksaGxqVX4ySaL/HAgAA
                 """,
                 """
                 androidx/compose/runtime/MutableStateImpl.class:
-                H4sIAAAAAAAAAI1RXU8TQRQ9M7vdLrWUpXwjfqFIW8RF4oOBpkZNjE2KJrRp
-                jDwN7QYG2l3SmRIe+yv8Af4CTTQmPpiGR3+U8c62IWpN7MPej7Pn3nPv3B8/
-                v30H8BibDHkRNjuRbF74jah9FqnA73RDLduBv9fV4rAVVLXQQbl91kqCMdSK
-                tZ3KiTgXfkuER/6bw5OgoXdLo1BlrL7FWm23tMvg/V2fhM2wNlaPJBwGpyhD
-                qUsMc7nRWfJ1IuRIygRWLl9Pw8W1FBJIMyTORasbMGRH69LIYGoCHB6DrY+l
-                YtgYby/zXrSWexTo+qD9bC4/KkDquTzNRUx1xUwWKd58SrtY8a/pymmkWzL0
-                9wItmkILwnj73KILMmOSDOyUoAtpsi2Kmo8Y3vV7mRRf5Kl+L3bcdVy+2O8V
-                bLff89i2m7Wz/BXb4s8ns45nLfMn/d7lB4d79v7KMH17+T5DkJfinrtsuwnP
-                WbXdpGcbhW0SrTGsj/cc5sLVUJyp40jHwMNTzTBRlUeh0N0OLW2/iJrkpioy
-                DF5324dBp2aqzVmihmjVRUeafAimqlG30wheSpMs7Q8U61JJ+vssDCOSkFGo
-                sEW3S4CelD5ujkl+nR6JYwkWxS7MdXOElMhz8qnCV0wWNr5g+lPMy5N1iAnM
-                oxDbmIUsZoA4+r1riqJZzA17+pSZykThM6Y//rNdekAYths0mSds4WqwneFg
-                zn+Hcq6GcrD4x1DWMLKwEfv7eED+JTGWSfv6AawyVsq4QRY3jblVxm3cOQBT
-                WMXdA0wozCjcU1hTyCiTzirMKSwoTP0Cc/1T9U0EAAA=
+                H4sIAAAAAAAA/41S204TURRdZ2Y6HcbSDuWOeEORtgiDxAcDTY2aEJsUTWjT
+                GHk6tBM40M6QnlPCY7/CD/ALNNGY+GAaHv0o4z7Thqg1sS97r71mn732ZX78
+                /PYdwBNsMuR52OxEonnpN6L2eSQDv9MNlWgH/n5X8aNWUFVcBeX2eSsJxlAr
+                1nYqp/yC+y0eHvtvjk6DhtotjVKVseoWa7Xd0i6D9/f7JCyG1bFqJGEz2EUR
+                ClVimM2N9pKvU0KOpDQwc/l6Cg5uuEggxZC44K1uwJAdfZdCGpkJGPAYLHUi
+                JMP6eHPpfdFYznGg6oPyM7n8qACp5/LUF2XK68xkkfDGM5rFjD9NVc4i1RKh
+                vx8o3uSKE2e0L0y6INPG0QYM7Iz4S6GjLULNxwzv+r20aywYbr8XO8OxHWOh
+                3ytYTr/nsW0na2WNV2zLeDGZtT1zyXja7119sA3POlgehm+v3qeJ8lzDc5Ys
+                J+HZK5aT9CytsE2iNYa18Xaiz1wN+bk8iVRMbJ4phomqOA656nZocutl1CSX
+                qYgweN1tHwWdmn6tbxM1eKvOO0LHQ9KtRt1OI9gTOlg8GCjWhRT09XkYRiQh
+                olBiiw6YoP3Q/0uILko+R0sysAiTsAN94jwxJfIGebfwFZOF9S+Y+hTnFcja
+                lAnMYT22cRaymNarJ/R7VZfQDGaHNX19GfKJwmdMffxnudQgYVhuUGSOuPnr
+                xnaGjdn/bcq+bsrGwh9NmUNk4lHs17BBfo8ylkj75iHMMpbLuFXGbdwhiLtl
+                3MPKIZjEfTw4xITEtMSqxEOJtNThjMSsxLxE5hfups0nUgQAAA==
                 """,
                 """
                 androidx/compose/runtime/ProduceStateScope.class:
-                H4sIAAAAAAAAAI1T328SQRCeXSh3INUr/gJarVqNSox3Ep+EEI2GFEO1EfSF
-                p+U4cOHYJbd72Efin+KDf4PxwRB8848yzkFpY7G2DzezM/PNN/vju1+/v/8A
-                gKewQ6DARCeQvHNgu3I4ksqzg1BoPvTs/UB2QtdraKa9hitHngGEQLPcfFbv
-                szGzfSZ69tt233N1qbKaqp9KvBdq1vYXxOVms1QpEbBO9hsQJ3DvXBwGJAik
-                2SfG9SuuIhhu80F9ILXPhd0fD+1uKFzNpVB29XDllJZ1VwYy1Fx4yn4pkVyE
-                LAKUHq4eiUD3LNrysv5ecLyWs6aUC5V/D7pbl0HP7nu6HTCOA5gQUrPFsDeh
-                70eHR9jO/2BSR0hEbSx3sedp1mGaYY4OxzHUAImMQYAMMHXAo8jBVecJnnU6
-                2UrRLE1NJ0fOIsuIWlHG7Gank0LcnE4sUjQz8QzdJQ59vW3F8tSJF9PWWn6e
-                dQwnsTv7+vznNzKdzL4kqGXOPtN4ipq5aFqRwKPT9bIiRNw+aRK4fz6JIRoI
-                JKU4EkdmeR/HakAFNgQbqY9Sz5seDzT2NHhPMB0G2LP5bkFdE2OuOHK/OL5q
-                fK6T1X0WsKGnveAvWKohw8D1qtxHxtxhz4cVPpQzhTXctBG9EP4HJiQhBjcx
-                opCCbfQJrF5Afwu/dYpBOoLO7RIYg9tzfwPuoK9idR1JL7YgVoNLNbDQwkZk
-                MjW4DFdaQBRchWstSCq4riCrIKfAVJBXsKlga75I/gEnsHRUOwQAAA==
+                H4sIAAAAAAAA/41T328SQRCeXSgcSPWKv4BWq7ZGJcY7iU9CiEbTFENrI+gL
+                T8tx4MKxS273sI/EP8UH/wbjgyH45h9lnINeG4u1fbid2ZlvvtnZ/e7X7+8/
+                AOAZbBMoMtHxJe8cWo4cjqRyLT8Qmg9d68CXncBxG5ppt+HIkZsEQqBZaT6v
+                99mYWR4TPettu+86ulxdDtXPJN4LNGt7C+JKs1mulgmYp+uTECdw/0IcSUgQ
+                yLBPjOvXXIUwPObD+kBqjwurPx5a3UA4mkuhrJ0jzy5HeUf6MtBcuMp6JZFc
+                BCwElB8tj0Sgex5tJcq/Fxyv5bwulWL1342269LvWX1Xt33GsQETQmq2aLYf
+                eF44PMK2/geTOkQiai06xZ6rWYdphjE6HMdQAyRcjHABAmSA8UMe7mz0Ok9x
+                4OlkI01zND2dHBuTRDtqhhGjm5tOinFjOjFJycjGs3SX2PTNphkrUDteypgr
+                hXnUTtqJ3dnXFz+/kelk9iVBTWP2mcbT1MiH3UoEHp8tmiU14gykSeDBxXSG
+                aBwwJcWxQrLRpZxIAmXYEGykPko9L3oy0FjT4D3BdOBjzfq7BXVNjLniyP3y
+                5L7xzU5nD5jPhq52/b9g6YYMfMfd4R4y5o9qPizxoaYprOChk+Hb4M9gQApi
+                sIk7Cmm4gzaB2Uto7+K3SnGTmT9juEbAGNyb29uwhXYHs6tIerkFsRpcqYFZ
+                gzXIogtXa3ANrreAKLgBN1uQUpBTkFdQUGAoWFewoeDW3En9Ae+dQkpABAAA
                 """,
                 """
                 androidx/compose/runtime/SnapshotStateKt$produceState$1.class:
-                H4sIAAAAAAAAAI1T3U4TURD+znb7w1poqYCAf6gVtkVZICZqCiSGSNJYNaGk
-                MeFq2V3Kge1Zsnu24bJP4QP4BJpoTLwwDZc+lHHOtjEoCF7s/GXmm2/OzP74
-                +e07gCdYZXhqCzcMuHtiOUHnOIg8K4yF5B3Pagr7ODoIZFPa0nsly8dh4MaO
-                l7jllSwYFTcO7a5t+bZoW2/3Dj1H1hr/xlOFazs7tY0aQ/Hvwix0hjuXF2eR
-                YcisccHlBsOkeb57pUUJJvVQRsqstPLI4ZqBNPIM6a7txx5D6XxdHmMojEBD
-                kUGXBzxieH7JJJe+DE03WlYcue23Bh1zbU8OzQmzcr49cTMrxJo4J3K8cRRI
-                nwvrtSdt15Y2xbRON0VLY0pkGdgRhU648pbJclcY1vu9UaPfM7RpzdByepX1
-                ezljut9bzZX0kvas31tm21NFbVaZ707f66cfMoahFdOzei5V1BUI3cPcFQsk
-                Jub/PkwWZYb82ddh2L9gaxdEhvMfdjvWfiwcyQMRWVtDa7VWuYplHvNYoDP7
-                g9HSkWQYafK2sGUcEhl9M3BJFRpceG/izp4X7th7fnIigaO2F3LlD4P5uhBe
-                uOnbUeTRgRReCscPIi7atKWDwGUwmkEcOt4WV9kz2wNCLR5xKn8hREAc1BxY
-                oUNLg4H+IZTU5ZGu0iI1TNMHOll1iotkbZFWEaP6FaPVxS8Y/5TkPSI5BrX8
-                eehYoPx5PCZvapBNqNeBxJo4g26QNZnkKGyLPEY6Xf2M8Y+/YTNJcCGByw8S
-                hnADkBvkLyXQLGkGzBAUiMZDmMOcFJYTXaFRgXXKnKGq2V2k6rhZxy2SuK3E
-                nTruYm4XLMI93N9FJlLmgwhjESYjTEUo/AJAwj8ArQQAAA==
+                H4sIAAAAAAAA/41T3U4TURD+znb7w1poqYCAiqhVtkVZqCZqCiSGSNJYNaGk
+                MeFq6S7lQHuW7DltuOxT+AA+gSYaEy9Mw6UPZZyzbQwKAhc7f5n55pszsz9/
+                ff8B4CmeMDxzhRcG3Dt2GkH7KJC+E3aE4m3fqQn3SO4HqqZc5b9W+aMw8DoN
+                P3LzK0kwKq4euF3Xabmi6bzbPfAbqlz9P54uXN3eLq+XGbL/FiZhMsxdXJxE
+                giGxygVX6wyT9tnuhTol2NRDGzG7UE8jhWsW4kgzxLtuq+Mz5M7WpTGGzAgM
+                ZBlMtc8lw4sLJrnwZWi60bzmyN1WfdAx1fTV0JywC2fbEze7QKyJcyTHq4eB
+                anHhvPGV67nKpZjR7sZoaUyLlBZgYIcUP+baWybLW2FY6/dGrX7PMqYNy0iZ
+                Rdbvpazpfq+Uypk543m/t8y2prLGrDbfn3wwTz4mLMvIxmfNVCxrapASw/wl
+                WyQ69lVfJ4kHDOnTT8Swd87qzokMH+Gg23b2OqKheCCkszm0SuXCZSzTWIBN
+                t/YXo6VDxTBS403hqk5IZMyNwCOVqXLhv+20d/1w291tRXcSNPQKQ679YTBd
+                EcIPN1qulD5dSeaVaLQCyUWTVrUfeAxWLeiEDX+T6+yZrQGhOpecyl8KERAH
+                PQdW6NritEP6kZDT50d6kRZpYJo+2jH0PT4ia5O0jljFbxgtLn7F+Oco7zHJ
+                MejlP4RJo46QXiJvapBNqNf1mZA1cQrdImsyytHYjr4i0vHiF4x/+gObiIIL
+                EVx6kDCEG4DcIN+JoFnUDJjBMkmTKBSGOTEaUesiSqTXKHOGqmZ3EKvgZgW3
+                KriNOTJxp4J53N0Bk7iH+ztISG3mJcYkJiWmJDK/AeLwkAKyBAAA
+                """,
+                """
+                androidx/compose/runtime/SnapshotStateKt$produceState$2.class:
+                H4sIAAAAAAAA/41T0U4TURA9d7ttl7XQUgEBFVGrbouyUEzUFEgMkaSxakKb
+                xoSnZbuWC+1dsnu34bFf4Qf4BZpoTHwwDY9+lHHutjEoCjzs3DOTmTPn3pn9
+                8fPbdwCPscbwxBGtwOetY9v1u0d+6NlBJCTvenZdOEfhvi/r0pHeS1k4CvxW
+                5HqxWyinwai4duD0HLvjiLb9Zu/Ac2Wl9n8+VbjeaFQ2Kwy5vwvT0BkWzi9O
+                I8WQWueCy02Gaets92KTEizqoUDCKjYzMHDFRBIZhmTP6UQeQ/5sXQYTyI5B
+                Q45Bl/s8ZHh2zk3OfRm63XhBaeROpznsaLQ9OYJTVvFse9JmFUk1aY7tZO3Q
+                lx0u7FeedFqOdCimdXsJGhpTxlAGDOyQ4sdceSuEWqsMG4P+uDnom9qsZmqG
+                XmKDvmHODvplI6/ntaeD/grbmclp8wq+PXmvn3xImaaWS87rRiKnK5Iyw+IF
+                UyQ51mVfJ417DJnTT8Rw/I/RXSoyepaDXtd+FwlXcl+E9vYIlSvFi3Rn8AAW
+                bd8fGpcPJcNYnbeFI6OA5OlbfouObI0L73XU3fOChrPXiTfHd9VQA678UTBT
+                FcILtjpOGHq0N9kXwu34IRdtGt6+32Iw634UuN42V9lzO0NBTR5yKn8uhE8a
+                1D2wSvuXpKnSr4W8Wkg6l2i0Gmbpo6lDbehDQtt0qohZ+orx0tIXTH6K8x6R
+                nYBahzJ0rFF+GcvkzQyzifWqWhxCU6fYTULTcY7ittVe0Zksfcbkx9+0qTi4
+                FtNlhgkjuiHJNfLtmJrFzYA5rJDVcR/FUU6CrqjOEskCNihzjqrmd5Go4noV
+                N6q4iQWCuFXFIm7vgoW4g7u7SIUKFkJMhJgOMRMi+wttlNMGxAQAAA==
+                """,
+                """
+                androidx/compose/runtime/SnapshotStateKt$produceState$3.class:
+                H4sIAAAAAAAA/41T3U4TURD+znbbLmuhpQJCVUStui3KQjFRU2hiiCSNVRNK
+                GpNeLdu1HGjPkt3Thss+hQ/gE2iiMfHCNFz6UMY528agIHix85eZb74zM/vj
+                57fvAB5jneGJI1qBz1vHtut3j/zQs4OekLzr2XXhHIX7vqxLR3ovZf4o8Fs9
+                14vc/HoSjIprB07fsTuOaNtv9g48V5Zr/8ZThRu7u+VKmSHzd2ESOsPixcVJ
+                JBgSG1xwWWGYtc52LzQowaIeyohZhUYKBq6YiCPFEO87nZ7HkD1bl8IU0hPQ
+                kGHQ5T4PGZ5d8JILJ0Ovm8wrjtzpNEYdjbYnx+aMVTjbnrhZBWJNnCM5XTv0
+                ZYcL+5UnnZYjHYpp3X6MlsaUMJQAAzuk+DFX3ipZrTWGzeFg0hwOTG1eMzVD
+                L7LhwDDnh4OSkdWz2tPhYJXtzGW0nDLfnrzXTz4kTFPLxHO6EcvoCqTEsHTJ
+                FomO9b/TSeIeQ+r0iBja56yuec4tjadw0O/a73rCldwXob09tkrlwmU0U3gA
+                i47tD0orh5Jhos7bwpG9gNjoW36LVLrGhfe6193zgl1nrxMdiu+qHQZc+eNg
+                qiqEF2x1nDD06EzSL4Tb8UMu2rSrfb/FYNb9XuB621xlL+yMCDV4yKn8uRA+
+                cVDvwBqdW5yWSH8Ssur+SC/TJjXM00dLhjrIh2Rtk1YRs/gVk8XlL5j+FOU9
+                IjkFtf1N6KhQ/iZWyJsbZRPqVXUnZM2cQjfJmo1yFLatzoh0vPgZ0x9/wyai
+                YCWCS40SxnAjkGvk2xE0i5oBC1glqeM+CuOcGD1R6SJKEUWaBlXlmohVcb2K
+                G1XcxCKZuFXFEm43wULcwd0mEqEy8yGmQsyGmAuR/gWU18KgswQAAA==
                 """,
                 """
                 androidx/compose/runtime/SnapshotStateKt.class:
-                H4sIAAAAAAAAAKVYW1PbVhD+jm1sIxwQBgKIBEjiBDAXG5ombXBJU3LB5drg
-                0KY0TYQtQGBLro5MkzemD33sj+gvaJ+SNDMdJn3rT+mP6HSPLBvb2AZSz0jn
-                tvudb/fs2dX473//+BPATRwwjKpGxjL1zMtY2szlTa7FrIJh6zkttm6oeb5r
-                2uu2amuLdgCMQd5TD9RYVjV2Yqtbe1qaZr0M7RnN0g+0jCO5us0wO7pUKzg7
-                ttRwp/sV6rMMjxKpOyf150ZTqbOCJEh0jpCuLZnWTmxPs7csVTd4TDUMk9Z1
-                k/orpr1SyGZJauxMmMlcPhtAK4M/oRu6PcfQU8/KjRDaEJIg4QLD9TMhB9DB
-                0HKgZgsaQ/gkJjk4V7DVraz2oQ5erlD/YAdXgpQc3Nh1ldJF112U0NvcKZU6
-                AfSTMyrtXtK5LWyfGm1CsipohQaRTDYw9zwwJYOjZ1cJYJDBO1qMiGEJQ7jC
-                0Flp0bKaFwZNnpkJKRCH54nFOvZs/C8bCTmRWpxNbZxyrLVKAdyQMCIsC+Ut
-                M1NIFy1j2K4ToXVm9k07qxuxvYNcbLtgpItX86Hbm2kWjqVg/qdxMJ97v8Rk
-                4/3WKsxbT5t59w5MliDTpmUWbN3QeGzeJBWj4CSaRFngCWUNUhivQ/ZUO0vh
-                d72x3LwzFrFFcpFmiY+ynisWdM/MCmKKYbDCObpha5ahZmNJw7YIQU/zAOKU
-                89K7WnrfzZ1rqqXmNBJkGGl+3OsCZMfJjjP4SMI0bjLcPmvpiVSGVmQ6gFsS
-                botUMtjcawF8SnEpsrWuZjeK+dW3r72aZhg+LfLIOTua7Sq9GD0tDhuHmqVt
-                Z2kcW6QAymuW/YoOu06KzzcI49MCY9yJjHNsn4hSuJGSKGT2rs7ngrhXjARn
-                OYh5hu7ROhxDmMODNtzBQ4YLET2yHTl2EEtSkYoIuIrJ4dMvb0CoED6D0thf
-                xI6XQXebeKSqzJ3nROrVcYYfz30kJ0vkOQ9GVGC6IysItUHBVyVHH5vvOvl4
-                YuTMVb+ztPOyZqsZ1VZpzpM78NKXIBOvAB3jvuh4aP6lLnp04T2ZacbeHB2u
-                SEeHkqfPI3mCPqctD71u6zluZVosrZce2VOlTnrKEMkpwbAv7FnwxNlVX/Do
-                UPbM+GWvQhPvf/V7ZJ8SllvKIn5XRBmUA0q7M9nqvKV4sLjUSg2TJUJuK2uF
-                6gFfUJ7J7WWRjmMRWYjMBOVOxdfH4uGZSblLGQuysBQuCfeU+vHe+MWwP+zI
-                xbsFbLBvIfDXG3Z06OzRr9yUFQHnokfL2xH+gIsvyZcUf5i8Eb+88P5nyVEc
-                VBLykEL4tYqh+ooly8oAw+9/8pCbg/3i/GaaBknNtzdLMUyc7/Nq/FwfL2yR
-                HrpcEJ+6bkBWZt0mm5+owLMVebxB+SWRwZLIg5e2RpXMNEr7pV45GHIVzal9
-                m6rEvJmhy9WxRIArhdyWZqXEVRKczbSoJZYuxu5k67q+Y6h2waL+wOMi26Rx
-                oHOdlu8dl16qy7Wr5RJaJRZKGoZmzWdVzjUaSutmwUprD3WxWb8LsXECnqqq
-                Bz6IH309owV+eJGj0S1qyeMIvYP0NPoa7UeQfxMXHQa9/c6aDFNIFOXQiTC1
-                eUcmgB9cqSC1/ehC90ncXoHbdwSlFrevIW5PDe4ALrm4w7QqfsF3GHr6GlcF
-                JqvAVFyEyzUI1xA5iTBCCKO1CJddhOs1CGOIkgcFwhohiRQYngjH3uDjd7gt
-                LPzkCHeqLfTjhmPhcFEas46Fopeghzm9KXxGGsUdx50dJepNCH70WPR0OFkY
-                k867SGUOd11jnrnH2hMNf0FUJsL36e2di77FI4ZqNu2IOWyEFe10cgtIOrx6
-                8CUWHV49WHJ59WAey2Venzs8urzu/tVcVrDqcsmRaAu1vVVcbvmik2/x2IPf
-                y2yEhR0UIRfpS810/mxooXEQ68RIGNOLFJ44jHrLjHpdRqInIsXrcltzuPX4
-                6nADCXFnO/qedMb9sB2lLArU/kLzG7T115vwJvFNEk/pjW+T2MR3SfLs95tg
-                HM/xYhNXOFo4VI4tjk4OP8cAR5rjGkeGQ+PY5ujieMLRzTHGscgxy5Hg2OGY
-                4tjl0Dn2nOE+R5RjjmOJY55jmeMuxwrH6n8UZ3xFehEAAA==
+                H4sIAAAAAAAA/91XbVMb1xV+riSkZS3DshgD6xgTW7ZBGISJa7dGpXVwiFXA
+                L4HQONhNFmmBBWlX3bsiuG+haZt/0Q/tL0g/JalnOoz7rT+l/QudTs9d7UpC
+                b0j2ZKZTzazu3XvPec5zXvbs3X/8569/A3ALf2CY0K2cY5u5w1TWLhRtbqSc
+                kuWaBSO1ZulFvmu7a67uGstuDIxB2dMP9FRet3ZSj7b2jCythhn6coZjHhg5
+                T/LRNsP8xEq94PzkSktL92vU5xneT6/fbdRfmFhf7xQkTaILhHRlxXZ2UnuG
+                u+XopsVTumXZtG/aNH9ouw9L+TxJTXaEmSkU8zH0MkTTpmW6CwxDzbzciOMM
+                4jJknGW42hFyDP0MPQd6vmQwqI2YFOBCydW38sbrBni1Rv21A1wLEgS4dehq
+                pcuhOy9juH1QanViGKVg1Pq9YnJX+D4z0YbkiaIVGkQy08LdbmACh5Odq8Qw
+                xhCeKFfEuIxLeJthoNajVb0oHJrumAkpEIdP0stN/Nl4Ix8JOb2+PL++cUpa
+                65ViuCbjuvAsXnTsXClb9oxhu0mFNlnZt928aaX2Dgqp7ZKVLT+aS/5srl05
+                BsX8z9bF3LW99HRre49r3FvL2kX/GZgOILO2Y5dc0zJ4atEmFavkNZp0ReBD
+                6hqkMNWE7Kl+BuV3tbXconcvaovkEu0aH3U9X0zyc+ZImGEYqwmOabmGY+n5
+                VMZyHUIwszyGWep52V0ju+/3zse6oxcMEmS43j7dawJkx+uOc3hHxk3cYrjT
+                6asnUVtaiZsx3JZxR7SSsfZRi+EHVJeiW5t6fqPcXyP7xoubDOOnVR7DYWcV
+                /F3U9L+7qun/uyp/zcKYi+HHMu6JwhBZnmPYaZLCze8kY/9qnbHuDf7PJ0iE
+                l0tYeu1UvRPDAxkZkarBJvGhzrRjuP4T++nEaeFvHWDH2M7TfWqZwlY0HPcF
+                udjEWrFF9k4Lx5QXjy7Mp5MUZFISp0h31+QLEh6V27C3LeEJw7mJJhzjWMHa
+                GdzFOsPZhJnYTlQDxDJ0QkwIuJrF8dNrNiZUCJ9Bax0vYscroLttInLijNlN
+                Rpodohk+6zoljefTLhMjjr/0gnqG+Blo+CQIdNV9P8jVhesdH7kHAsurhqvn
+                dFentVDhIEyfYUz8SeIPlMt9MQnR5qEpZvTKDeVustCfjo8+ko+P5NBISA5J
+                EW+s3Ib9MVQdFbEZLASXEqoqKAMVHLrXLpGCJqkRNfQgNMsuR6TjIyU0F1XC
+                Gi28+nM0pEQ0VempiER9EW1MiWl93mKv9y/PSuWtXhqYIhPymYpWvBnwWW1H
+                6auI9FdFFCEyJykDWmSEzao0G/Rn08o5bVJiqqwGaueD+ezI7LAaVT252SFh
+                QBp9EPv7N+z4yLOmac87s/YGNi5o++1snFfe0uKqRNhlhIuX39jimHZLuSTA
+                fVvJinHyaNz3SFbe1qIqZXv28oNXX8qe4hUtrSQ0wq9XjDdXDDJXAbj66osQ
+                lZE0Kgp1ru0jUfeZz6iV3ejuS26qq+8ktkwXtRKIr2r/8as94LUx3vCWna85
+                MrZ4xZLIWCDy3qFr0KHZtgJ76y88DOUEzZl9l96li3aOWkn/CgE+LBW2DGdd
+                NA7B2c6KY6tjint/sXfN3LF0t+TQ/MIHZbYZ68DkJm3fq57y6ROgfrdyWj8h
+                Fs9YluEs5nXODbqV1+ySkzWWTGFs1IfYaICnA3wIEdGy0ItR9CCKMH5Fd7dp
+                pIgj/hLy0+TX6DuG8pXoaPg1/Ue9PQW/ERJlOQxApfFzTyaGI19KonEUgzjX
+                iDsscEeOodXjjrTEHarDvYC3fNxx2hU/6SUuPf0al7/yunAVU/MRLtYhXEGi
+                EeE6IUzUI1z0Ea7WIUwiSREUCI8JSfR69Yaa+gbfe4k7wsPvH+PuSQ+juOZ5
+                OF6WxrznoZil6WLebAY/JI2yxSnPokyzG4IfXb+lq99752Da+w+oLPhUPiZo
+                kVZ12qdyT1B5twmVOY9KsizdlMoiXaEKqbBP6kdNSQ1FakidpHbfp7bhR2lk
+                Sn2fqAWxygiCP2kgGCeT1ViNVAiOYAnLfrXUxuq9drGSagitYNVP/HP/ERhK
+                qo89Qh/Qf3gh+S0+ZDjJpo+sBuHqoyrfwE89XkP4CE89XkMU+nLghvAEmxVe
+                Dz0eg+EKi9rgPCMOZS4FEu2hcfgEl9uR5PS3+DSEv1TYCA/7yYXzhCwY3SK1
+                foLTiZFwZhhbyHqMhiuMhn1GYiaeqiCVPwtS18gNJPSF9/iE6UsdHtt7/pjx
+                x995IL/E72n8I+nliIqxiXAG2xnsZLALM4M97GeQR2ETjMOCvYlrHD0cRY6f
+                cwxwRDkucDgcVzg4h8tR4hjkyHKc45jkeMoxz5HmOOBY5Jjh+IzjkOOFt/IL
+                jgWOJMeSd3ufY4XjY44nHJscqxzPOJ7/F4J6TSkwFwAA
                 """,
                 """
                 androidx/compose/runtime/SnapshotStateList.class:
-                H4sIAAAAAAAAAI1QXUsbQRQ9M5tkdY26ftTGj9pXG8Q1UhCsCFYoBLYWmpCX
-                PE2yg45JZmRnInnc3+I/6FOhD7L46I8S70ZftC/OwLn3nDncj3l4/HcH4Cs+
-                M9SFTlKjkknUN6NrY2WUjrVTIxm1tLi2l8a1nHAyVtb5YAw7x+2j+ErciGgo
-                9EX0q3cl++7byf8SQ/hW81FiqBwrrdwJg7fzpVNFBX6AMmYYSu5SWYbd+P0T
-                UZOleGDcUOnop3QiEU6Qxkc3Hu3HCvAZ2ICkiSrYPmVJg7bIs2rAazzIs4CH
-                BHlWy7N6aSbPQnbA9/n38v1thYde4T+gEm1G9RC+mmBv4GjqM5NIhsVYaXk+
-                HvVk2ha9ISnLsemLYUekquAv4mxLXWjhxinlQcuM0778oYqH9d/PO3aUVeQ8
-                1dpQC2W0RQOcPqg4NEfxX4TrxKIpB8r1v5j9QwnHBmFlKm7SBarPBgSYo+hh
-                a+ry8Gkaa9imeEieKnnmu/CaWGhikRBhAUtNLGOlC2axig9dlCzmLNYsPlr4
-                TxVS9+dFAgAA
+                H4sIAAAAAAAA/41Qy0ocQRQ9VT0v24n2+IijJpqlGcRWEQQVIQrCQJuAM8xm
+                VjXThdY8qqSrRlz2t/gHWQVcSOMyHxW8Pbox2WRz7j2nDvfeU7//PD4BOMAX
+                hobQcWJUfB/2zfjWWBkmE+3UWIYtLW7tjXEtJ5yMlHVlMIatk/ZRNBB3IhwJ
+                fR3+6A1k3x2f/isxBH9rZRQYSidKK3fK4G197VRRQtlHERWGgrtRlmE7+v+L
+                aEktGho3Ujq8lE7EwgnS+PjOo3wsh0oOYGBD0u9Vznapi/coSpZWfV7nfpb6
+                PCDI0nqWNgqVLA3YPt/lZ8XnhxIPvNy/TyPaLJ8UvDtjZ+jo9HMTS4b5SGn5
+                fTLuyaQteiNSFiLTF6OOSFTO38SZlrrWwk0S6v2WmSR9eaHyh9Wr16AdZRU5
+                v2ltaIUy2mIPnH7pLUr+aYRrxMIpB4qNX5j5SQ3HOmFpKq7jE2H11QAfs1Q9
+                fJ66PGxM6yo2qR6Sp0qeD114Tcw1Md9EgBq1WGhiEUtdMItlfOyiYDFrsWJR
+                tyi/AEh/yvNKAgAA
                 """,
                 """
                 androidx/compose/runtime/SnapshotStateMap.class:
-                H4sIAAAAAAAAAI1QTU8bMRB99ibZsKSwQKGBftBjoVIXUE80QqKVkCKWVmqq
-                veTkZC0wSexo7SCO+1v4Bz1V6qFa9ciPqjoOvbTlgCW/N/P8bM/M7a/vPwC8
-                xUuGHaHzwqj8OhmaydRYmRQz7dREJj0tpvbCuJ4TTp6JaQjG0OmcHqaX4kok
-                Y6HPk0+DSzl077J7tKP/JYb4Xy1EjaHRUVq5I4bg1U7WQgNhhDqaDDV3oSzD
-                6/TBRdIfK+nIuLHSyZl0IhdOkMYnVwF1zDyEDGxE0rXy2R5F+T5DUpVLEW/z
-                iDdpx1UZVWW7KndrzaqMGRGL+QHfC97Xf940eFzz1w7opVPaGaOnEf9Vy5uR
-                o/o/mFwyLKdKy4+zyUAWX8RgTMpqaoZinIlC+fyPuNBT51q4WUFx1DOzYihP
-                lD/Y/HzXbaasIuex1oa+UEZb7IPTqPyiOvzkCLcoS+Y5UN/9hoWvFHA8JWzM
-                xRd4Rti6MyDCInGA53NXQKeeN7FNfEieFnke9RF0sdTFMiFiDytdrGKtD2bx
-                GOt91C0WLTYsnli0LcLftbcdUGUCAAA=
+                H4sIAAAAAAAA/42Qy04bMRSGf3tyY0hhoFxCb7S7QqUOoK4gQqKVKkUMrdRU
+                s8nKyVhgktjR2EEs51n6Bl0hdYFGLHkoxHFg08uiC//nnM+/j318e/frGsAH
+                vGHYEjrLjcou44EZT4yVcT7VTo1l3NViYs+M6zrh5ImY1MEY2u3j/eRcXIh4
+                JPRp/LV/LgfuIP0HO/wbMUR/sjoqDLW20sodMgRvt9ImaqiHqKLBUHFnyjK8
+                S/77kXTHUjI0bqR0fCKdyIQTxPj4IqCJmZeGFzCwIfFL5asdyrJdhrgsFkLe
+                4iFv0IrKIiyLVllsVxplETEKLOJ7fCf4WL35UeNRxR/bo07HtFLmm0a/Pej9
+                0NEQn0wmGRYTpeWX6bgv8++iPyKynJiBGKUiV75+hHNddaqFm+aUh10zzQfy
+                s/IbG98eRk6VVeQ80trQFcpoi11w+q/Hqfz3kT6nKp7VQHX7CnM/KeF4QVqb
+                wVd4Sdp8MCDEPMWAqHcF2JzFZ3hNcZ88TfI86SHoYKGDxQ4iLFGK5Q6eYqUH
+                ZrGKtR6qFvMW6xYtiw2L+j3Q7/VyagIAAA==
                 """,
                 """
                 androidx/compose/runtime/State.class:
-                H4sIAAAAAAAAAH1QTUvDQBB9k7RpjF/xu1YQj9WDUfEgfoEXoVARbBGhp7Vd
-                69p0I91t8Zjf4sEf4UGCR3+UOFFPKu7hzbw3O7Nv5+39+QXALlYIq0J3Bonq
-                PETtpH+fGBkNhtqqvowaVlhZAhGqh839+p0YiSgWuhudX9/Jtj04/i0Rwp9a
-                CQWC35X2UsRDSZivrv/VV6yuN5scZ+q9xMZKR2fSio6wgjWnP3LZLuVQIlCP
-                pQeVsy3OOtuEwyydCpyyE2Rp4IQ5+K5/U87SDc/P0pDWaMfZci5mQ7fi7GXp
-                1etT4fXR8yoFvxAW8xk7hLX6/5tgI9QktoHi6OsrYUOLe3Ob2M/6Zs8Sxhqq
-                q4UdDrgcNJLhoC1PVcxk+eJr1qUy6jqWJ1on3KQSbTx+H0Xkh3hVHnjlKDNz
-                4MP9zlwsf8YlVDge8Y0x7glacGsYr2GCEZM5TNUwjbAFMpjBbAuewZzBvMGC
-                waLJaekDuDg1Tf4BAAA=
+                H4sIAAAAAAAA/31Qy07jQBCsthPHmJeT5RECQhzDHtaAOKx4SXtBihSERCKE
+                lNOQDGGIM0aZScTR38KBj+CALI77USvaYU+AuHR1VU/3VPfff88vAPaxQdgU
+                ujdKVO8h6ibD+8TIaDTWVg1l1LLCyhKIUD9qHzTvxEREsdD96Pz6Tnbt4cln
+                iRB+1EooEPy+tJciHkvCUn37q75ifbvdZiw3B4mNlY7OpBU9YQVrznDisl3K
+                g58HEGjA+oPK2Q5nvV3CUZYuBE7VCbI0cMI8+K5/U83Sn56fpSFt0Z6z41xU
+                Qrfm/M7Sq9enwuuj59UKfiEs5jP2CFvN78/BbqhNuYHi5H2fsKXFvblN7LT+
+                a2AJMy3V18KOR1wOWsl41JWnKmaydvE+61IZdR3LP1on3KQSbTz+H0VMd+N7
+                eeC7Y42ZAx/u/8xFbYpVrDMe84sZ7gk6cBuYbWCugXkscIrFBkKUOyCDCn50
+                4BksGSwbrBismpyW3gBNs/uhAwIAAA==
+                """
+    )
+
+    val Effects: TestFile = bytecodeStub(
+        filename = "Effects.kt",
+        filepath = "androidx/compose/runtime",
+        checksum = 0xb63b1aec,
+        """
+            package androidx.compose.runtime
+
+            @Composable
+            fun SideEffect(
+                effect: () -> Unit
+            ) {
+                effect()
+            }
+
+            class DisposableEffectScope {
+                inline fun onDispose(
+                    crossinline onDisposeEffect: () -> Unit
+                ): DisposableEffectResult = object : DisposableEffectResult {
+                    override fun dispose() {
+                        onDisposeEffect()
+                    }
+                }
+            }
+
+            interface DisposableEffectResult {
+                fun dispose()
+            }
+
+            private class DisposableEffectImpl(
+                private val effect: DisposableEffectScope.() -> DisposableEffectResult
+            )
+
+            @Composable
+            @Deprecated("Provide at least one key", level = DeprecationLevel.ERROR)
+            fun DisposableEffect(
+                effect: DisposableEffectScope.() -> DisposableEffectResult
+            ): Unit = error("Provide at least one key.")
+
+            @Composable
+            fun DisposableEffect(
+                key1: Any?,
+                effect: DisposableEffectScope.() -> DisposableEffectResult
+            ) {
+                remember(key1) { DisposableEffectImpl(effect) }
+            }
+
+            @Composable
+            fun DisposableEffect(
+                key1: Any?,
+                key2: Any?,
+                effect: DisposableEffectScope.() -> DisposableEffectResult
+            ) {
+                remember(key1, key2) { DisposableEffectImpl(effect) }
+            }
+
+            @Composable
+            fun DisposableEffect(
+                key1: Any?,
+                key2: Any?,
+                key3: Any?,
+                effect: DisposableEffectScope.() -> DisposableEffectResult
+            ) {
+                remember(key1, key2, key3) { DisposableEffectImpl(effect) }
+            }
+
+            @Composable
+            fun DisposableEffect(
+                vararg keys: Any?,
+                effect: DisposableEffectScope.() -> DisposableEffectResult
+            ) {
+                remember(*keys) { DisposableEffectImpl(effect) }
+            }
+
+            internal class LaunchedEffectImpl(
+                private val task: suspend () -> Unit
+            )
+
+            @Deprecated("Provide at least one key", level = DeprecationLevel.ERROR)
+            @Composable
+            fun LaunchedEffect(
+                block: suspend () -> Unit
+            ): Unit = error("Provide at least one key")
+
+            @Composable
+            fun LaunchedEffect(
+                key1: Any?,
+                block: suspend () -> Unit
+            ) {
+                remember(key1) { LaunchedEffectImpl(block) }
+            }
+
+            @Composable
+            fun LaunchedEffect(
+                key1: Any?,
+                key2: Any?,
+                block: suspend () -> Unit
+            ) {
+                remember(key1, key2) { LaunchedEffectImpl(block) }
+            }
+
+            @Composable
+            fun LaunchedEffect(
+                key1: Any?,
+                key2: Any?,
+                key3: Any?,
+                block: suspend () -> Unit
+            ) {
+                remember(key1, key2, key3) { LaunchedEffectImpl(block) }
+            }
+
+            @Composable
+            fun LaunchedEffect(
+                vararg keys: Any?,
+                block: suspend () -> Unit
+            ) {
+                remember(*keys) { LaunchedEffectImpl(block) }
+            }
+        """,
+        """
+                META-INF/main.kotlin_module:
+                H4sIAAAAAAAA/2NgYGBmYGBgBGIOBijgMuSSSMxLKcrPTKnQS87PLcgvTtUr
+                Ks0rycxNFeJ0TUtLTS4p9i4R4gpKzU3NTUot8i7h4uNiKUktLhFiCwGS3iVK
+                DFoMAHVSFrpbAAAA
+                """,
+                """
+                androidx/compose/runtime/DisposableEffectImpl.class:
+                H4sIAAAAAAAA/51TS08UQRD+enbZx4iyLPJGQEFZQJgFvS0hUYRkkxUNS4gJ
+                p2a2gV5me8h0L8Eb0Yu/w3/gwWg8GMLRH2Ws3ocgxgAmM9VV1f1VfV1V/ePn
+                t+8AnuIJwxxXlSiUlWPPD2uHoRZeVFdG1oT3Qmqy+U4gVnd3hW+KtcMgCcaQ
+                qfIj7gVc7Xmvdqq0k0SMIbEklTTLDBO50kFoAqm86lHN260r38hQaW+tpS0U
+                prcYPlx1ammudG1qZT88FIXZ6wM2hK4HprDcoDJRCqM9ryrMTsQlUeBKhYY3
+                6ayHZr0eBAW6n2ggU0gzjF6gLpURkeKBV1QmIrj0dRK3GHr9feEftPCvecRr
+                gg4yTOVKl+tXuOAp2yB7xKsTt3HHRSe6GGI5a3eg20UcWYbxqyrciTTupuGg
+                lyFu9qVm8K5fHdtpuvH7qzp00wb9R38YutssXgrDK9xw8jm1oxgNMLMiZQUY
+                2AH5j6W18qRVFhii05MR1xlwXCdzeuLS19Av/KcnKWfg9GTRybPno9n+jDPU
+                k41nnXy8ITvysbOPCSeVsDKT3Bj71/6bs3dxq1E8m3mRWT7ZNu/zrjDkb1oy
+                hoUbV43ms5179dgIGslQtUlsvm0EdZsAPX9gGNJluae4qUeCYXijGbuojqSW
+                FPnZ+WOgUVoJK3SoqySVWK/XdkS0abPby4Y+D7Z4JK3dck5ejvX7FfwR1C2H
+                9cgXa9JiBluYrb+yI0/jHG+0Omunm6xZshwM4jGtCfKnmoNADyWBGObIKtG+
+                Q2tmNut+RWbmC3pmZj+j71MDOU/yDp1MUAwXQ+ii1SNfXxODfgzY2SLN5mOt
+                fEliAiRZK6GDhYacwSKtK+QdIgLD24gVMVLEvSJGMUYqxou4jwfbYBoTmNxG
+                SmNA46FGWuORxpRGTmNaI/ELZm5MUZ8FAAA=
+                """,
+                """
+                androidx/compose/runtime/DisposableEffectResult.class:
+                H4sIAAAAAAAA/5VPzU4CMRicrwvsuiou/qIPQPTiAjHx4MlEjWswJphw4VTY
+                YgrL1tBCOPJcHgxnH8r4LTyBSTOd+X4605/fr28ANzgjxDJPZ0any3hopp/G
+                qng2z52eqvhBW9ZykKnH0UgNXVfZeeZ8ECEay4WMM5l/xG+DMfd8eAQ/3Wwo
+                gnd51SPUOhPjMp3Hr8rJVDp5RxDThcfWVEBQAAg04fpSF6rJLG0RGutVNRR1
+                EYpovQr5iEgEo/p61RZNegkicSGa3nOjmG4TWp1/foKDsG+4LdnriWPxbuaz
+                oXrSGec/727Xe9pqXr3Pc+Ok0ya3FbZECZvgJUIZFWYCJxs8xinft/y0z52g
+                Dy/BToIwwS72mGI/QRUHfZBFhFofJYtDiyOLMuMfURqWxJYBAAA=
+                """,
+                """
+                androidx/compose/runtime/DisposableEffectScope$onDispose$1.class:
+                H4sIAAAAAAAA/8VUXVPTQBQ9mxZaQoHwIQIqVkFtA5Km4hcwzDBYxmpRh2p9
+                4CltQ1mabpgk7fDk9CfpjI6jD06f/VGON0mRjjry8eJD9t69e/bs3bP35vuP
+                r98ALGOVYcUQVcfm1SOtYjcObdfUnKbweMPUnnCX5kbZMnN7e2bFK1bsQ3Pe
+                FmHcnNdjYAzKgdEyNMsQNe1l+YBgMUQYtDOz7phu06JNfQz9a1xwb51hLlWo
+                257FhXbQamh7TVHxuC1cbavrZVbTJYbsaai14/U3RLu6HmxSTi4Qns+QPO2w
+                BGQMDkBCgiGSSpcSiGFYRhQjDFFvn7sMa4WLy0iPEKuGE4bZf+cSwwTpxEXL
+                rhN4IpUu/C4/ZTuJy4O4hCl6hnMqxDB6HNk2PaNqeAbFpEYrQuXC/CHuD2Bg
+                dYofcX/2nryqzpDqtEfkTluWpqTAKJLKOu24PNVpZ6UMexZXpBkpE3k66eOz
+                DPrZNQuLhFKhk5fOp3QMKYaBX3IzFE8vrnMnloCKBQY5DLpLdSortfcYLjzT
+                EYalFe2mUzGfmOVmLXfkmcKlE+lefS3DalJy74rbG696aOTnAYesFpPH3pa8
+                kNSTPZCLV56sFmR9Tl/U9eUVmuRkUqrIa8Lwmg4lE920q2RGClyYL5qNsum8
+                9skYxgp2xbBKhsP9eTeYyAthOpuW4bomdcRITlQs2+WiRpW0b1dJnfDuW9xH
+                T/xNCIbpnTD3Enc5sW4IYXtG8D4MV7predH6YxU6tWeUqqMfTFH8fiVfp+KU
+                MEMfdRjiZLMUWScrkZXVhU8YUj9D+RDg7tFIu9GHYfozgrooQGEUY37Jk9fL
+                OkDeOCFZwPnY7wiyg+pHDH3BNMPbE1I5IFICKp84EUK7xP24H2BYgAKm8YDG
+                KNJYxMOA4y4ekf3PlUFXBOVDr0ACXd1FJI9reczmcR1JcnEjj5uY2wVzMY9b
+                u4i6vnvbxbiLO1ihzb5WS/RpASjzE1u+Yp2EBgAA
+                """,
+                """
+                androidx/compose/runtime/DisposableEffectScope.class:
+                H4sIAAAAAAAA/51UW2/TSBT+xrnYNaVJw603CgsBWgq1E9gLpCBBF7RBoSAC
+                lVCfJs60TOuMkcep+ljxsP9hX/cX7D4VLdIqKm/8KMQZJy3d7kMpljzn/p05
+                c87Mp8///AvgNu4wzHPVjiPZ3vKCqPM20sKLuyqRHeH9KjXJvBWKR6urIkia
+                QfRW2GAMxXW+yb2QqzXvWWudTDYyDPkFqWRynyEzM7s8jBzyLrKwGbLJG6kZ
+                /MbxUtUYhiLVNwmG5kxjI0pCqbz1zY632lVBIiOlvccDzq/NfnuCF0J3w4Qy
+                tI5CXdizv6Liave/K8nlRhSveesiacVcEjhXKkp4P9FSlCx1w5C8CvvF9sMd
+                FBimD+xOqkTEiodeXSUx4chA2xhlOBO8EcHGAOg5j3lHkCPDtZnG4UbVDmia
+                BmStZnp1CqddlHCG4e7xelTe33O5YuMclXp0l9LZGHcxhgkG75inaWOKYaQs
+                y6vlA7PB6gwXj0rMMLrn8lQkvM0TTjqrs5mhu8DM4pgFBLdB+i1pJJ+4doVh
+                pbc94VpjlmsVe9uu5VipYNhUZ431tquWzx7mdv/Mk/hkqpiZsPxsdcTJFnMT
+                Tilbsnzbz/+2+7vz8T3rbe++s2w35+z+UfWZSVFlJnHlO4artFfUwUrdvpOe
+                30jo/i1GbTqkQkMqsdTttET80uCY0Cjg4TKPpZEHyqGmXFM86cbET77oZ6+r
+                TaklmR98nVyG8mHr/uz9x224rpSIF0OutSDRbUbdOBCPpUk2PoBY/h88KrDo
+                +TCfRSdDrwmtPkmeaRDR3PUdOH+n5gqt+VR5AlVah/sOGIJLdJS0wwRlgpeR
+                gWnr6blS8T3OZu59wNjruR1M9nD+r30sl6iDEboWpRTvIsU4hDGNC2Sh6AGy
+                4QpkZbiVxp6kJ7W/kxGiP9Jvs4GQwU8pMKOxN984fk5DPPxCdJH0P9CGL60g
+                U8flOsp1XMFVYnGtjhnMroBpXMfcChwNV+OGRl7jhMZNjYLGPGm+AB149ZDV
+                BQAA
+                """,
+                """
+                androidx/compose/runtime/EffectsKt.class:
+                H4sIAAAAAAAA/+1Y21Mb1xn/Vhe0LALL4q6kjmJIA8JYWnExWBjH5hKrljGV
+                bKhL63QRC16QdhXtSgYnbdxmOtOX/APpQ2f63Je8JG4z43qat/5Rnf7O0UpI
+                aAHBOEwfyoz2fHvOd/l9t3P28O///OM1EU3S7wW6quhbRUPb2o9mjXzBMNVo
+                saRbWl6NLm1vq1nLvG/5SBAosKuUlWhO0XeiDzd3seAjt0BSRttSK4wCDY2k
+                9gwrp+nR3XI+ul3Ss5Zm6GZ02aZiidE1geKncc1V1x/rmpWY50IfpI6FucDf
+                lc2cmgCElFHcie6q1mZR0aBU0XXDUioGVgxrpZTLgatN5YhFkgS6UgdG0y21
+                qCu5aFK3ihDXsqaP/AL1Zp+p2T1bflUpKnkVjAJ9OJI6GpVE3UyGKdkBfj91
+                0SWJOinQaM/BeR8FgU/Ty8aeKlDPyGizBT/1UG8HdVOfQOHTIo7ELWqmHaBW
+                EyXzmP/pNK658eOzctRoJmsU1MRY6wJp1SzlqunvriJZVAtFNatY6hY88+VV
+                01R2EKeB1aJRRiWGFSucUxXTChu6Gt5TDwTy5tSymhNo8KgKeJBiS1DkXUqn
+                H6YFeu8w1slcTt1RchkUj7q0n1ULjN9HV6HoOFvXRRoWSLSMSt6PZs+uBj/9
+                lD6UyEMjyPOchhKfR4GNOJdNhMYkGqJrpyZahhdTTtXYQp6/PrvcxWV++KSG
+                RjfafT/esvZkvpDzUZzFNiTRBE0KNDasDSvD40U1r+Y31eJ4beMbPio8LAsk
+                JAXqhMT2cFVAIA/Sj6VgcyAF+vjkTeIMifrb29B0cak7Q1jjlRBieNKaj+eK
+                3+sfR/f/ZEQnKhHFMD2ycb5w/fkcghcXC+adKdJS9cukZGm56J1iUTnAmf0x
+                9lZoP3i4LdCokxvJUYdJPyXpZxLdo/tnCjV2kHZNL5Qsc1gr46xy0CxQV0pB
+                jJ6pW9UzWGvhdLUZskbRgHu6auJjB6HRS3wDPGSwP5TGmg1XYuXdzBnZPZHS
+                Il0R6LNz7fdvC8vYselujFBln16TaJ1t0aPHpqNRim3QX72lbfJtedwydmx/
+                f/nRtqgL9wZbz+fn20EuHCoK7HJV6QPVUrYUS0HPuvJlN65JAnuI7EE4/fcY
+                4cLivsaoGKgtWXD98ObltPTmpeQKuPgw4Gr48RX2EN0gLldGtiZ62Rh6H4sh
+                V0yI9wdcoe6gJ+iKefjTG3P/669tLrHtni80VmUSA76QZ0CIiSeyTzWzi4H2
+                FgRvnyQoBqQWVMxXVfQFOkL+oCgKQS4U8189VZiNgc5QKtDVAoh3A5dCA9Au
+                BUWuR4gFgm22Lfc93w+vhDcvucLLobsnKGxZTTA006SmZeHu0LItfFxYWlbV
+                Gbpuq2pZpCfUF+jlIn4Ev8rch4D/weVBJQ6yUsYeROxD1u6G+vtF7KxHu0Dy
+                mQ93XJCrtpf2LRV3cEOvgnh0wJWGT9kgEg742UVYsvv/+h6O30j9NlS792eM
+                UjGrLqqbpZ2adXY/LCu5kioIZubBndU6NdJ9rkOKZMJValkaC8vhOpbT/70C
+                iXg4bW9SJ4pUmSATSUnykHxNlidvypI8MRS/KctTUjzGiWkpPsOJG9LEJCdm
+                qjyzNk88ZvPEZZsnHj/0g0fgfM4A2GRsSOZwJmc5MS1NVYgb0vQNTsxIM1Oc
+                mJVmJxkBOHKsQsEdOc4pAFqS8KW3YGzhjn8phUyvlJj/j1jZsCwbWSW3phQ1
+                9m5Ptme0HV2xSkXQ76Qr8JJ6WTM1LN85vD3ianl0tfafnQY26fB/Drj82zJr
+                DvqkSvUsawxGj1MpkUwu3PzZXwcNkpfa8PYcb3OYR+NRVyTY8YouR/5O/QKt
+                f8NOGtrHU8LYRiJ1Ujsd4N1f4cb8AMYXnM9Hn2Fsw4qI8XP8fDiuIEAwNUgh
+                LDFTt8jNhbsrpr6noSfBD76l0e9o/J81e0yLRO/X2erG+/WarXfot9yL3+EX
+                xPq7mPuJSVfoPZXCkDsRSdR2Og8kLBi9YxyJ+5Z7/nuaeDL2HU3V+97H4zQC
+                XSYwjMCeST00yrFFoEDCyjTdgDIRUZ0B5WJKaZZucoy9HLlgI481YetkoZf5
+                4V5FmLAR7kOpl0G4VkE47572MIjXHCHKUGkBgoxgWTAb5xAnsdaBlVsA5sHo
+                5xDdXKoKsY/mQQmcYmBdNti5JrDd7hrYRsi3bchfQnUbxv7xCuRpj3vayzCP
+                O2JOQEEJmBIIaQlzcxzzPFb8sHkHSL3gqGBmueqvYe6vYe6nu6BcnGLo3Tb6
+                j5rQD3qOoG/0YcH24Y922IciwWX4UCmPSOT1K0o9qOXAqUz8UNFLZTTFAsqw
+                TFdpsZaDAaB4wL3oreWgGy20Qg859iFaAkWcqi+YReeCaa9DvurUXJlqcz1y
+                bq6VhuZK1zXXz8/fXKtOzZWpNdc6i9ovmqvgMXQ9B6bHsPcclbDW0FxPTmyu
+                dF2sHp/eXKtOzZU5bK511lwOEJ9C5T4gPEWw9mH2k4bm2mixudJ1zfXLVptr
+                1am5MnXNtc6aywHzM75b+zH2YOwjraG5fn3m5krXNdevztZcq8c3V6apuZzL
+                xE8FxP0FGqmAMnyB5vq0obk+abG56gvm6WnN5aEvOKNJLzH1/++vi//+4ttQ
+                Aen4DfKsbJA7SZtJyiZpi9QkbdNOEvWtbSA5tEt7G9Rv0qBJOZzVJuVN0k0y
+                TCqYNMsn5026a9ISpxdMWjUpbdKnJt02KWFS1KSQSV6TirwuumDVwq/EtZf/
+                C7XJpgW2HAAA
+                """,
+                """
+                androidx/compose/runtime/LaunchedEffectImpl.class:
+                H4sIAAAAAAAA/5VSW08TURD+znbZbleUsgKWi4iCUKiwhfhWQqJETJOKBpSY
+                8HS6Xcppt2fJXhoeG179F/4DH4zEB9Pgmz/KONuLIKiEZPfM5Xwz852Z+fHz
+                6zcAT7HKkOOy4nuicmzZXuPICxzLj2QoGo5V4pG0D53Ki4MDxw6LjSM3CcaQ
+                rvEmt1wuq9brco1ukkgwaOtCinCDYTZbqnuhK6RVazasA0oRCk8G1lZPWy0s
+                7jGI61Dry32A7fleFArpBNamR8xkxGPEOeAdFS5sFHKly8TIGdeaLXl+1ao5
+                YdnngmpwKb2Qd+tte+F25LoFBjXkQV1HimH6AjMhQ8eX3LWKMvQpWNhBErcY
+                Rqkxdr0X/Yb7vOEQkGEhe5XFBc9unKRKrAZxG3cMDGKIIZGN7QEMG1BhMsxc
+                18BBpDCSgoLRmPahCBiWSzcYI722cl37b9r9vzWfYbiPeuWEvMJDTj6l0UzQ
+                8rH40OMDDKxO/mMRW3nSKrSXH9qtCUPJKIaSbrcM+jp6x6ZfV/R2K9NurSl5
+                9nzenEorExmdmYapm6qp5AfyqqmZaoblWT7x/ZS1W2cfNSWt7Sz+D/j+7ETt
+                g1Wqkzw7UUjq4zGlNRYTNfsPOp/GhYH9o1kEMbrdD1bqIUNqV1QlDyPfYZjc
+                6Y6pKJsiEGXXeXa+nTTdTa9CoKES5dyOGmXHf8sJE/PwbO7ucV/Eds85dznX
+                78X8I6mx60W+7WyJOGa8F7N3pTpWacPUznjMeOHIWiRLwTiWSGrk17vDo93V
+                kECOrBLdKyTTOdM4RXrpC+4u5T5j7FMn8gmddwipYQsGXmKI5DL5xroxuIdM
+                vA+kxfVYr14SKySTrFdQgdU5s8iT3CTvBBGY3EeiiKki7hcxjQekYqaIh3i0
+                DxZgFnP70ANkAjwOkAowH2Cho2sBRn4Bxaa1HQ8FAAA=
                 """
     )
 
diff --git a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
index 48cb5ef..71b1990 100644
--- a/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
+++ b/compose/lint/common/src/main/java/androidx/compose/lint/Names.kt
@@ -45,6 +45,11 @@
         val MutableStateMapOf = Name(PackageName, "mutableStateMapOf")
         val ProduceState = Name(PackageName, "produceState")
         val Remember = Name(PackageName, "remember")
+        val DisposableEffect = Name(PackageName, "DisposableEffect")
+        val RememberSaveable = Name(PackageName, "rememberSaveable")
+        val LaunchedEffect = Name(PackageName, "LaunchedEffect")
+        val ReusableContent = Name(PackageName, "ReusableContent")
+        val Key = Name(PackageName, "key")
     }
     object Ui {
         val PackageName = Package("androidx.compose.ui")
diff --git a/compose/material/material-icons-core/build.gradle b/compose/material/material-icons-core/build.gradle
index 0276b88..615b415 100644
--- a/compose/material/material-icons-core/build.gradle
+++ b/compose/material/material-icons-core/build.gradle
@@ -15,65 +15,100 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 import androidx.compose.material.icons.generator.tasks.IconGenerationTask
 
+
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.compose.ui:ui:1.2.1")
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(libs.kotlinStdlib)
-
-        samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
-    }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:ui:ui"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
             }
         }
-    }
-    dependencies {
-        samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:ui:ui"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
 IconGenerationTask.registerCoreIconProject(
         project,
         android,
-        AndroidXComposePlugin.isMultiplatformEnabled(project)
+        true
 )
 
 androidx {
diff --git a/compose/material/material-icons-extended/build.gradle b/compose/material/material-icons-extended/build.gradle
index 0903147..b7e2fea 100644
--- a/compose/material/material-icons-extended/build.gradle
+++ b/compose/material/material-icons-extended/build.gradle
@@ -14,11 +14,10 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 import androidx.build.RunApiTasks
 import androidx.compose.material.icons.generator.tasks.IconGenerationTask
-import androidx.compose.material.icons.generator.tasks.ExtendedIconGenerationTask
 
 plugins {
     id("AndroidXPlugin")
@@ -26,7 +25,7 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
 IconGenerationTask.registerExtendedIconMainProject(
         project,
@@ -35,38 +34,62 @@
 
 apply from: "shared-dependencies.gradle"
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make an analogous update in the
-         * corresponding block below
-         */
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.kotlinReflect)
-        androidTestImplementation(libs.truth)
+    sourceSets {
+        commonMain {
+            dependencies {
+                api(project(":compose:material:material-icons-core"))
+                implementation(libs.kotlinStdlibCommon)
+                implementation(project(":compose:runtime:runtime"))
+            }
+        }
 
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation(project(":compose:foundation:foundation-layout"))
-        androidTestImplementation(project(":compose:ui:ui"))
-        androidTestImplementation(project(":test:screenshot:screenshot"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
-        androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
-    }
-}
+        commonTest {
+            dependencies {
+            }
+        }
 
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            androidAndroidTest.dependencies {
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:foundation:foundation"))
                 implementation(project(":compose:foundation:foundation-layout"))
                 implementation(project(":compose:ui:ui"))
@@ -83,6 +106,21 @@
                 implementation(libs.truth)
             }
         }
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
@@ -102,43 +140,7 @@
 
 }
 
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    def allThemeProjects = [
-            project(":compose:material:material-icons-extended-filled"),
-            project(":compose:material:material-icons-extended-outlined"),
-            project(":compose:material:material-icons-extended-rounded"),
-            project(":compose:material:material-icons-extended-sharp"),
-            project(":compose:material:material-icons-extended-twotone")
-    ]
-
-    for (themeProject in allThemeProjects) {
-        project.dependencies.add("embedThemesDebug", themeProject)
-        project.dependencies.add("embedThemesRelease", themeProject)
-    }
-    // Compiling all of the icons in this project takes a while,
-    // so when possible, we compile each theme in its own project and merge them here.
-    // Hopefully we can revert this when parallel compilation is supported:
-    // https://youtrack.jetbrains.com/issue/KT-46085
-    android {
-        libraryVariants.all { v ->
-            if (v.name.toLowerCase().contains("debug")) {
-                v.registerPostJavacGeneratedBytecode(configurations.embedThemesDebug)
-            } else {
-                v.registerPostJavacGeneratedBytecode(configurations.embedThemesRelease)
-            }
-            // Manually set up source jar generation
-            ExtendedIconGenerationTask.registerSourceJarOnly(project, v)
-        }
-    }
-} else {
-    // We're not sure how to compile these icons in parallel when multiplatform is enabled
-    IconGenerationTask.registerExtendedIconThemeProject(
-            project,
-            android,
-            AndroidXComposePlugin.isMultiplatformEnabled(project)
-    )
-}
-
+IconGenerationTask.registerExtendedIconThemeProject(project, android)
 
 androidx {
     name = "Compose Material Icons Extended"
diff --git a/compose/material/material-icons-extended/generate.gradle b/compose/material/material-icons-extended/generate.gradle
index aed94d9..eb45afa 100644
--- a/compose/material/material-icons-extended/generate.gradle
+++ b/compose/material/material-icons-extended/generate.gradle
@@ -25,17 +25,9 @@
 apply plugin: "com.android.library"
 apply plugin: "AndroidXComposePlugin"
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
 apply from: "${buildscript.sourceFile.parentFile}/shared-dependencies.gradle"
 
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    // We're not sure how to merge icons jars when multiplatform is enabled
-    IconGenerationTask.registerExtendedIconThemeProject(
-            project,
-            android,
-            AndroidXComposePlugin.isMultiplatformEnabled(project)
-    )
-}
+IconGenerationTask.registerExtendedIconThemeProject(project, android)
 
 dependencies.attributesSchema {
     attribute(iconExportAttr)
diff --git a/compose/material/material-icons-extended/shared-dependencies.gradle b/compose/material/material-icons-extended/shared-dependencies.gradle
index f2cbca1..2de0ef0 100644
--- a/compose/material/material-icons-extended/shared-dependencies.gradle
+++ b/compose/material/material-icons-extended/shared-dependencies.gradle
@@ -18,36 +18,25 @@
 // by its specific theme projects (each of which compile a specific theme)
 
 import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 
-dependencies {
-    if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make an analogous update in the
-         * corresponding block below
-         */
-       api(project(":compose:material:material-icons-core"))
-       implementation(libs.kotlinStdlibCommon)
-       implementation(project(":compose:runtime:runtime"))
-    }
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 }
 
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
-                api(project(":compose:material:material-icons-core"))
-                implementation(libs.kotlinStdlibCommon)
-                implementation(project(":compose:runtime:runtime"))
-            }
+kotlin {
+    /*
+     * When updating dependencies, make sure to make an analogous update in the
+     * corresponding block above
+     */
+    sourceSets {
+        commonMain.dependencies {
+            api(project(":compose:material:material-icons-core"))
+            implementation(libs.kotlinStdlibCommon)
+            implementation(project(":compose:runtime:runtime"))
         }
     }
 }
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
index 31a99bb..999f59f 100644
--- a/compose/material/material-ripple/build.gradle
+++ b/compose/material/material-ripple/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,48 +23,15 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        api("androidx.compose.foundation:foundation:1.2.1")
-        api("androidx.compose.runtime:runtime:1.2.1")
-
-        implementation(libs.kotlinStdlibCommon)
-        implementation("androidx.compose.animation:animation:1.2.1")
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:foundation:foundation"))
                 api(project(":compose:runtime:runtime"))
@@ -73,19 +39,55 @@
                 implementation(project(":compose:animation:animation"))
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        commonTest {
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:foundation:foundation"))
+                    api(project(":compose:runtime:runtime"))
+                    implementation(project(":compose:animation:animation"))
+                    implementation(project(":compose:ui:ui-util"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:test-utils"))
 
                 implementation(libs.testRules)
@@ -94,6 +96,29 @@
                 implementation(libs.truth)
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
@@ -105,7 +130,7 @@
     // Disable strict API mode for MPP builds as it will fail to compile androidAndroidTest
     // sources, as it doesn't understand that they are tests and thinks they should have explicit
     // visibility
-    legacyDisableKotlinStrictApiMode = AndroidXComposePlugin.isMultiplatformEnabled(project)
+    legacyDisableKotlinStrictApiMode = true
 }
 
 android {
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index d9fe905..a45a971 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
@@ -426,7 +427,7 @@
   }
 
   @androidx.compose.material.ExperimentalMaterialApi @kotlin.jvm.JvmDefaultWithCompatibility public interface ExposedDropdownMenuBoxScope {
-    method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public default void ExposedDropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.ScrollState scrollState, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
     method public androidx.compose.ui.Modifier exposedDropdownSize(androidx.compose.ui.Modifier, optional boolean matchTextFieldWidth);
   }
 
@@ -531,7 +532,7 @@
   }
 
   public final class ModalBottomSheetKt {
-    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.ModalBottomSheetState sheetState, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
     method @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.ModalBottomSheetState ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, androidx.compose.ui.unit.Density density, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean isSkipHalfExpanded);
     method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.ModalBottomSheetState ModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean isSkipHalfExpanded);
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.ModalBottomSheetState rememberModalBottomSheetState(androidx.compose.material.ModalBottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.ModalBottomSheetValue,java.lang.Boolean> confirmValueChange, optional boolean skipHalfExpanded);
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 4eea647..c9c5c53 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -7,7 +7,8 @@
   }
 
   public final class AndroidMenu_androidKt {
-    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.foundation.ScrollState scrollState, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> content);
+    method @Deprecated @androidx.compose.runtime.Composable public static void DropdownMenu(boolean expanded, kotlin.jvm.functions.Function0<? extends kotlin.Unit> onDismissRequest, optional androidx.compose.ui.Modifier modifier, optional long offset, optional androidx.compose.ui.window.PopupProperties properties, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,? extends kotlin.Unit> content);
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 82e2838..3019ae0 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 
 plugins {
@@ -24,70 +24,15 @@
     id("AndroidXPaparazziPlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        api("androidx.compose.animation:animation-core:1.2.1")
-        api(project(":compose:foundation:foundation"))
-        api(project(":compose:material:material-icons-core"))
-        api(project(":compose:material:material-ripple"))
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api("androidx.compose.ui:ui:1.2.1")
-        api(project(":compose:ui:ui-text"))
-
-        implementation(libs.kotlinStdlibCommon)
-        implementation("androidx.compose.animation:animation:1.2.1")
-        implementation("androidx.compose.foundation:foundation-layout:1.2.1")
-        implementation("androidx.compose.ui:ui-util:1.2.1")
-
-        // TODO: remove next 3 dependencies when b/202810604 is fixed
-        implementation("androidx.savedstate:savedstate:1.2.1")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(project(":compose:material:material:material-samples"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":test:screenshot:screenshot"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.mockitoKotlin)
-        androidTestImplementation(libs.testUiautomator)
-
-        lintPublish project(":compose:material:material-lint")
-
-        samples(project(":compose:material:material:material-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:animation:animation-core"))
                 api(project(":compose:foundation:foundation"))
@@ -101,8 +46,37 @@
                 implementation(project(":compose:foundation:foundation-layout"))
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:animation:animation-core"))
+                    api(project(":compose:runtime:runtime"))
+                    api(project(":compose:ui:ui"))
+                    api(project(":compose:ui:ui-text"))
+                    implementation(project(":compose:animation:animation"))
+                    implementation(project(":compose:foundation:foundation-layout"))
+                    implementation(project(":compose:ui:ui-util"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
 
                 // TODO: remove next 3 dependencies when b/202810604 is fixed
@@ -110,23 +84,27 @@
                 implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
                 implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+                }
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:material:material:material-samples"))
                 implementation(project(":compose:test-utils"))
                 implementation(project(":test:screenshot:screenshot"))
@@ -140,18 +118,39 @@
                 implementation(libs.mockitoKotlin)
                 implementation(libs.testUiautomator)
             }
+        }
 
-            desktopTest.dependencies {
-                implementation(project(":compose:ui:ui-test-junit4"))
-                implementation(libs.truth)
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
                 implementation(libs.junit)
-                implementation(libs.skikoCurrentOs)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                    implementation(project(":compose:ui:ui-test-junit4"))
+                    implementation(libs.truth)
+                    implementation(libs.junit)
+                    implementation(libs.skikoCurrentOs)
+                }
             }
         }
     }
-    dependencies {
-        samples(project(":compose:material:material:material-samples"))
-    }
+}
+
+dependencies {
+    lintPublish project(":compose:material:material-lint")
 }
 
 androidx {
diff --git a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
index 2fe5d63..d705315 100644
--- a/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
+++ b/compose/material/material/icons/generator/src/main/kotlin/androidx/compose/material/icons/generator/tasks/IconGenerationTask.kt
@@ -157,15 +157,10 @@
         @JvmStatic
         fun registerExtendedIconThemeProject(
             project: Project,
-            libraryExtension: LibraryExtension,
-            isMpp: Boolean
+            libraryExtension: LibraryExtension
         ) {
-            if (isMpp) {
-                ExtendedIconGenerationTask.register(project, null)
-            } else {
-                libraryExtension.libraryVariants.all { variant ->
-                    ExtendedIconGenerationTask.register(project, variant)
-                }
+            libraryExtension.libraryVariants.all { variant ->
+                ExtendedIconGenerationTask.register(project, variant)
             }
 
             // b/175401659 - disable lint as it takes a long time, and most errors should
diff --git a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
index ccbd6ff..30fa679 100644
--- a/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
+++ b/compose/material/material/integration-tests/material-catalog/src/main/java/androidx/compose/material/catalog/library/model/Examples.kt
@@ -50,6 +50,7 @@
 import androidx.compose.material.samples.LeadingIconTabs
 import androidx.compose.material.samples.LinearProgressIndicatorSample
 import androidx.compose.material.samples.MenuSample
+import androidx.compose.material.samples.MenuWithScrollStateSample
 import androidx.compose.material.samples.ModalBottomSheetSample
 import androidx.compose.material.samples.ModalDrawerSample
 import androidx.compose.material.samples.NavigationRailBottomAlignSample
@@ -398,6 +399,13 @@
         MenuSample()
     },
     Example(
+        name = ::MenuWithScrollStateSample.name,
+        description = MenusExampleDescription,
+        sourceUrl = MenusExampleSourceUrl
+    ) {
+        MenuWithScrollStateSample()
+    },
+    Example(
         name = ::ExposedDropdownMenuSample.name,
         description = MenusExampleDescription,
         sourceUrl = MenusExampleSourceUrl
@@ -703,14 +711,18 @@
         description = TextFieldsExampleDescription,
         sourceUrl = TextFieldsExampleSourceUrl
     ) {
-       TextArea()
+        TextArea()
     }
 ).map {
     // By default text field samples are minimal and don't have a `width` modifier to restrict the
     // width. As a result, they grow horizontally if enough text is typed. To prevent this behavior
     // in Catalog app the code below restricts the width of every text field sample
     it.copy(content = {
-        Box(Modifier.wrapContentWidth().width(280.dp)) { it.content() }
+        Box(
+            Modifier
+                .wrapContentWidth()
+                .width(280.dp)
+        ) { it.content() }
     })
 }
 
diff --git a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
index bf6f420..f9c6a77 100644
--- a/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
+++ b/compose/material/material/samples/src/main/java/androidx/compose/material/samples/MenuSamples.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material.Divider
 import androidx.compose.material.DropdownMenu
 import androidx.compose.material.DropdownMenuItem
@@ -29,6 +30,7 @@
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -41,7 +43,9 @@
 fun MenuSample() {
     var expanded by remember { mutableStateOf(false) }
 
-    Box(modifier = Modifier.fillMaxSize().wrapContentSize(Alignment.TopStart)) {
+    Box(modifier = Modifier
+        .fillMaxSize()
+        .wrapContentSize(Alignment.TopStart)) {
         IconButton(onClick = { expanded = true }) {
             Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
         }
@@ -61,4 +65,37 @@
             }
         }
     }
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun MenuWithScrollStateSample() {
+    var expanded by remember { mutableStateOf(false) }
+    val scrollState = rememberScrollState()
+    Box(
+        modifier = Modifier
+            .fillMaxSize()
+            .wrapContentSize(Alignment.TopStart)
+    ) {
+        IconButton(onClick = { expanded = true }) {
+            Icon(Icons.Default.MoreVert, contentDescription = "Localized description")
+        }
+        DropdownMenu(
+            expanded = expanded,
+            onDismissRequest = { expanded = false },
+            scrollState = scrollState
+        ) {
+            repeat(30) {
+                DropdownMenuItem(onClick = { /* Handle item! */ }) {
+                    Text("Item ${it + 1}")
+                }
+            }
+        }
+        LaunchedEffect(expanded) {
+            if (expanded) {
+                // Scroll to show the bottom menu items.
+                scrollState.scrollTo(scrollState.maxValue)
+            }
+        }
+    }
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index 17c6cc8..23300d9 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -22,7 +22,9 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -35,9 +37,11 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertTextContains
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -336,6 +340,47 @@
         // Should not have crashed.
     }
 
+    @Test
+    fun withScrolledContent() {
+        rule.setMaterialContent {
+            Box(Modifier.fillMaxSize()) {
+                ExposedDropdownMenuBox(
+                    modifier = Modifier.align(Alignment.Center),
+                    expanded = true,
+                    onExpandedChange = { }
+                ) {
+                    val scrollState = rememberScrollState()
+                    TextField(
+                        value = "",
+                        onValueChange = { },
+                        label = { Text("Label") },
+                    )
+                    ExposedDropdownMenu(
+                        expanded = true,
+                        onDismissRequest = { },
+                        scrollState = scrollState
+                    ) {
+                        repeat(100) {
+                            Box(
+                                Modifier
+                                    .testTag("MenuContent ${it + 1}")
+                                    .size(with(LocalDensity.current) { 70.toDp() })
+                            )
+                        }
+                    }
+                    LaunchedEffect(Unit) {
+                        scrollState.scrollTo(scrollState.maxValue)
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+    }
+
     @Composable
     fun ExposedDropdownMenuForTest(
         expanded: Boolean,
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index ace0f2a..8ad3242 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
@@ -29,6 +31,8 @@
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.hasAnyDescendant
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.isPopup
@@ -128,6 +132,42 @@
     }
 
     @Test
+    fun menu_scrolledContent() {
+        rule.setContent {
+            with(LocalDensity.current) {
+                Box(
+                    Modifier
+                        .requiredSize(20.toDp())
+                        .background(color = Color.Blue)
+                ) {
+                    val scrollState = rememberScrollState()
+                    DropdownMenu(
+                        expanded = true,
+                        onDismissRequest = {},
+                        scrollState = scrollState
+                    ) {
+                        repeat(100) {
+                            Box(
+                                Modifier
+                                    .testTag("MenuContent ${it + 1}")
+                                    .size(70.toDp())
+                            )
+                        }
+                    }
+                    LaunchedEffect(Unit) {
+                        scrollState.scrollTo(scrollState.maxValue)
+                    }
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("MenuContent 1").assertIsNotDisplayed()
+        rule.onNodeWithTag("MenuContent 100").assertIsDisplayed()
+    }
+
+    @Test
     fun menu_positioning_bottomEnd() {
         val screenWidth = 500
         val screenHeight = 1000
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 9043b15..cb2f6d2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -36,6 +36,10 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.testTag
@@ -56,11 +60,13 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.Velocity
 import androidx.compose.ui.unit.coerceAtMost
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -875,6 +881,7 @@
                         }
                     }
                 },
+                sheetGesturesEnabled = true,
                 content = { Box(Modifier.fillMaxSize()) }
             )
         }
@@ -926,6 +933,53 @@
     }
 
     @Test
+    fun modalBottomSheet_gesturesDisabled_doesNotParticipateInNestedScroll() =
+        runBlocking(AutoTestFrameClock()) {
+            lateinit var sheetState: ModalBottomSheetState
+            val sheetContentTag = "sheetContent"
+            val scrollConnection = object : NestedScrollConnection {}
+            val scrollDispatcher = NestedScrollDispatcher()
+            val sheetHeight = 300.dp
+            val sheetHeightPx = with(rule.density) { sheetHeight.toPx() }
+
+            rule.setContent {
+                sheetState = rememberModalBottomSheetState(
+                    initialValue = ModalBottomSheetValue.Expanded,
+                )
+                ModalBottomSheetLayout(
+                    sheetState = sheetState,
+                    sheetContent = {
+                        Box(
+                            Modifier
+                                .fillMaxWidth()
+                                .requiredHeight(sheetHeight)
+                                .nestedScroll(scrollConnection, scrollDispatcher)
+                                .testTag(sheetContentTag),
+                        )
+                    },
+                    sheetGesturesEnabled = false,
+                    content = { Box(Modifier.fillMaxSize()) },
+                )
+            }
+
+            assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+
+            val offsetBeforeScroll = sheetState.requireOffset()
+            scrollDispatcher.dispatchPreScroll(
+                Offset(x = 0f, y = -sheetHeightPx),
+                NestedScrollSource.Drag,
+            )
+            rule.waitForIdle()
+            assertWithMessage("Offset after scroll is equal to offset before scroll")
+                .that(sheetState.requireOffset()).isEqualTo(offsetBeforeScroll)
+
+            val highFlingVelocity = Velocity(x = 0f, y = with(rule.density) { 500.dp.toPx() })
+            scrollDispatcher.dispatchPreFling(highFlingVelocity)
+            rule.waitForIdle()
+            assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
+        }
+
+    @Test
     fun modalBottomSheet_anchorsChange_retainsCurrentValue() {
         lateinit var state: ModalBottomSheetState
         var amountOfItems by mutableStateOf(0)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
index 94c0373..e1bd14d 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/AndroidMenu.android.kt
@@ -17,11 +17,13 @@
 package androidx.compose.material
 
 import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.interaction.Interaction
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -70,6 +72,15 @@
  * tapping outside the menu's bounds
  * @param offset [DpOffset] to be added to the position of the menu
  */
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "DropdownMenu(expanded,onDismissRequest, modifier, offset, " +
+            "rememberScrollState(), properties, content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
@@ -78,6 +89,69 @@
     offset: DpOffset = DpOffset(0.dp, 0.dp),
     properties: PopupProperties = PopupProperties(focusable = true),
     content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    modifier = modifier,
+    offset = offset,
+    scrollState = rememberScrollState(),
+    properties = properties,
+    content = content
+)
+
+/**
+ * <a href="https://material.io/components/menus#dropdown-menu" class="external" target="_blank">Material Design dropdown menu</a>.
+ *
+ * A dropdown menu is a compact way of displaying multiple choices. It appears upon interaction with
+ * an element (such as an icon or button) or when users perform a specific action.
+ *
+ * ![Menus image](https://developer.android.com/images/reference/androidx/compose/material/menus.png)
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded whether the menu is expanded or not
+ * @param onDismissRequest called when the user requests to dismiss the menu, such as by tapping
+ * outside the menu's bounds
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param properties [PopupProperties] for further customization of this popup's behavior
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    scrollState: ScrollState = rememberScrollState(),
+    properties: PopupProperties = PopupProperties(focusable = true),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -100,6 +174,7 @@
             DropdownMenuContent(
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
+                scrollState = scrollState,
                 modifier = modifier,
                 content = content
             )
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index 4a46e56..660f1c2 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -21,6 +21,7 @@
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.gestures.awaitFirstDown
 import androidx.compose.foundation.gestures.waitForUpOrCancellation
@@ -30,6 +31,7 @@
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.ArrowDropDown
 import androidx.compose.material.internal.ExposedDropdownMenuPopup
@@ -223,6 +225,7 @@
      * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
      * tapping outside the menu's bounds
      * @param modifier The modifier to apply to this layout
+     * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
      * @param content The content of the [ExposedDropdownMenu]
      */
     @Composable
@@ -230,6 +233,7 @@
         expanded: Boolean,
         onDismissRequest: () -> Unit,
         modifier: Modifier = Modifier,
+        scrollState: ScrollState = rememberScrollState(),
         content: @Composable ColumnScope.() -> Unit
     ) {
         // TODO(b/202810604): use DropdownMenu when PopupProperties constructor is stable
@@ -261,6 +265,7 @@
                 DropdownMenuContent(
                     expandedStates = expandedStates,
                     transformOriginState = transformOriginState,
+                    scrollState = scrollState,
                     modifier = modifier.exposedDropdownSize(),
                     content = content
                 )
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 3e80837..9d3dadbd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -21,8 +21,9 @@
 import androidx.compose.animation.core.animateFloat
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.IntrinsicSize
@@ -33,14 +34,13 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.getValue
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -57,11 +57,11 @@
 import kotlin.math.max
 import kotlin.math.min
 
-@Suppress("ModifierParameter")
 @Composable
 internal fun DropdownMenuContent(
     expandedStates: MutableTransitionState<Boolean>,
     transformOriginState: MutableState<TransformOrigin>,
+    scrollState: ScrollState,
     modifier: Modifier = Modifier,
     content: @Composable ColumnScope.() -> Unit
 ) {
@@ -126,7 +126,7 @@
             modifier = modifier
                 .padding(vertical = DropdownMenuVerticalPadding)
                 .width(IntrinsicSize.Max)
-                .verticalScroll(rememberScrollState()),
+                .verticalScroll(scrollState),
             content = content
         )
     }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 9d1da3f..8b9eef6 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -528,6 +528,7 @@
  * @param sheetContent The content of the bottom sheet.
  * @param modifier Optional [Modifier] for the entire component.
  * @param sheetState The state of the bottom sheet.
+ * @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures.
  * @param sheetShape The shape of the bottom sheet.
  * @param sheetElevation The elevation of the bottom sheet.
  * @param sheetBackgroundColor The background color of the bottom sheet.
@@ -547,6 +548,7 @@
     modifier: Modifier = Modifier,
     sheetState: ModalBottomSheetState =
         rememberModalBottomSheetState(Hidden),
+    sheetGesturesEnabled: Boolean = true,
     sheetShape: Shape = MaterialTheme.shapes.large,
     sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
     sheetBackgroundColor: Color = MaterialTheme.colors.surface,
@@ -585,13 +587,17 @@
                 .align(Alignment.TopCenter) // We offset from the top so we'll center from there
                 .widthIn(max = MaxModalBottomSheetWidth)
                 .fillMaxWidth()
-                .nestedScroll(
-                    remember(sheetState.anchoredDraggableState, orientation) {
-                        ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
-                            state = sheetState.anchoredDraggableState,
-                            orientation = orientation
+                .then(
+                    if (sheetGesturesEnabled) {
+                        Modifier.nestedScroll(
+                            remember(sheetState.anchoredDraggableState, orientation) {
+                                ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+                                    state = sheetState.anchoredDraggableState,
+                                    orientation = orientation
+                                )
+                            }
                         )
-                    }
+                    } else Modifier
                 )
                 .offset {
                     IntOffset(
@@ -604,7 +610,8 @@
                 .anchoredDraggable(
                     state = sheetState.anchoredDraggableState,
                     orientation = orientation,
-                    enabled = sheetState.anchoredDraggableState.currentValue != Hidden,
+                    enabled = sheetGesturesEnabled &&
+                        sheetState.anchoredDraggableState.currentValue != Hidden,
                 )
                 .onSizeChanged { sheetSize ->
                     val anchors = buildMap {
@@ -619,37 +626,45 @@
                     }
                     sheetState.anchoredDraggableState.updateAnchors(anchors, anchorChangeCallback)
                 }
-                .semantics {
-                    if (sheetState.isVisible) {
-                        dismiss {
-                            if (sheetState.anchoredDraggableState.confirmValueChange(Hidden)) {
-                                scope.launch { sheetState.hide() }
-                            }
-                            true
-                        }
-                        if (sheetState.anchoredDraggableState.currentValue == HalfExpanded) {
-                            expand {
-                                if (sheetState.anchoredDraggableState.confirmValueChange(
-                                        Expanded
-                                    )
-                                ) {
-                                    scope.launch { sheetState.expand() }
+                .then(
+                    if (sheetGesturesEnabled) {
+                        Modifier.semantics {
+                            if (sheetState.isVisible) {
+                                dismiss {
+                                    if (
+                                        sheetState.anchoredDraggableState.confirmValueChange(Hidden)
+                                    ) {
+                                        scope.launch { sheetState.hide() }
+                                    }
+                                    true
                                 }
-                                true
-                            }
-                        } else if (sheetState.hasHalfExpandedState) {
-                            collapse {
-                                if (sheetState.anchoredDraggableState.confirmValueChange(
-                                        HalfExpanded
-                                    )
+                                if (sheetState.anchoredDraggableState.currentValue
+                                    == HalfExpanded
                                 ) {
-                                    scope.launch { sheetState.halfExpand() }
+                                    expand {
+                                        if (sheetState.anchoredDraggableState.confirmValueChange(
+                                                Expanded
+                                            )
+                                        ) {
+                                            scope.launch { sheetState.expand() }
+                                        }
+                                        true
+                                    }
+                                } else if (sheetState.hasHalfExpandedState) {
+                                    collapse {
+                                        if (sheetState.anchoredDraggableState.confirmValueChange(
+                                                HalfExpanded
+                                            )
+                                        ) {
+                                            scope.launch { sheetState.halfExpand() }
+                                        }
+                                        true
+                                    }
                                 }
-                                true
                             }
                         }
-                    }
-                },
+                    } else Modifier
+                ),
             shape = sheetShape,
             elevation = sheetElevation,
             color = sheetBackgroundColor,
diff --git a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
index 4da69f2..ea91b6c 100644
--- a/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
+++ b/compose/material/material/src/desktopMain/kotlin/androidx/compose/material/DesktopMenu.desktop.kt
@@ -21,6 +21,8 @@
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.ScrollState
+import androidx.compose.foundation.rememberScrollState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.mutableStateOf
@@ -74,7 +76,15 @@
  * @param offset [DpOffset] to be added to the position of the menu
  * @param content content lambda
  */
-@Suppress("ModifierParameter")
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "DropdownMenu(expanded,onDismissRequest, focusable, modifier, offset, " +
+            "rememberScrollState(), content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a DropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
@@ -83,6 +93,64 @@
     modifier: Modifier = Modifier,
     offset: DpOffset = DpOffset(0.dp, 0.dp),
     content: @Composable ColumnScope.() -> Unit
+) = DropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    focusable = focusable,
+    modifier = modifier,
+    offset = offset,
+    scrollState = rememberScrollState(),
+    content = content
+)
+
+/**
+ * A Material Design [dropdown menu](https://material.io/components/menus#dropdown-menu).
+ *
+ * A [DropdownMenu] behaves similarly to a [Popup], and will use the position of the parent layout
+ * to position itself on screen. Commonly a [DropdownMenu] will be placed in a [Box] with a sibling
+ * that will be used as the 'anchor'. Note that a [DropdownMenu] by itself will not take up any
+ * space in a layout, as the menu is displayed in a separate window, on top of other content.
+ *
+ * The [content] of a [DropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus. Also note that the [content] is placed inside a scrollable [Column],
+ * so using a [LazyColumn] as the root layout inside [content] is unsupported.
+ *
+ * [onDismissRequest] will be called when the menu should close - for example when there is a
+ * tap outside the menu, or when the back key is pressed.
+ *
+ * [DropdownMenu] changes its positioning depending on the available space, always trying to be
+ * fully visible. It will try to expand horizontally, depending on layout direction, to the end of
+ * its parent, then to the start of its parent, and then screen end-aligned. Vertically, it will
+ * try to expand to the bottom of its parent, then from the top of its parent, and then screen
+ * top-aligned. An [offset] can be provided to adjust the positioning of the menu for cases when
+ * the layout bounds of its parent do not coincide with its visual bounds. Note the offset will
+ * be applied in the direction in which the menu will decide to expand.
+ *
+ * Example usage:
+ * @sample androidx.compose.material.samples.MenuSample
+ *
+ * Example usage with a [ScrollState] to control the menu items scroll position:
+ * @sample androidx.compose.material.samples.MenuWithScrollStateSample
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param offset [DpOffset] to be added to the position of the menu
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun DropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    focusable: Boolean = true,
+    modifier: Modifier = Modifier,
+    offset: DpOffset = DpOffset(0.dp, 0.dp),
+    scrollState: ScrollState = rememberScrollState(),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -110,6 +178,7 @@
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
                 modifier = modifier,
+                scrollState = scrollState,
                 content = content
             )
         }
@@ -163,8 +232,19 @@
  * @param expanded Whether the menu is currently open and visible to the user
  * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
  * tapping outside the menu's bounds
+ * @param focusable Sets the ability for the menu to capture focus
+ * @param modifier The modifier for this layout.
+ * @param content The content lambda.
  */
-@Suppress("ModifierParameter")
+@Deprecated(
+    level = DeprecationLevel.HIDDEN,
+    replaceWith = ReplaceWith(
+        expression = "CursorDropdownMenu(expanded,onDismissRequest, focusable, modifier, " +
+            "rememberScrollState(), content)",
+        "androidx.compose.foundation.rememberScrollState"
+    ),
+    message = "Replaced by a CursorDropdownMenu function with a ScrollState parameter"
+)
 @Composable
 fun CursorDropdownMenu(
     expanded: Boolean,
@@ -172,6 +252,40 @@
     focusable: Boolean = true,
     modifier: Modifier = Modifier,
     content: @Composable ColumnScope.() -> Unit
+) = CursorDropdownMenu(
+    expanded = expanded,
+    onDismissRequest = onDismissRequest,
+    focusable = focusable,
+    modifier = modifier,
+    scrollState = rememberScrollState(),
+    content = content
+)
+
+/**
+ *
+ * A [CursorDropdownMenu] behaves similarly to [Popup] and will use the current position of the mouse
+ * cursor to position itself on screen.
+ *
+ * The [content] of a [CursorDropdownMenu] will typically be [DropdownMenuItem]s, as well as custom
+ * content. Using [DropdownMenuItem]s will result in a menu that matches the Material
+ * specification for menus.
+ *
+ * @param expanded Whether the menu is currently open and visible to the user
+ * @param onDismissRequest Called when the user requests to dismiss the menu, such as by
+ * tapping outside the menu's bounds
+ * @param focusable Whether the dropdown can capture focus
+ * @param modifier [Modifier] to be applied to the menu's content
+ * @param scrollState a [ScrollState] to used by the menu's content for items vertical scrolling
+ * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
+ */
+@Composable
+fun CursorDropdownMenu(
+    expanded: Boolean,
+    onDismissRequest: () -> Unit,
+    focusable: Boolean = true,
+    modifier: Modifier = Modifier,
+    scrollState: ScrollState = rememberScrollState(),
+    content: @Composable ColumnScope.() -> Unit
 ) {
     val expandedStates = remember { MutableTransitionState(false) }
     expandedStates.targetState = expanded
@@ -188,6 +302,7 @@
                 expandedStates = expandedStates,
                 transformOriginState = transformOriginState,
                 modifier = modifier,
+                scrollState = scrollState,
                 content = content
             )
         }
diff --git a/compose/material3/material3-adaptive/build.gradle b/compose/material3/material3-adaptive/build.gradle
index c263c79..b1aaced 100644
--- a/compose/material3/material3-adaptive/build.gradle
+++ b/compose/material3/material3-adaptive/build.gradle
@@ -17,7 +17,7 @@
 import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import androidx.build.KmpPlatformsKt
 
 plugins {
     id("AndroidXPlugin")
@@ -25,49 +25,61 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the analogous update in the
-         * corresponding block below
-         */
-        implementation(libs.kotlinStdlibCommon)
-
-        api("androidx.annotation:annotation:1.1.0")
-
-        api(project(":compose:foundation:foundation"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:foundation:foundation"))
             }
+        }
 
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
+        commonTest {
+            dependencies {
             }
+        }
 
-            desktopMain.dependencies {
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
             }
         }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+        }
     }
 }
 
diff --git a/compose/material3/material3-window-size-class/build.gradle b/compose/material3/material3-window-size-class/build.gradle
index 9eb3383..495dd73 100644
--- a/compose/material3/material3-window-size-class/build.gradle
+++ b/compose/material3/material3-window-size-class/build.gradle
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
+import androidx.build.KmpPlatformsKt
 import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,69 +25,73 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        implementation(libs.kotlinStdlibCommon)
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api("androidx.compose.ui:ui:1.2.1")
-        api("androidx.compose.ui:ui-unit:1.2.1")
-        implementation("androidx.window:window:1.0.0")
-
-        testImplementation(libs.kotlinTest)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-
-        samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    kotlin {
-        android()
-        jvm("desktop")
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:runtime:runtime"))
                 api(project(":compose:ui:ui"))
                 api(project(":compose:ui:ui-unit"))
             }
+        }
 
-            jvmMain.dependencies {
+        commonTest {
+            dependencies {
+
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
             }
+        }
 
-            androidMain.dependencies {
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    // Because dependencies are pinned in the android/common code.
+                    api(project(":compose:runtime:runtime"))
+                    api(project(":compose:ui:ui"))
+                    api(project(":compose:ui:ui-unit"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 implementation("androidx.window:window:1.0.0")
             }
+        }
 
-            androidMain.dependsOn(jvmMain)
-            desktopMain.dependsOn(jvmMain)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
 
-            androidTest.dependencies {
-                implementation(libs.kotlinTest)
-                implementation(libs.truth)
+                }
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:test-utils"))
                 implementation(project(":compose:foundation:foundation"))
                 implementation(libs.testRules)
@@ -96,9 +100,24 @@
                 implementation(libs.truth)
             }
         }
-    }
-    dependencies {
-        samples(project(":compose:material3:material3-window-size-class:material3-window-size-class-samples"))
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.kotlinTest)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+
+                }
+            }
+        }
     }
 }
 
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
index 7374b08..1f62b814 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DividerScreenshotTest.kt
@@ -31,6 +31,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Assume.assumeFalse
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -64,6 +65,8 @@
 
     @Test
     fun darkTheme() {
+        assumeFalse("See b/272301182", Build.VERSION.SDK_INT == 33)
+
         composeTestRule.setMaterialContent(darkColorScheme()) {
             Column(Modifier.testTag(Tag)) {
                 Spacer(Modifier.size(10.dp))
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
index 0df58c9..26f91f1 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/MenuTest.kt
@@ -146,7 +146,6 @@
         }
     }
 
-    @OptIn(ExperimentalMaterial3Api::class)
     @Test
     fun menu_scrolledContent() {
         rule.setContent {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
index 3693245..3c05d96 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarScreenshotTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.runtime.Composable
@@ -33,6 +34,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -112,6 +114,25 @@
     }
 
     @Test
+    fun lightTheme_customHeight() {
+        val interactionSource = MutableInteractionSource()
+
+        var scope: CoroutineScope? = null
+
+        composeTestRule.setMaterialContent(lightColorScheme()) {
+            scope = rememberCoroutineScope()
+            DefaultNavigationBar(interactionSource, Modifier.height(64.dp))
+        }
+
+        assertNavigationBarMatches(
+            scope = scope!!,
+            interactionSource = interactionSource,
+            interaction = null,
+            goldenIdentifier = "navigationBar_lightTheme_customHeight"
+        )
+    }
+
+    @Test
     fun darkTheme_defaultColors() {
         val interactionSource = MutableInteractionSource()
 
@@ -211,14 +232,16 @@
  *
  * @param interactionSource the [MutableInteractionSource] for the first [NavigationBarItem], to
  * control its visual state.
+ * @param modifier the [Modifier] applied to the navigation bar
  * @param setUnselectedItemsAsDisabled when true, marks unselected items as disabled
  */
 @Composable
 private fun DefaultNavigationBar(
     interactionSource: MutableInteractionSource,
+    modifier: Modifier = Modifier,
     setUnselectedItemsAsDisabled: Boolean = false,
 ) {
-    Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
+    Box(modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
         NavigationBar {
             NavigationBarItem(
                 icon = { Icon(Icons.Filled.Favorite, null) },
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
index e9a0168..2f6c21f 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/NavigationBarTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
 import androidx.compose.material3.tokens.NavigationBarTokens
@@ -266,42 +267,43 @@
     @Test
     fun navigationBarItemContent_withLabel_sizeAndPosition() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box {
-                NavigationBar {
-                    NavigationBarItem(
-                        modifier = Modifier.testTag("item"),
-                        icon = {
-                            Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
-                        },
-                        label = {
-                            Text("ItemText")
-                        },
-                        selected = true,
-                        onClick = {}
-                    )
-                }
+            NavigationBar {
+                NavigationBarItem(
+                    modifier = Modifier.testTag("item"),
+                    icon = {
+                        Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+                    },
+                    label = {
+                        Text("ItemText")
+                    },
+                    selected = true,
+                    onClick = {}
+                )
             }
         }
 
         val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
         val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
             .getUnclippedBoundsInRoot()
-        val textBounds = rule.onNodeWithText("ItemText", useUnmergedTree = true)
-            .getUnclippedBoundsInRoot()
 
-        // Distance from the bottom of the item to the text bottom, and from the top of the icon to
-        // the top of the item
-        val verticalPadding = NavigationBarItemVerticalPadding
-
-        val itemBottom = itemBounds.height + itemBounds.top
-        // Text bottom should be `verticalPadding` from the bottom of the item
-        textBounds.bottom.assertIsEqualTo(itemBottom - verticalPadding)
+        // Distance from the top of the item to the top of the icon for the default height
+        val verticalPadding = 16.dp
 
         rule.onNodeWithTag("icon", useUnmergedTree = true)
-            // The icon should be centered in the item
+            // The icon should be horizontally centered in the item
             .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
             // The top of the icon is `verticalPadding` below the top of the item
             .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+        val iconBottom = iconBounds.top + iconBounds.height
+        // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+        // bottom of the icon
+        rule.onNodeWithText("ItemText", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+            .top
+            .assertIsEqualTo(
+                iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+            )
     }
 
     @Test
@@ -367,6 +369,49 @@
     }
 
     @Test
+    fun navigationBarItemContent_customHeight_withLabel_sizeAndPosition() {
+        val defaultHeight = NavigationBarTokens.ContainerHeight
+        val customHeight = 64.dp
+
+        rule.setMaterialContent(lightColorScheme()) {
+            NavigationBar(Modifier.height(customHeight)) {
+                NavigationBarItem(
+                    modifier = Modifier.testTag("item"),
+                    icon = {
+                        Icon(Icons.Filled.Favorite, null, Modifier.testTag("icon"))
+                    },
+                    label = { Text("Label") },
+                    selected = true,
+                    onClick = {}
+                )
+            }
+        }
+
+        // Vertical padding is removed symmetrically from top and bottom for smaller heights
+        val verticalPadding = 16.dp - (defaultHeight - customHeight) / 2
+
+        val itemBounds = rule.onNodeWithTag("item").getUnclippedBoundsInRoot()
+        val iconBounds = rule.onNodeWithTag("icon", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+
+        rule.onNodeWithTag("icon", useUnmergedTree = true)
+            // The icon should be horizontally centered in the item
+            .assertLeftPositionInRootIsEqualTo((itemBounds.width - iconBounds.width) / 2)
+            // The top of the icon is `verticalPadding` below the top of the item
+            .assertTopPositionInRootIsEqualTo(itemBounds.top + verticalPadding)
+
+        val iconBottom = iconBounds.top + iconBounds.height
+        // Text should be `IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding` from the
+        // bottom of the item
+        rule.onNodeWithText("Label", useUnmergedTree = true)
+            .getUnclippedBoundsInRoot()
+            .top
+            .assertIsEqualTo(
+                iconBottom + IndicatorVerticalPadding + NavigationBarIndicatorToLabelPadding
+            )
+    }
+
+    @Test
     fun navigationBar_selectNewItem() {
         rule.setMaterialContent(lightColorScheme()) {
             var selectedItem by remember { mutableStateOf(0) }
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
index 0acd46a..5948252 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/AndroidMenu.android.kt
@@ -75,7 +75,6 @@
  * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
  */
 @OptIn(ExperimentalMaterial3Api::class)
-@Suppress("ModifierParameter")
 @Deprecated(
     level = DeprecationLevel.HIDDEN,
     replaceWith = ReplaceWith(
@@ -147,7 +146,6 @@
  * @param properties [PopupProperties] for further customization of this popup's behavior
  * @param content the content of this dropdown menu, typically a [DropdownMenuItem]
  */
-@Suppress("ModifierParameter")
 @Composable
 fun DropdownMenu(
     expanded: Boolean,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
index 318985d..4b558c6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Menu.kt
@@ -61,7 +61,6 @@
 import kotlin.math.max
 import kotlin.math.min
 
-@Suppress("ModifierParameter")
 @Composable
 internal fun DropdownMenuContent(
     expandedStates: MutableTransitionState<Boolean>,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
index 2746436..5f1bf664 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt
@@ -535,8 +535,8 @@
  * [animationProgress].
  *
  * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] will be placed
- * near the top of the item and the [labelPlaceable] will be placed near the bottom, according to
- * the spec.
+ * near the top of the item and the [labelPlaceable] will be placed beneath it with padding,
+ * according to the spec.
  *
  * When [animationProgress] is 1 (representing the selected state), the positions will be the same
  * as above.
@@ -573,11 +573,13 @@
 ): MeasureResult {
     val height = constraints.maxHeight
 
-    // Label should be `ItemVerticalPadding` from the bottom
-    val labelY = height - labelPlaceable.height - NavigationBarItemVerticalPadding.roundToPx()
+    val contentTotalHeight = iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+        NavigationBarIndicatorToLabelPadding.roundToPx() + labelPlaceable.height
+    val contentVerticalPadding = ((height - contentTotalHeight) / 2)
+        .coerceAtLeast(IndicatorVerticalPadding.roundToPx())
 
-    // Icon (when selected) should be `ItemVerticalPadding` from the top
-    val selectedIconY = NavigationBarItemVerticalPadding.roundToPx()
+    // Icon (when selected) should be `contentVerticalPadding` from top
+    val selectedIconY = contentVerticalPadding
     val unselectedIconY =
         if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2
 
@@ -588,6 +590,10 @@
     // animationProgress.
     val offset = (iconDistance * (1 - animationProgress)).roundToInt()
 
+    // Label should be fixed padding below icon
+    val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPadding.roundToPx() +
+        NavigationBarIndicatorToLabelPadding.roundToPx()
+
     val containerWidth = constraints.maxWidth
 
     val labelX = (containerWidth - labelPlaceable.width) / 2
@@ -626,12 +632,13 @@
 internal val NavigationBarItemHorizontalPadding: Dp = 8.dp
 
 /*@VisibleForTesting*/
-internal val NavigationBarItemVerticalPadding: Dp = 16.dp
+internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp
 
 private val IndicatorHorizontalPadding: Dp =
     (NavigationBarTokens.ActiveIndicatorWidth - NavigationBarTokens.IconSize) / 2
 
-private val IndicatorVerticalPadding: Dp =
+/*@VisibleForTesting*/
+internal val IndicatorVerticalPadding: Dp =
     (NavigationBarTokens.ActiveIndicatorHeight - NavigationBarTokens.IconSize) / 2
 
 private val IndicatorVerticalOffset: Dp = 12.dp
\ No newline at end of file
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt
new file mode 100644
index 0000000..70fd1cd
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetector.kt
@@ -0,0 +1,253 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("UnstableApiUsage")
+
+package androidx.compose.runtime.lint
+
+import androidx.compose.lint.Names
+import androidx.compose.lint.isInPackageName
+import androidx.compose.lint.isVoidOrUnit
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.UastLintUtils.Companion.tryResolveUDeclaration
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiParameter
+import java.util.EnumSet
+import org.jetbrains.kotlin.psi.psiUtil.parameterIndex
+import org.jetbrains.uast.UBlockExpression
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UDeclarationsExpression
+import org.jetbrains.uast.UElement
+import org.jetbrains.uast.UExpression
+import org.jetbrains.uast.UExpressionList
+import org.jetbrains.uast.UParameter
+import org.jetbrains.uast.USimpleNameReferenceExpression
+import org.jetbrains.uast.UVariable
+import org.jetbrains.uast.skipParenthesizedExprDown
+import org.jetbrains.uast.toUElement
+
+/**
+ * Detector to warn when [Unit] is being passed "opaquely" as an argument to any of the methods in
+ * [getApplicableMethodNames]. An argument is defined as an opaque unit key if all the following
+ * are true:
+ *  - The argument is an expression of type `Unit`
+ *  - The argument is being passed to a parameter of type `Any?`
+ *  - The argument is not the `Unit` literal
+ *  - The argument is not a trivial variable or property read expression
+ */
+class OpaqueUnitKeyDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableMethodNames(): List<String> = listOf(
+        Names.Runtime.Remember.shortName,
+        Names.Runtime.RememberSaveable.shortName,
+        Names.Runtime.DisposableEffect.shortName,
+        Names.Runtime.LaunchedEffect.shortName,
+        Names.Runtime.ProduceState.shortName,
+        Names.Runtime.ReusableContent.shortName,
+        Names.Runtime.Key.shortName,
+    )
+
+    override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+        if (!method.isInPackageName(Names.Runtime.PackageName)) return
+
+        method.parameterList.parameters.forEach { parameter ->
+            val arg = node.getArgumentForParameter(parameter.parameterIndex())
+            if (parameter.isNullableAny()) {
+                if (arg?.isOpaqueUnitExpression() == true) {
+                    reportOpaqueUnitArgKey(
+                        context = context,
+                        method = method,
+                        methodInvocation = node,
+                        parameter = parameter,
+                        argument = arg
+                    )
+                }
+            } else if (parameter.isPotentiallyVarArgs() && arg is UExpressionList) {
+                arg.expressions.forEach { varArg ->
+                    if (varArg.isOpaqueUnitExpression()) {
+                        reportOpaqueUnitArgKey(
+                            context = context,
+                            method = method,
+                            methodInvocation = node,
+                            parameter = parameter,
+                            argument = varArg
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    private fun reportOpaqueUnitArgKey(
+        context: JavaContext,
+        method: PsiMethod,
+        methodInvocation: UCallExpression,
+        parameter: PsiParameter,
+        argument: UExpression
+    ) {
+        val rootExpression = methodInvocation.resolveRootExpression()
+        val rootExpressionLocation = context.getLocation(rootExpression)
+
+        context.report(
+            OpaqueUnitKey,
+            argument,
+            context.getLocation(argument),
+            "Implicitly passing `Unit` as argument to ${parameter.name}",
+            fix()
+                .name(
+                    "Move expression outside of `${method.name}`'s arguments " +
+                        "and pass `Unit` explicitly"
+                )
+                .composite(
+                    if (rootExpression.isInPhysicalBlock()) {
+                        // If we're in a block where we can add an expression without breaking any
+                        // syntax rules, promote the argument's expression to a sibling.
+                        fix()
+                            .replace()
+                            .range(rootExpressionLocation)
+                            .beginning()
+                            .with("${argument.asSourceString()}\n")
+                            .reformat(true)
+                            .build()
+                    } else {
+                        // If we're not in a block, then introduce one for cheap by wrapping the
+                        // call with Kotlin's `run` function to a format that appears as follows:
+                        //
+                        // ```
+                        // run {
+                        //    theArgument()
+                        //    theMethod(...)
+                        // }
+                        // ```
+                        fix()
+                            .composite(
+                                fix()
+                                    .replace()
+                                    .range(rootExpressionLocation)
+                                    .beginning()
+                                    .with("kotlin.run {\n${argument.asSourceString()}\n")
+                                    .reformat(true)
+                                    .shortenNames()
+                                    .build(),
+                                fix()
+                                    .replace()
+                                    .range(rootExpressionLocation)
+                                    .end()
+                                    .with("\n}")
+                                    .reformat(true)
+                                    .build()
+                            )
+                    },
+
+                    // Replace the old parameter with the Unit literal
+                    fix()
+                        .replace()
+                        .range(context.getLocation(argument))
+                        .with(FqUnitName)
+                        .shortenNames()
+                        .build(),
+                )
+        )
+    }
+
+    private fun UCallExpression.resolveRootExpression(): UExpression {
+        var root: UExpression = this
+        var parent: UExpression? = root.getParentExpression()
+        while (parent != null && parent !is UBlockExpression) {
+            if (!parent.isVirtual) { root = parent }
+            parent = parent.getParentExpression()
+        }
+        return root
+    }
+
+    private fun UExpression.isInPhysicalBlock(): Boolean {
+        var parent: UElement? = this
+        while (parent != null) {
+            if (parent is UBlockExpression) {
+                return !parent.isVirtual
+            }
+            parent = parent.uastParent
+        }
+        return false
+    }
+
+    private val UElement.isVirtual get() = sourcePsi == null
+
+    private fun UExpression.getParentExpression(): UExpression? {
+        return when (val parent = uastParent) {
+            is UVariable -> parent.uastParent as UDeclarationsExpression
+            is UExpression -> parent
+            else -> null
+        }
+    }
+
+    private fun PsiParameter.isNullableAny(): Boolean {
+        val element = toUElement() as UParameter
+        return element.type.canonicalText == FqJavaObjectName &&
+            element.getAnnotations().any { it.qualifiedName == FqKotlinNullableAnnotation }
+    }
+
+    private fun PsiParameter.isPotentiallyVarArgs(): Boolean {
+        return type.canonicalText == "$FqJavaObjectName[]"
+    }
+
+    private fun UExpression.isOpaqueUnitExpression(): Boolean {
+        return getExpressionType().isVoidOrUnit && !isUnitLiteral()
+    }
+
+    private fun UExpression.isUnitLiteral(): Boolean {
+        val expr = skipParenthesizedExprDown() ?: this
+        if (expr !is USimpleNameReferenceExpression) return false
+
+        return (expr.tryResolveUDeclaration() as? UClass)?.qualifiedName == FqUnitName
+    }
+
+    companion object {
+        private const val FqJavaObjectName = "java.lang.Object"
+        private const val FqUnitName = "kotlin.Unit"
+        private const val FqKotlinNullableAnnotation = "org.jetbrains.annotations.Nullable"
+
+        val OpaqueUnitKey = Issue.create(
+            "OpaqueUnitKey",
+            "Passing an expression which always returns `Unit` as a key argument",
+            "Certain Compose functions including `remember`, `LaunchedEffect`, and " +
+                "`DisposableEffect` declare (and sometimes require) one or more key parameters. " +
+                "When a key parameter changes, it is a signal that the previous invocation is " +
+                "now invalid. In certain cases, it may be required to pass `Unit` as a key to " +
+                "one of these functions, indicating that the invocation never becomes invalid. " +
+                "Using `Unit` as a key should be done infrequently, and should always be done " +
+                "explicitly by passing the `Unit` literal. This inspection checks for " +
+                "invocations where `Unit` is being passed as a key argument in any form other " +
+                "than the `Unit` literal. This is usually done by mistake, and can harm " +
+                "readability. If a Unit expression is being passed as a key, it is always " +
+                "equivalent to move the expression before the function invocation and pass the " +
+                "`Unit` literal instead.",
+            Category.CORRECTNESS, 3, Severity.WARNING,
+            Implementation(
+                OpaqueUnitKeyDetector::class.java,
+                EnumSet.of(Scope.JAVA_FILE, Scope.TEST_SOURCES)
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
index bc749d0..8a2444e 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/RuntimeIssueRegistry.kt
@@ -41,6 +41,7 @@
         MutableCollectionMutableStateDetector.MutableCollectionMutableState,
         ProduceStateDetector.ProduceStateDoesNotAssignValue,
         RememberDetector.RememberReturnType,
+        OpaqueUnitKeyDetector.OpaqueUnitKey,
         UnrememberedStateDetector.UnrememberedState
     )
     override val vendor = Vendor(
diff --git a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt
new file mode 100644
index 0000000..597189a
--- /dev/null
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/OpaqueUnitKeyDetectorTest.kt
@@ -0,0 +1,1744 @@
+/*
+ * 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.runtime.lint
+
+import androidx.compose.lint.test.Stubs
+import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.detector.api.Detector
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/* ktlint-disable max-line-length */
+@RunWith(JUnit4::class)
+class OpaqueUnitKeyDetectorTest : LintDetectorTest() {
+    override fun getDetector(): Detector = OpaqueUnitKeyDetector()
+
+    override fun getIssues() = listOf(OpaqueUnitKeyDetector.OpaqueUnitKey)
+
+    // region remember test cases
+
+    @Test
+    fun remember_withUnitLiteralKey_doesNotError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x = remember(Unit) { listOf(1, 2, 3) }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun remember_withUnitPropertyRead_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    val unitProperty = Unit
+
+                    @Composable
+                    fun Test() {
+                        val x = remember(unitProperty) { listOf(1, 2, 3) }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x = remember(unitProperty) { listOf(1, 2, 3) }
+                                         ~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+-                         val x = remember(unitProperty) { listOf(1, 2, 3) }
++                         unitProperty
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+                """
+            )
+    }
+
+    @Test
+    fun remember_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x = remember(produceUnit()) { listOf(1, 2, 3) }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x = remember(produceUnit()) { listOf(1, 2, 3) }
+                                         ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x = remember(produceUnit()) { listOf(1, 2, 3) }
++                         produceUnit()
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+                """
+            )
+    }
+
+    @Test
+    fun remember_withUnitComposableInvocation_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
+                    }
+
+                    @Composable
+                    fun AnotherComposable() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
+                                         ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x = remember(AnotherComposable()) { listOf(1, 2, 3) }
++                         AnotherComposable()
++ val x = remember(kotlin.Unit) { listOf(1, 2, 3) }
+                """
+            )
+    }
+
+    @Test
+    fun remember_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                    fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
+                                          ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 7: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+-                     fun test() = remember(produceUnit()) { listOf(1, 2, 3) }
++                     fun test() = kotlin.run {
++ produceUnit()
++ remember(kotlin.Unit) { listOf(1, 2, 3) }
++ }
+                """
+            )
+    }
+
+    @Test
+    fun remember_withIfStatementThatReturnsUnit_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        val x = remember(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                doSomethingElse()
+                            }
+                        ) { listOf(1, 2, 3) }
+                    }
+
+                    fun doSomething() {}
+                    fun doSomethingElse() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                            if (condition) {
+                            ^
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x = remember(
+-                             if (condition) {
+-                                 doSomething()
+-                             } else {
+-                                 doSomethingElse()
+-                             }
++                         if (condition) {
++     doSomething()
++ }else {
++     doSomethingElse()
++ }
++ val x = remember(
++                             kotlin.Unit
+                """
+            )
+    }
+
+    @Test
+    fun remember_withIfStatementCoercedToAny_doesNotReportError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        val x = remember(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                42
+                            }
+                        ) { listOf(1, 2, 3) }
+                    }
+
+                    fun doSomething() {}
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun remember_twoKeys_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+                        val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
+                                             ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `remember`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x = remember(42, produceUnit()) { listOf(1, 2, 3) }
++                         produceUnit()
++ val x = remember(42, kotlin.Unit) { listOf(1, 2, 3) }
+                """
+            )
+    }
+
+    // endregion remember test cases
+
+    // region produceState test cases
+
+    @Test
+    fun produceState_withUnitLiteralKey_doesNotError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x by produceState("123", Unit) { /* Do nothing. */ }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun produceState_withUnitPropertyRead_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    val unitProperty = Unit
+
+                    @Composable
+                    fun Test() {
+                        val x by produceState("123", unitProperty) { /* Do nothing. */ }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x by produceState("123", unitProperty) { /* Do nothing. */ }
+                                                     ~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+-                         val x by produceState("123", unitProperty) { /* Do nothing. */ }
++                         unitProperty
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+                """
+            )
+    }
+
+    @Test
+    fun produceState_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x by produceState("123", produceUnit()) { /* Do nothing. */ }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x by produceState("123", produceUnit()) { /* Do nothing. */ }
+                                                     ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x by produceState("123", produceUnit()) { /* Do nothing. */ }
++                         produceUnit()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+                """
+            )
+    }
+
+    @Test
+    fun produceState_withUnitComposableInvocation_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
+                    }
+
+                    @Composable
+                    fun AnotherComposable() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
+                                                     ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x by produceState("123", AnotherComposable()) { /* Do nothing. */ }
++                         AnotherComposable()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing. */ }
+                """
+            )
+    }
+
+    @Test
+    fun produceState_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                    fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
+                                                     ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 7: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+-                     fun test() = produceState("123", produceUnit()) { /* Do nothing. */ }
++                     fun test() = kotlin.run {
++ produceUnit()
++ produceState("123", kotlin.Unit) { /* Do nothing. */ }
++ }
+                """
+            )
+    }
+
+    @Test
+    fun produceState_withIfStatementThatReturnsUnit_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        val x by produceState(
+                            initialValue = "123",
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                doSomethingElse()
+                            }
+                        ) { /* Do nothing. */ }
+                    }
+
+                    fun doSomething() {}
+                    fun doSomethingElse() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                            if (condition) {
+                            ^
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x by produceState(
++                         if (condition) {
++     doSomething()
++ }else {
++     doSomethingElse()
++ }
++ val x by produceState(
+-                             if (condition) {
+-                                 doSomething()
+-                             } else {
+-                                 doSomethingElse()
+-                             }
++                             kotlin.Unit
+                """
+            )
+    }
+
+    @Test
+    fun produceState_withIfStatementCoercedToAny_doesNotReportError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        val x by produceState(
+                            initialValue = "123",
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                42
+                            }
+                        ) { /* Do nothing */ }
+                    }
+
+                    fun doSomething() {}
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun produceState_twoKeys_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.SnapshotState,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        val x by produceState("123", produceUnit()) { /* Do nothing */ }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        val x by produceState("123", produceUnit()) { /* Do nothing */ }
+                                                     ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `produceState`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         val x by produceState("123", produceUnit()) { /* Do nothing */ }
++                         produceUnit()
++ val x by produceState("123", kotlin.Unit) { /* Do nothing */ }
+                """
+            )
+    }
+
+    // endregion produceState test cases
+
+    // region DisposableEffect test cases
+
+    @Test
+    fun disposableEffect_withUnitLiteralKey_doesNotError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        DisposableEffect(Unit) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun disposableEffect_withUnitPropertyRead_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    val unitProperty = Unit
+
+                    @Composable
+                    fun Test() {
+                        DisposableEffect(unitProperty) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        DisposableEffect(unitProperty) {
+                                         ~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+-                         DisposableEffect(unitProperty) {
++                         unitProperty
++ DisposableEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun disposableEffect_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        DisposableEffect(produceUnit()) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        DisposableEffect(produceUnit()) {
+                                         ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         DisposableEffect(produceUnit()) {
++                         produceUnit()
++ DisposableEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun disposableEffect_withUnitComposableInvocation_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        DisposableEffect(AnotherComposable()) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+
+                    @Composable
+                    fun AnotherComposable() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        DisposableEffect(AnotherComposable()) {
+                                         ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         DisposableEffect(AnotherComposable()) {
++                         AnotherComposable()
++ DisposableEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun disposableEffect_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun test() = DisposableEffect(produceUnit()) {
+                        onDispose {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                    fun test() = DisposableEffect(produceUnit()) {
+                                                  ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 7: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+-                     fun test() = DisposableEffect(produceUnit()) {
++                     fun test() = kotlin.run {
++ produceUnit()
++ DisposableEffect(kotlin.Unit) {
+@@ -12 +14
++ }
+                """
+            )
+    }
+
+    @Test
+    fun disposableEffect_withIfStatementThatReturnsUnit_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        DisposableEffect(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                doSomethingElse()
+                            }
+                        ) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+
+                    fun doSomething() {}
+                    fun doSomethingElse() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                            if (condition) {
+                            ^
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 9: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         DisposableEffect(
+-                             if (condition) {
+-                                 doSomething()
+-                             } else {
+-                                 doSomethingElse()
+-                             }
++                         if (condition) {
++     doSomething()
++ }else {
++     doSomethingElse()
++ }
++ DisposableEffect(
++                             kotlin.Unit
+                """
+            )
+    }
+
+    @Test
+    fun disposableEffect_withIfStatementCoercedToAny_doesNotReportError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        DisposableEffect(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                42
+                            }
+                        ) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+
+                    fun doSomething() {}
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun disposableEffect_twoKeys_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        DisposableEffect(42, produceUnit()) {
+                            onDispose {
+                                // Do nothing.
+                            }
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+                        DisposableEffect(42, produceUnit()) {
+                                             ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `DisposableEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         DisposableEffect(42, produceUnit()) {
++                         produceUnit()
++ DisposableEffect(42, kotlin.Unit) {
+                """
+            )
+    }
+
+    // endregion DisposableEffect test cases
+
+    // region LaunchedEffect test cases
+
+    @Test
+    fun launchedEffect_withUnitLiteralKey_doesNotError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        LaunchedEffect(Unit) {
+                            // Do nothing.
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun launchedEffect_withUnitPropertyRead_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    val unitProperty = Unit
+
+                    @Composable
+                    fun Test() {
+                        LaunchedEffect(unitProperty) {
+                            // Do nothing.
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        LaunchedEffect(unitProperty) {
+                                       ~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+-                         LaunchedEffect(unitProperty) {
++                         unitProperty
++ LaunchedEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun launchedEffect_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        LaunchedEffect(produceUnit()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        LaunchedEffect(produceUnit()) {
+                                       ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         LaunchedEffect(produceUnit()) {
++                         produceUnit()
++ LaunchedEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun launchedEffect_withUnitComposableInvocation_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        LaunchedEffect(AnotherComposable()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    @Composable
+                    fun AnotherComposable() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                        LaunchedEffect(AnotherComposable()) {
+                                       ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         LaunchedEffect(AnotherComposable()) {
++                         AnotherComposable()
++ LaunchedEffect(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun launchedEffect_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun test() = LaunchedEffect(produceUnit()) {
+                        // Do nothing.
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                    fun test() = LaunchedEffect(produceUnit()) {
+                                                ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 7: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+-                     fun test() = LaunchedEffect(produceUnit()) {
++                     fun test() = kotlin.run {
++ produceUnit()
++ LaunchedEffect(kotlin.Unit) {
+@@ -10 +12
++ }
+                """
+            )
+    }
+
+    @Test
+    fun launchedEffect_withIfStatementThatReturnsUnit_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        LaunchedEffect(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                doSomethingElse()
+                            }
+                        ) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun doSomething() {}
+                    fun doSomethingElse() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to key1 [OpaqueUnitKey]
+                            if (condition) {
+                            ^
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 9: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         LaunchedEffect(
+-                             if (condition) {
+-                                 doSomething()
+-                             } else {
+-                                 doSomethingElse()
+-                             }
++                         if (condition) {
++     doSomething()
++ }else {
++     doSomethingElse()
++ }
++ LaunchedEffect(
++                             kotlin.Unit
+                """
+            )
+    }
+
+    @Test
+    fun launchedEffect_withIfStatementCoercedToAny_doesNotReportError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        LaunchedEffect(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                42
+                            }
+                        ) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun doSomething() {}
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun launchedEffect_twoKeys_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Effects,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        LaunchedEffect(42, produceUnit()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to key2 [OpaqueUnitKey]
+                        LaunchedEffect(42, produceUnit()) {
+                                           ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `LaunchedEffect`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         LaunchedEffect(42, produceUnit()) {
++                         produceUnit()
++ LaunchedEffect(42, kotlin.Unit) {
+                """
+            )
+    }
+
+    // endregion LaunchedEffect test cases
+
+    // region key() test cases
+
+    @Test
+    fun key_withUnitLiteralKey_doesNotError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        key(Unit) {
+                            // Do nothing.
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun key_withUnitPropertyRead_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    val unitProperty = Unit
+
+                    @Composable
+                    fun Test() {
+                        key(unitProperty) {
+                            // Do nothing.
+                        }
+                    }
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:10: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                        key(unitProperty) {
+                            ~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 10: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -10 +10
+-                         key(unitProperty) {
++                         unitProperty
++ key(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun key_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        key(produceUnit()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                        key(produceUnit()) {
+                            ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         key(produceUnit()) {
++                         produceUnit()
++ key(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun key_withUnitComposableInvocation_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        key(AnotherComposable()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    @Composable
+                    fun AnotherComposable() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                        key(AnotherComposable()) {
+                            ~~~~~~~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         key(AnotherComposable()) {
++                         AnotherComposable()
++ key(kotlin.Unit) {
+                """
+            )
+    }
+
+    @Test
+    fun key_withUnitComposableInvocation_reportsError_withFixInSingleExpressionFun() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun test() = key(produceUnit()) {
+                        // Do nothing.
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:7: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                    fun test() = key(produceUnit()) {
+                                     ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 7: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -7 +7
+-                     fun test() = key(produceUnit()) {
++                     fun test() = kotlin.run {
++ produceUnit()
++ key(kotlin.Unit) {
+@@ -10 +12
++ }
+                """
+            )
+    }
+
+    @Test
+    fun key_withIfStatementThatReturnsUnit_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        key(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                doSomethingElse()
+                            }
+                        ) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun doSomething() {}
+                    fun doSomethingElse() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:9: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                            if (condition) {
+                            ^
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 9: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         key(
+-                             if (condition) {
+-                                 doSomething()
+-                             } else {
+-                                 doSomethingElse()
+-                             }
++                         if (condition) {
++     doSomething()
++ }else {
++     doSomethingElse()
++ }
++ key(
++                             kotlin.Unit
+                """
+            )
+    }
+
+    @Test
+    fun key_withIfStatementCoercedToAny_doesNotReportError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test(condition: Boolean) {
+                        key(
+                            if (condition) {
+                                doSomething()
+                            } else {
+                                42
+                            }
+                        ) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun doSomething() {}
+                    """
+                )
+            )
+            .run()
+            .expectClean()
+    }
+
+    @Test
+    fun key_twoKeys_withUnitFunctionCall_reportsError() {
+        lint()
+            .files(
+                Stubs.Remember,
+                Stubs.Composable,
+                Stubs.Composables,
+                kotlin(
+                    """
+                    package test
+
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun Test() {
+                        key(42, produceUnit()) {
+                            // Do nothing.
+                        }
+                    }
+
+                    fun produceUnit() {}
+                    """
+                )
+            )
+            .run()
+            .expect(
+                """
+src/test/test.kt:8: Warning: Implicitly passing Unit as argument to keys [OpaqueUnitKey]
+                        key(42, produceUnit()) {
+                                ~~~~~~~~~~~~~
+0 errors, 1 warnings
+                """
+            )
+            .expectFixDiffs(
+                """
+Fix for src/test/test.kt line 8: Move expression outside of `key`'s arguments and pass `Unit` explicitly:
+@@ -8 +8
+-                         key(42, produceUnit()) {
++                         produceUnit()
++ key(42, kotlin.Unit) {
+                """
+            )
+    }
+
+    // endregion key() test cases
+}
+/* ktlint-enable max-line-length */
\ No newline at end of file
diff --git a/compose/runtime/runtime-saveable/build.gradle b/compose/runtime/runtime-saveable/build.gradle
index b4d8073..d23376d 100644
--- a/compose/runtime/runtime-saveable/build.gradle
+++ b/compose/runtime/runtime-saveable/build.gradle
@@ -15,9 +15,8 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,77 +24,54 @@
     id("com.android.library")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /* When updating dependencies, make sure to make the an an analogous update in the
-            corresponding block below */
-        api project(":compose:runtime:runtime")
-        api "androidx.annotation:annotation:1.1.0"
-
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.testCore)
-        testImplementation(libs.testRules)
-
-        androidTestImplementation projectOrArtifact(':compose:ui:ui')
-        androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
-        androidTestImplementation projectOrArtifact(":compose:test-utils")
-        androidTestImplementation "androidx.fragment:fragment:1.3.0"
-        androidTestImplementation projectOrArtifact(":activity:activity-compose")
-        androidTestImplementation(libs.testUiautomator)
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-
-        lintPublish(project(":compose:runtime:runtime-saveable-lint"))
-
-        samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /* When updating dependencies, make sure to make the an an analogous update in the
-            corresponding block above */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api project(":compose:runtime:runtime")
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
                 api "androidx.annotation:annotation:1.1.0"
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation project(':compose:ui:ui')
                 implementation project(":compose:ui:ui-test-junit4")
                 implementation project(":compose:test-utils")
@@ -112,10 +88,32 @@
                 implementation(libs.mockitoCore)
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
-    }
+}
+
+dependencies {
+    samples(projectOrArtifact(":compose:runtime:runtime-saveable:runtime-saveable-samples"))
+    lintPublish(project(":compose:runtime:runtime-saveable-lint"))
 }
 
 androidx {
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 0fee604..35456b2 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -765,6 +765,7 @@
   public static final class Snapshot.Companion {
     method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
     method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+    method public boolean isApplyObserverNotificationPending();
     method public void notifyObjectsInitialized();
     method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
     method public androidx.compose.runtime.snapshots.ObserverHandle registerApplyObserver(kotlin.jvm.functions.Function2<? super java.util.Set<?>,? super androidx.compose.runtime.snapshots.Snapshot,kotlin.Unit> observer);
@@ -775,6 +776,7 @@
     method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
     method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
     property public final androidx.compose.runtime.snapshots.Snapshot current;
+    property public final boolean isApplyObserverNotificationPending;
   }
 
   public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 1fa82d6..5c926bd 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -833,6 +833,7 @@
   public static final class Snapshot.Companion {
     method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
     method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+    method public boolean isApplyObserverNotificationPending();
     method public void notifyObjectsInitialized();
     method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.InternalComposeApi public int openSnapshotCount();
@@ -844,6 +845,7 @@
     method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
     method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
     property public final androidx.compose.runtime.snapshots.Snapshot current;
+    property public final boolean isApplyObserverNotificationPending;
   }
 
   public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5ce4dbc..b855aea 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -806,6 +806,7 @@
     method @kotlin.PublishedApi internal androidx.compose.runtime.snapshots.Snapshot createNonObservableSnapshot();
     method public androidx.compose.runtime.snapshots.Snapshot getCurrent();
     method public inline <T> T global(kotlin.jvm.functions.Function0<? extends T> block);
+    method public boolean isApplyObserverNotificationPending();
     method public void notifyObjectsInitialized();
     method public <T> T observe(optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? readObserver, optional kotlin.jvm.functions.Function1<java.lang.Object,kotlin.Unit>? writeObserver, kotlin.jvm.functions.Function0<? extends T> block);
     method public androidx.compose.runtime.snapshots.ObserverHandle registerApplyObserver(kotlin.jvm.functions.Function2<? super java.util.Set<?>,? super androidx.compose.runtime.snapshots.Snapshot,kotlin.Unit> observer);
@@ -818,6 +819,7 @@
     method public inline <R> R withMutableSnapshot(kotlin.jvm.functions.Function0<? extends R> block);
     method public inline <T> T withoutReadObservation(kotlin.jvm.functions.Function0<? extends T> block);
     property public final androidx.compose.runtime.snapshots.Snapshot current;
+    property public final boolean isApplyObserverNotificationPending;
   }
 
   public final class SnapshotApplyConflictException extends java.lang.Exception {
diff --git a/compose/runtime/runtime/integration-tests/build.gradle b/compose/runtime/runtime/integration-tests/build.gradle
index b9b65e5..f62e6ba 100644
--- a/compose/runtime/runtime/integration-tests/build.gradle
+++ b/compose/runtime/runtime/integration-tests/build.gradle
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
@@ -24,69 +23,63 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        androidTestImplementation(projectOrArtifact(":compose:ui:ui"))
-        androidTestImplementation(projectOrArtifact(":compose:material:material"))
-        androidTestImplementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":compose:runtime:runtime"))
-        androidTestImplementation(projectOrArtifact(":compose:test-utils"))
-        androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
-
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.kotlinTestJunit)
-        androidTestImplementation(libs.testExtJunit)
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.kotlinStdlib)
-        androidTestImplementation(libs.kotlinReflect)
-        androidTestImplementation(libs.truth)
-    }
-}
-
-android {
-    namespace "androidx.compose.runtime.integrationtests"
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 implementation(libs.kotlinCoroutinesCore)
                 implementation(projectOrArtifact(":compose:ui:ui"))
             }
-            jvmMain.dependencies {
+        }
+
+        commonTest {
+            dependencies {
+                implementation(kotlin("test-junit"))
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
                 api(libs.kotlinCoroutinesCore)
             }
-            androidMain.dependencies {
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api(libs.kotlinCoroutinesAndroid)
                 api("androidx.annotation:annotation:1.1.0")
 
                 implementation("androidx.core:core-ktx:1.1.0")
             }
-            desktopMain.dependencies {
-                api(libs.kotlinCoroutinesSwing)
-            }
+        }
 
-            commonTest.dependencies {
-                implementation(kotlin("test-junit"))
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    api(libs.kotlinCoroutinesSwing)
+                }
             }
-            androidAndroidTest.dependencies {
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(projectOrArtifact(":compose:ui:ui"))
                 implementation(projectOrArtifact(":compose:material:material"))
                 implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
@@ -98,9 +91,23 @@
                 implementation(libs.truth)
             }
         }
+
+        androidTest {
+            dependsOn(jvmTest)
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
 }
 
+android {
+    namespace "androidx.compose.runtime.integrationtests"
+}
+
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
         incremental = false
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index 06d1f47..d5d312b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.runtime.snapshots
 
+import androidx.compose.runtime.AtomicInt
 import androidx.compose.runtime.AtomicReference
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisallowComposableCalls
@@ -280,6 +281,13 @@
         val current get() = currentSnapshot()
 
         /**
+         * Returns whether any threads are currently in the process of notifying observers about
+         * changes to the global snapshot.
+         */
+        val isApplyObserverNotificationPending: Boolean
+            get() = pendingApplyObserverCount.get() > 0
+
+        /**
          * Take a snapshot of the current value of all state objects. The values are preserved until
          * [Snapshot.dispose] is called on the result.
          *
@@ -1767,20 +1775,36 @@
     return result
 }
 
+/**
+ * Counts the number of threads currently inside `advanceGlobalSnapshot`, notifying observers of
+ * changes to the global snapshot.
+ */
+private var pendingApplyObserverCount = AtomicInt(0)
+
 private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
     var previousGlobalSnapshot = snapshotInitializer as GlobalSnapshot
+
+    var modified: IdentityArraySet<StateObject>? = null // Effectively val; can be with contracts
     val result = sync {
         previousGlobalSnapshot = currentGlobalSnapshot.get()
+        modified = previousGlobalSnapshot.modified
+        if (modified != null) {
+            pendingApplyObserverCount.add(1)
+        }
         takeNewGlobalSnapshot(previousGlobalSnapshot, block)
     }
 
     // If the previous global snapshot had any modified states then notify the registered apply
     // observers.
-    val modified = previousGlobalSnapshot.modified
-    if (modified != null) {
-        val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
-        observers.fastForEach { observer ->
-            observer(modified, previousGlobalSnapshot)
+    modified?.let {
+        try {
+            val observers: List<(Set<Any>, Snapshot) -> Unit> =
+                sync { applyObservers.toMutableList() }
+            observers.fastForEach { observer ->
+                observer(it, previousGlobalSnapshot)
+            }
+        } finally {
+            pendingApplyObserverCount.add(-1)
         }
     }
 
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 8efc203..3ec3fb7 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -269,6 +269,33 @@
     }
 
     @Test
+    fun applyObserverNotificationIsPendingWhileSendingApplyNotifications() {
+        val state = mutableStateOf(0)
+
+        var notificationsPendingWhileObserving = false
+        val unregister = Snapshot.registerApplyObserver { _, _ ->
+            notificationsPendingWhileObserving = Snapshot.isApplyObserverNotificationPending
+        }
+
+        try {
+            // Normally not pending
+            assertFalse(Snapshot.isApplyObserverNotificationPending)
+
+            state.value = 1
+
+            Snapshot.sendApplyNotifications()
+
+            // Was pending while sending apply notifications
+            assertTrue(notificationsPendingWhileObserving)
+
+            // Not pending afterwards
+            assertFalse(Snapshot.isApplyObserverNotificationPending)
+        } finally {
+            unregister.dispose()
+        }
+    }
+
+    @Test
     fun aNestedSnapshotCanBeTaken() {
         val state = mutableStateOf<Int>(0)
 
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 5b5d26c..46eecc3 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
 import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,89 +23,93 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = false // b/276387374 TODO: KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-
-        api("androidx.activity:activity:1.2.0")
-        api(projectOrArtifact(":compose:ui:ui-test-junit4"))
-        api(project(":test:screenshot:screenshot"))
-
-        implementation(libs.kotlinStdlibCommon)
-        implementation(projectOrArtifact(":compose:runtime:runtime"))
-        implementation(projectOrArtifact(":compose:ui:ui-unit"))
-        implementation(projectOrArtifact(":compose:ui:ui-graphics"))
-        implementation("androidx.activity:activity-compose:1.3.1")
-        // old version of common-java8 conflicts with newer version, because both have
-        // DefaultLifecycleEventObserver.
-        // Outside of androidx this is resolved via constraint added to lifecycle-common,
-        // but it doesn't work in androidx.
-        // See aosp/1804059
-        implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
-        implementation(libs.testCore)
-        implementation(libs.testRules)
-
-        // This has stub APIs for access to legacy Android APIs, so we don't want
-        // any dependency on this module.
-        compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-
-        testImplementation(libs.truth)
-
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(projectOrArtifact(":compose:material:material"))
-    }
-}
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 implementation(projectOrArtifact(":compose:runtime:runtime"))
                 implementation(projectOrArtifact(":compose:ui:ui-unit"))
                 implementation(projectOrArtifact(":compose:ui:ui-graphics"))
                 implementation(projectOrArtifact(":compose:ui:ui-test-junit4"))
             }
+        }
+        androidMain.dependencies {
+            api("androidx.activity:activity:1.2.0")
+            implementation "androidx.activity:activity-compose:1.3.1"
+            api(projectOrArtifact(":compose:ui:ui-test-junit4"))
+            api(project(":test:screenshot:screenshot"))
+            // This has stub APIs for access to legacy Android APIs, so we don't want
+            // any dependency on this module.
+            compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
+            implementation(libs.testCore)
+            implementation(libs.testRules)
+        }
 
-            androidMain.dependencies {
-                api("androidx.activity:activity:1.2.0")
-                implementation "androidx.activity:activity-compose:1.3.1"
-                api(projectOrArtifact(":compose:ui:ui-test-junit4"))
-                api(project(":test:screenshot:screenshot"))
-                // This has stub APIs for access to legacy Android APIs, so we don't want
-                // any dependency on this module.
-                compileOnly(projectOrArtifact(":compose:ui:ui-android-stubs"))
-                implementation(libs.testCore)
-                implementation(libs.testRules)
+        commonTest {
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.truth)
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
             }
+        }
 
-            androidAndroidTest.dependencies {
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.truth)
                 implementation(projectOrArtifact(":compose:material:material"))
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui-geometry/build.gradle b/compose/ui/ui-geometry/build.gradle
index 1a05daf..15e6e16 100644
--- a/compose/ui/ui-geometry/build.gradle
+++ b/compose/ui/ui-geometry/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,53 +23,69 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.1.0")
-
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(project(":compose:ui:ui-util"))
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinTest)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
-                implementation(project(":compose:runtime:runtime"))
+                implementation("androidx.compose.runtime:runtime:1.2.1")
                 implementation(project(":compose:ui:ui-util"))
             }
-            jvmMain.dependencies {
+        }
+
+        commonTest {
+            dependencies {
+                implementation(kotlin("test-junit"))
+            }
+        }
+
+        jvmMain {
+            dependencies {
                 implementation(libs.kotlinStdlib)
             }
-            androidMain.dependencies {
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
             }
-            commonTest.dependencies {
-                implementation(kotlin("test-junit"))
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
             }
         }
     }
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 2042050..18cdc50 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -401,7 +401,7 @@
   }
 
   public fun interface ColorProducer {
-    method public long invoke();
+    method public long produce();
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class FilterQuality {
@@ -634,6 +634,7 @@
     method public void reset();
     method public default void rewind();
     method public void setFillType(int);
+    method public default void transform(float[] matrix);
     method public void translate(long offset);
     property public abstract int fillType;
     property public abstract boolean isConvex;
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index cb69cd3..75d0b80 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -401,7 +401,7 @@
   }
 
   public fun interface ColorProducer {
-    method public long invoke();
+    method public long produce();
   }
 
   @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGraphicsApi {
@@ -637,6 +637,7 @@
     method public void reset();
     method public default void rewind();
     method public void setFillType(int);
+    method public default void transform(float[] matrix);
     method public void translate(long offset);
     property public abstract int fillType;
     property public abstract boolean isConvex;
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index 032a956b..08b7e05 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -432,7 +432,7 @@
   }
 
   public fun interface ColorProducer {
-    method public long invoke();
+    method public long produce();
   }
 
   public final class DegreesKt {
@@ -669,6 +669,7 @@
     method public void reset();
     method public default void rewind();
     method public void setFillType(int);
+    method public default void transform(float[] matrix);
     method public void translate(long offset);
     property public abstract int fillType;
     property public abstract boolean isConvex;
diff --git a/compose/ui/ui-graphics/build.gradle b/compose/ui/ui-graphics/build.gradle
index 815c1df..cb9f0da 100644
--- a/compose/ui/ui-graphics/build.gradle
+++ b/compose/ui/ui-graphics/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,102 +23,72 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api("androidx.annotation:annotation:1.2.0")
-        api(project(":compose:ui:ui-unit"))
-
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(project(":compose:ui:ui-util"))
-        implementation(libs.kotlinStdlibCommon)
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.kotlinTestJunit)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(project(":compose:ui:ui-graphics:ui-graphics-samples"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-
-        lintPublish(project(":compose:ui:ui-graphics-lint"))
-
-        samples(projectOrArtifact(":compose:ui:ui-graphics:ui-graphics-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
 
                 api(project(":compose:ui:ui-unit"))
                 implementation(project(":compose:runtime:runtime"))
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.2.0")
+        commonTest {
+            dependencies {
+                implementation(kotlin("test"))
             }
+        }
 
+        if (desktopEnabled) {
             skikoMain {
                 dependsOn(commonMain)
                 dependencies {
                     api(libs.skikoCommon)
+                    implementation(project(":compose:runtime:runtime"))
                 }
             }
+        }
 
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.2.0")
+            }
+        }
+
+        if (desktopEnabled) {
             desktopMain {
-                dependsOn skikoMain
+                dependsOn(jvmMain)
+                dependsOn(skikoMain)
                 dependencies {
                     implementation(libs.kotlinStdlib)
                     implementation(libs.kotlinStdlibJdk8)
                 }
             }
+        }
 
-            commonTest {
-                dependencies {
-                    implementation(kotlin("test"))
-                }
+        jvmTest {
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
-            }
-
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:ui:ui-graphics:ui-graphics-samples"))
                 implementation(project(":compose:ui:ui-test-junit4"))
                 implementation(project(":compose:test-utils"))
@@ -128,9 +97,26 @@
                 implementation(libs.espressoCore)
                 implementation(libs.junit)
             }
+        }
 
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
             desktopTest {
                 resources.srcDirs += "src/desktopTest/res"
+                dependsOn(jvmTest)
                 dependencies {
                     implementation(project(":compose:ui:ui-test-junit4"))
                     implementation(libs.junit)
@@ -140,9 +126,10 @@
             }
         }
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:ui:ui-graphics:ui-graphics-samples"))
-    }
+}
+
+dependencies {
+    lintPublish(project(":compose:ui:ui-graphics-lint"))
 }
 
 androidx {
@@ -161,7 +148,7 @@
     namespace "androidx.compose.ui.graphics"
 }
 
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+if (desktopEnabled) {
     tasks.findByName("desktopTest").configure {
         systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
     }
diff --git a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
index 4784e83..b825b33 100644
--- a/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
+++ b/compose/ui/ui-graphics/samples/src/main/java/androidx/compose/ui/graphics/samples/BrushSamples.kt
@@ -26,8 +26,10 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.TileMode
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -57,4 +59,108 @@
         val sweep = Brush.sweepGradient(listOf(Color.Cyan, Color.Magenta))
         Box(modifier = Modifier.size(120.dp).background(sweep))
     }
+}
+
+@Sampled
+fun LinearGradientColorStopSample() {
+    Brush.linearGradient(
+        0.0f to Color.Red,
+        0.3f to Color.Green,
+        1.0f to Color.Blue,
+        start = Offset(0.0f, 50.0f),
+        end = Offset(0.0f, 100.0f)
+    )
+}
+
+@Sampled
+fun LinearGradientSample() {
+    Brush.linearGradient(
+        listOf(Color.Red, Color.Green, Color.Blue),
+        start = Offset(0.0f, 50.0f),
+        end = Offset(0.0f, 100.0f)
+    )
+}
+
+@Sampled
+fun HorizontalGradientSample() {
+    Brush.horizontalGradient(
+        listOf(Color.Red, Color.Green, Color.Blue),
+        startX = 10.0f,
+        endX = 20.0f
+    )
+}
+
+@Sampled
+fun HorizontalGradientColorStopSample() {
+    Brush.horizontalGradient(
+        0.0f to Color.Red,
+        0.3f to Color.Green,
+        1.0f to Color.Blue,
+        startX = 0.0f,
+        endX = 100.0f
+    )
+}
+
+@Sampled
+fun VerticalGradientColorStopSample() {
+    Brush.verticalGradient(
+        0.1f to Color.Red,
+        0.3f to Color.Green,
+        0.5f to Color.Blue,
+        startY = 0.0f,
+        endY = 100.0f
+    )
+}
+
+@Sampled
+fun VerticalGradientSample() {
+    Brush.verticalGradient(
+        listOf(Color.Red, Color.Green, Color.Blue),
+        startY = 0.0f,
+        endY = 100.0f
+    )
+}
+
+@Sampled
+fun RadialBrushColorStopSample() {
+    val side1 = 100
+    val side2 = 200
+    Brush.radialGradient(
+        0.0f to Color.Red,
+        0.3f to Color.Green,
+        1.0f to Color.Blue,
+        center = Offset(side1 / 2.0f, side2 / 2.0f),
+        radius = side1 / 2.0f,
+        tileMode = TileMode.Repeated
+    )
+}
+
+@Sampled
+fun RadialBrushSample() {
+    val side1 = 100
+    val side2 = 200
+    Brush.radialGradient(
+        listOf(Color.Red, Color.Green, Color.Blue),
+        center = Offset(side1 / 2.0f, side2 / 2.0f),
+        radius = side1 / 2.0f,
+        tileMode = TileMode.Repeated
+    )
+}
+
+@Sampled
+fun SweepGradientColorStopSample() {
+    Brush.sweepGradient(
+        0.0f to Color.Red,
+        0.3f to Color.Green,
+        1.0f to Color.Blue,
+        center = Offset(0.0f, 100.0f)
+    )
+}
+
+@Sampled
+fun SweepGradientSample() {
+    Brush.sweepGradient(
+        listOf(Color.Red, Color.Green, Color.Blue),
+        center = Offset(10.0f, 20.0f)
+    )
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
index 6975ef7..def70c3 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/PathTest.kt
@@ -97,6 +97,37 @@
         assertEquals(0, androidPath.resetCount)
     }
 
+    @Test
+    fun testPathTransform() {
+        val width = 100
+        val height = 100
+        val image = ImageBitmap(width, height)
+        val canvas = Canvas(image)
+
+        val path = Path().apply {
+            addRect(Rect(0f, 0f, 50f, 50f))
+            transform(
+                Matrix().apply { translate(50f, 50f) }
+            )
+        }
+
+        val paint = Paint().apply { color = Color.Black }
+        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+        paint.color = Color.Red
+        canvas.drawPath(path, paint)
+
+        image.toPixelMap().apply {
+            assertEquals(Color.Black, this[width / 2 - 3, height / 2 - 3])
+            assertEquals(Color.Black, this[width / 2, height / 2 - 3])
+            assertEquals(Color.Black, this[width / 2 - 3, height / 2])
+
+            assertEquals(Color.Red, this[width / 2 + 2, height / 2 + 2])
+            assertEquals(Color.Red, this[width - 2, height / 2 + 2])
+            assertEquals(Color.Red, this[width - 2, height - 2])
+            assertEquals(Color.Red, this[width / 2 + 2, height - 2])
+        }
+    }
+
     class TestAndroidPath : android.graphics.Path() {
 
         var resetCount = 0
diff --git a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
index 786ed1f..a1c9071 100644
--- a/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
+++ b/compose/ui/ui-graphics/src/androidAndroidTest/kotlin/androidx/compose/ui/graphics/ShaderTest.kt
@@ -28,6 +28,8 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.math.roundToInt
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -197,6 +199,53 @@
         )
     }
 
+    @Test
+    fun testInvalidWidthBrush() {
+        // Verify that attempts to create a RadialGradient with a width of 0 do not throw
+        // IllegalArgumentExceptions for an invalid radius
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        brush.applyTo(Size(0f, 10f), paint, 1.0f)
+    }
+
+    @Test
+    fun testInvalidHeightBrush() {
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        // Verify that attempts to create a RadialGradient with a height of 0 do not throw
+        // IllegalArgumentExceptions for an invalid radius
+        brush.applyTo(Size(10f, 0f), paint, 1.0f)
+    }
+
+    @Test
+    fun testValidToInvalidWidthBrush() {
+        // Verify that attempts to create a RadialGradient with a non-zero width/height that
+        // is later attempted to be recreated with a zero width remove the shader from the Paint
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+        brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+        assertNotNull(paint.shader)
+
+        brush.applyTo(Size(0f, 10f), paint, 1.0f)
+        assertNull(paint.shader)
+    }
+
+    @Test
+    fun testValidToInvalidHeightBrush() {
+        // Verify that attempts to create a RadialGradient with a non-zero width/height that
+        // is later attempted to be recreated with a zero height remove the shader from the Paint
+        val brush = Brush.radialGradient(listOf(Color.Red, Color.Blue))
+        val paint = Paint()
+
+        brush.applyTo(Size(10f, 10f), paint, 1.0f)
+
+        assertNotNull(paint.shader)
+
+        brush.applyTo(Size(10f, 0f), paint, 1.0f)
+        assertNull(paint.shader)
+    }
+
     private fun ImageBitmap.drawInto(
         block: DrawScope.() -> Unit
     ) = CanvasDrawScope().draw(
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
index 1293ae9..8f2debb 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidCanvas.android.kt
@@ -335,7 +335,9 @@
      */
     private fun drawLines(points: List<Offset>, paint: Paint, stepBy: Int) {
         if (points.size >= 2) {
-            for (i in 0 until points.size - 1 step stepBy) {
+            val frameworkPaint = paint.asFrameworkPaint()
+            var i = 0
+            while (i < points.size - 1) {
                 val p1 = points[i]
                 val p2 = points[i + 1]
                 internalCanvas.drawLine(
@@ -343,8 +345,9 @@
                     p1.y,
                     p2.x,
                     p2.y,
-                    paint.asFrameworkPaint()
+                    frameworkPaint
                 )
+                i += stepBy
             }
         }
     }
@@ -365,10 +368,13 @@
 
     private fun drawRawPoints(points: FloatArray, paint: Paint, stepBy: Int) {
         if (points.size % 2 == 0) {
-            for (i in 0 until points.size - 1 step stepBy) {
+            val frameworkPaint = paint.asFrameworkPaint()
+            var i = 0
+            while (i < points.size - 1) {
                 val x = points[i]
                 val y = points[i + 1]
-                internalCanvas.drawPoint(x, y, paint.asFrameworkPaint())
+                internalCanvas.drawPoint(x, y, frameworkPaint)
+                i += stepBy
             }
         }
     }
@@ -390,18 +396,15 @@
         // Float array is treated as alternative set of x and y coordinates
         // x1, y1, x2, y2, x3, y3, ... etc.
         if (points.size >= 4 && points.size % 2 == 0) {
-            for (i in 0 until points.size - 3 step stepBy * 2) {
+            val frameworkPaint = paint.asFrameworkPaint()
+            var i = 0
+            while (i < points.size - 3) {
                 val x1 = points[i]
                 val y1 = points[i + 1]
                 val x2 = points[i + 2]
                 val y2 = points[i + 3]
-                internalCanvas.drawLine(
-                    x1,
-                    y1,
-                    x2,
-                    y2,
-                    paint.asFrameworkPaint()
-                )
+                internalCanvas.drawLine(x1, y1, x2, y2, frameworkPaint)
+                i += stepBy * 2
             }
         }
     }
diff --git a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
index 18705bc..f4272bb 100644
--- a/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
+++ b/compose/ui/ui-graphics/src/androidMain/kotlin/androidx/compose/ui/graphics/AndroidPath.android.kt
@@ -190,6 +190,11 @@
         internalPath.transform(mMatrix)
     }
 
+    override fun transform(matrix: Matrix) {
+        mMatrix.setFrom(matrix)
+        internalPath.transform(mMatrix)
+    }
+
     override fun getBounds(): Rect {
         internalPath.computeBounds(rectF, true)
         return Rect(
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
index bda9615..5f7a1d0 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Brush.kt
@@ -57,7 +57,8 @@
          * )
          * ```
          *
-         * @see androidx.compose.ui.graphics.samples.GradientBrushSample
+         * @sample androidx.compose.ui.graphics.samples.LinearGradientColorStopSample
+         * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colorStops Colors and their offset in the gradient area
          * @param start Starting position of the linear gradient. This can be set to
@@ -88,11 +89,12 @@
          * ```
          *  Brush.linearGradient(
          *      listOf(Color.Red, Color.Green, Color.Blue),
-         *      start = Offset(0.0f, 50.0f)
+         *      start = Offset(0.0f, 50.0f),
          *      end = Offset(0.0f, 100.0f)
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.LinearGradientSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colors Colors to be rendered as part of the gradient
@@ -129,6 +131,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.HorizontalGradientSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colors colors Colors to be rendered as part of the gradient
@@ -163,6 +166,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.HorizontalGradientColorStopSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -197,9 +201,9 @@
          *      startY = 0.0f,
          *      endY = 100.0f
          * )
-         *
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.VerticalGradientSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colors colors Colors to be rendered as part of the gradient
@@ -234,6 +238,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.VerticalGradientColorStopSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -273,6 +278,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.RadialBrushColorStopSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -305,13 +311,13 @@
          * ```
          * Brush.radialGradient(
          *      listOf(Color.Red, Color.Green, Color.Blue),
-         *      centerX = side1 / 2.0f,
-         *      centerY = side2 / 2.0f,
+         *      center = Offset(side1 / 2.0f, side2 / 2.0f),
          *      radius = side1 / 2.0f,
          *      tileMode = TileMode.Repeated
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.RadialBrushSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colors Colors to be rendered as part of the gradient
@@ -353,6 +359,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.SweepGradientColorStopSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colorStops Colors and offsets to determine how the colors are dispersed throughout
@@ -384,6 +391,7 @@
          * )
          * ```
          *
+         * @sample androidx.compose.ui.graphics.samples.SweepGradientSample
          * @sample androidx.compose.ui.graphics.samples.GradientBrushSample
          *
          * @param colors List of colors to fill the sweep gradient
@@ -645,8 +653,14 @@
     final override fun applyTo(size: Size, p: Paint, alpha: Float) {
         var shader = internalShader
         if (shader == null || createdSize != size) {
-            shader = createShader(size).also { internalShader = it }
-            createdSize = size
+            if (size.isEmpty()) {
+                shader = null
+                internalShader = null
+                createdSize = Size.Unspecified
+            } else {
+                shader = createShader(size).also { internalShader = it }
+                createdSize = size
+            }
         }
         if (p.color != Color.Black) p.color = Color.Black
         if (p.shader != shader) p.shader = shader
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 ca53498..a17406a 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
@@ -668,5 +668,5 @@
     /**
      * Return the color
      */
-    fun invoke(): Color
+    fun produce(): Color
 }
\ No newline at end of file
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 b12273e..e54eef0 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
@@ -233,6 +233,13 @@
     fun translate(offset: Offset)
 
     /**
+     * Transform the points in this path by the provided matrix
+     */
+    fun transform(matrix: Matrix) {
+        // NO-OP to ensure runtime + compile time compatibility
+    }
+
+    /**
      * Compute the bounds of the control points of the path, and write the
      * answer into bounds. If the path contains 0 or 1 points, the bounds is
      * set to (0,0,0,0)
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
index 42cf68f..b4e0ed9 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/PathParser.kt
@@ -139,7 +139,7 @@
         return this
     }
 
-    fun toNodes(): List<PathNode> = nodes
+    fun toNodes() = nodes
 
     fun toPath(target: Path = Path()) = nodes.toPath(target)
 
@@ -156,7 +156,7 @@
  * created [Path].
  */
 fun List<PathNode>.toPath(target: Path = Path()): Path {
-    // Rewind unsets the filltype so reset it here
+    // Rewind unsets the fill type so reset it here
     val fillType = target.fillType
     target.rewind()
     target.fillType = fillType
diff --git a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
index 92b808d..31ff184 100644
--- a/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
+++ b/compose/ui/ui-graphics/src/commonTest/kotlin/androidx/compose/ui/graphics/vector/PathParserTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.RoundRect
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.PathFillType
 import androidx.compose.ui.graphics.PathOperation
@@ -187,6 +188,10 @@
             // NO-OP
         }
 
+        override fun transform(matrix: Matrix) {
+            // NO-OP
+        }
+
         override fun getBounds(): Rect = Rect.Zero
 
         override fun op(path1: Path, path2: Path, operation: PathOperation): Boolean = false
diff --git a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
index 332b62b..4dc92b9 100644
--- a/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
+++ b/compose/ui/ui-graphics/src/desktopTest/kotlin/androidx/compose/ui/graphics/DesktopPathTest.kt
@@ -225,4 +225,35 @@
 
         assertTrue(path.isEmpty)
     }
+
+    @Test
+    fun testTransform() {
+        val width = 100
+        val height = 100
+        val image = ImageBitmap(width, height)
+        val canvas = Canvas(image)
+
+        val path = Path().apply {
+            addRect(Rect(0f, 0f, 50f, 50f))
+            transform(
+                Matrix().apply { translate(50f, 50f) }
+            )
+        }
+
+        val paint = Paint().apply { color = Color.Black }
+        canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint)
+        paint.color = Color.Red
+        canvas.drawPath(path, paint)
+
+        image.toPixelMap().apply {
+            assertEquals(Color.Black, this[width / 2 - 3, height / 2 - 3])
+            assertEquals(Color.Black, this[width / 2, height / 2 - 3])
+            assertEquals(Color.Black, this[width / 2 - 3, height / 2])
+
+            assertEquals(Color.Red, this[width / 2 + 2, height / 2 + 2])
+            assertEquals(Color.Red, this[width - 2, height / 2 + 2])
+            assertEquals(Color.Red, this[width - 2, height - 2])
+            assertEquals(Color.Red, this[width / 2 + 2, height - 2])
+        }
+    }
 }
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
index 403e36e..0285785 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPath.skiko.kt
@@ -172,6 +172,10 @@
         internalPath.transform(Matrix33.makeTranslate(offset.x, offset.y))
     }
 
+    override fun transform(matrix: Matrix) {
+        internalPath.transform(Matrix33.makeTranslate(0f, 0f).apply { setFrom(matrix) })
+    }
+
     override fun getBounds(): Rect {
         val bounds = internalPath.bounds
         return Rect(
@@ -209,4 +213,63 @@
     override val isConvex: Boolean get() = internalPath.isConvex
 
     override val isEmpty: Boolean get() = internalPath.isEmpty
+
+    fun Matrix33.setFrom(matrix: Matrix) {
+        require(
+            matrix[0, 2] == 0f &&
+                matrix[1, 2] == 0f &&
+                matrix[2, 2] == 1f &&
+                matrix[3, 2] == 0f &&
+                matrix[2, 0] == 0f &&
+                matrix[2, 1] == 0f &&
+                matrix[2, 3] == 0f
+        ) {
+            "Matrix33 does not support arbitrary transforms"
+        }
+
+        // We'll reuse the array used in Matrix to avoid allocation by temporarily
+        // setting it to the 3x3 matrix used by android.graphics.Matrix
+        // Store the values of the 4 x 4 matrix into temporary variables
+        // to be reset after the 3 x 3 matrix is configured
+        val scaleX = matrix.values[Matrix.ScaleX] // 0
+        val skewY = matrix.values[Matrix.SkewY] // 1
+        val v2 = matrix.values[2] // 2
+        val persp0 = matrix.values[Matrix.Perspective0] // 3
+        val skewX = matrix.values[Matrix.SkewX] // 4
+        val scaleY = matrix.values[Matrix.ScaleY] // 5
+        val v6 = matrix.values[6] // 6
+        val persp1 = matrix.values[Matrix.Perspective1] // 7
+        val v8 = matrix.values[8] // 8
+
+        val translateX = matrix.values[Matrix.TranslateX]
+        val translateY = matrix.values[Matrix.TranslateY]
+        val persp2 = matrix.values[Matrix.Perspective2]
+
+        val v = matrix.values
+
+        v[0] = scaleX // MSCALE_X = 0
+        v[1] = skewX // MSKEW_X = 1
+        v[2] = translateX // MTRANS_X = 2
+        v[3] = skewY // MSKEW_Y = 3
+        v[4] = scaleY // MSCALE_Y = 4
+        v[5] = translateY // MTRANS_Y
+        v[6] = persp0 // MPERSP_0 = 6
+        v[7] = persp1 // MPERSP_1 = 7
+        v[8] = persp2 // MPERSP_2 = 8
+
+        for (i in 0..8) {
+            mat[i] = v[i]
+        }
+
+        // Reset the values back after the android.graphics.Matrix is configured
+        v[Matrix.ScaleX] = scaleX // 0
+        v[Matrix.SkewY] = skewY // 1
+        v[2] = v2 // 2
+        v[Matrix.Perspective0] = persp0 // 3
+        v[Matrix.SkewX] = skewX // 4
+        v[Matrix.ScaleY] = scaleY // 5
+        v[6] = v6 // 6
+        v[Matrix.Perspective1] = persp1 // 7
+        v[8] = v8 // 8
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 5e4501d..5db2d70 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,81 +23,42 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-android {
-    lintOptions {
-        disable("InvalidPackage")
-    }
-    namespace "androidx.compose.ui.test.junit4"
-}
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-dependencies {
-
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        api(project(":compose:ui:ui-test"))
-        api("androidx.activity:activity:1.2.1")
-        api(libs.junit)
-        api(libs.kotlinStdlib)
-        api(libs.kotlinStdlibCommon)
-        api(libs.testExtJunit)
-
-        implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
-        implementation("androidx.activity:activity-compose:1.3.0")
-        implementation("androidx.annotation:annotation:1.1.0")
-        implementation("androidx.lifecycle:lifecycle-common:2.5.1")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
-        implementation("androidx.test:core:1.5.0")
-        implementation("androidx.test:monitor:1.6.0")
-        implementation("androidx.test.espresso:espresso-core:3.5.0")
-        implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
-        implementation(libs.kotlinCoroutinesCore)
-        implementation(libs.kotlinCoroutinesTest)
-
-        testImplementation(project(":compose:animation:animation-core"))
-        testImplementation(project(":compose:material:material"))
-        testImplementation(project(":compose:test-utils"))
-        testImplementation(libs.truth)
-        testImplementation(libs.robolectric)
-
-        androidTestImplementation(project(":compose:animation:animation"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation("androidx.fragment:fragment-testing:1.4.1")
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoKotlin)
-    }
-}
-
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 api(project(":compose:ui:ui-test"))
                 implementation(libs.kotlinStdlib)
                 implementation(libs.kotlinCoroutinesCore)
                 implementation(libs.kotlinCoroutinesTest)
             }
+        }
 
-            jvmMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
                 api(libs.junit)
                 api(libs.kotlinStdlib)
                 api(libs.kotlinStdlibCommon)
 
                 compileOnly("androidx.annotation:annotation:1.1.0")
             }
+        }
 
-            androidMain.dependencies {
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.activity:activity:1.2.1")
                 implementation "androidx.activity:activity-compose:1.3.0"
                 api(libs.testExtJunit)
@@ -107,24 +67,31 @@
                 implementation(project(":compose:runtime:runtime-saveable"))
                 implementation("androidx.lifecycle:lifecycle-common:2.5.1")
                 implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
-                implementation("androidx.test:core:1.4.0")
+                implementation("androidx.test:core:1.5.0")
                 implementation(libs.testMonitor)
                 implementation("androidx.test.espresso:espresso-core:3.3.0")
-                implementation("androidx.test.espresso:espresso-idling-resource:3.3.0")
+                implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(project(":compose:animation:animation-core"))
-                implementation(project(":compose:material:material"))
-                implementation(project(":compose:test-utils"))
-                implementation(libs.truth)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.truth)
+                    implementation(libs.skiko)
+                }
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:animation:animation"))
                 implementation(project(":compose:test-utils"))
                 implementation(project(":compose:material:material"))
@@ -136,28 +103,50 @@
                 implementation(libs.dexmakerMockito)
                 implementation(libs.mockitoKotlin)
             }
+        }
 
-            desktopMain.dependencies {
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(project(":compose:animation:animation-core"))
+                implementation(project(":compose:material:material"))
+                implementation(project(":compose:test-utils"))
                 implementation(libs.truth)
-                implementation(libs.skiko)
             }
+        }
 
-            desktopTest.dependencies {
-                implementation(libs.truth)
-                implementation(libs.junit)
-                implementation(libs.kotlinTest)
-                implementation(libs.skikoCurrentOs)
-                implementation(project(":compose:foundation:foundation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependencies {
+                    implementation(libs.truth)
+                    implementation(libs.junit)
+                    implementation(libs.kotlinTest)
+                    implementation(libs.skikoCurrentOs)
+                    implementation(project(":compose:foundation:foundation"))
+                    implementation(project(":compose:ui:ui-test-junit4"))
+                }
             }
         }
     }
+}
 
-    dependencies {
-        // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
-        // leaks into instrumented tests (b/214407011)
-        testImplementation(libs.robolectric)
+
+android {
+    lintOptions {
+        disable("InvalidPackage")
     }
+    namespace "androidx.compose.ui.test.junit4"
+}
+
+dependencies {
+    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // leaks into instrumented tests (b/214407011)
+    testImplementation(libs.robolectric)
 }
 
 androidx {
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
index 8af350a..6977ddf 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeUiTest.desktop.kt
@@ -182,6 +182,10 @@
         }
     }
 
+    /**
+     * @param matcher
+     * @param useUnmergedTree
+     */
     override fun onNode(
         matcher: SemanticsMatcher,
         useUnmergedTree: Boolean
@@ -189,6 +193,10 @@
         return SemanticsNodeInteraction(testContext, useUnmergedTree, matcher)
     }
 
+    /**
+     * @param matcher
+     * @param useUnmergedTree
+     */
     override fun onAllNodes(
         matcher: SemanticsMatcher,
         useUnmergedTree: Boolean
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 097486a..ead9fac 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,14 +23,115 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
+
+    sourceSets {
+        commonMain {
+            dependencies {
+                api(project(":compose:ui:ui"))
+                api(project(":compose:ui:ui-text"))
+                api(project(":compose:ui:ui-unit"))
+                api(project(":compose:runtime:runtime"))
+                api(libs.kotlinStdlib)
+
+                implementation(project(":compose:ui:ui-util"))
+            }
+        }
+
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+                api(libs.kotlinCoroutinesCore)
+                api(libs.kotlinCoroutinesTest)
+                api(libs.kotlinStdlibCommon)
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api(project(":compose:ui:ui-graphics"))
+
+                implementation("androidx.annotation:annotation:1.1.0")
+                implementation("androidx.core:core-ktx:1.2.0")
+                implementation("androidx.test.espresso:espresso-core:3.5.0")
+                implementation(libs.testMonitor)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.junit)
+                    implementation(libs.truth)
+                    implementation(libs.skiko)
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidCommonTest {
+            dependsOn(commonTest)
+            dependencies {
+                implementation(project(":compose:test-utils"))
+                implementation(libs.truth)
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependsOn(androidCommonTest)
+            dependencies {
+                implementation(project(":compose:material:material"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation("androidx.activity:activity-compose:1.3.1")
+                implementation(libs.mockitoCore)
+                implementation(libs.mockitoKotlin)
+                implementation(libs.dexmakerMockito)
+                implementation(libs.kotlinTest)
+            }
+        }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependsOn(androidCommonTest)
+            dependencies {
+                implementation(libs.mockitoCore4)
+                implementation(libs.mockitoKotlin4)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
+    }
+}
 
 android {
-    if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        sourceSets {
-            test.java.srcDirs += "src/androidCommonTest/kotlin"
-            androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
-        }
+    sourceSets {
+        test.java.srcDirs += "src/androidCommonTest/kotlin"
+        androidTest.java.srcDirs += "src/androidCommonTest/kotlin"
     }
 
     lintOptions {
@@ -41,121 +141,9 @@
 }
 
 dependencies {
-
-    if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api(project(":compose:ui:ui"))
-        api(project(":compose:ui:ui-graphics"))
-        api(project(":compose:ui:ui-text"))
-        api(project(":compose:ui:ui-unit"))
-        api(libs.kotlinCoroutinesCore)
-        api(libs.kotlinCoroutinesTest)
-        api(libs.kotlinStdlib)
-        api(libs.kotlinStdlibCommon)
-
-        implementation(project(":compose:ui:ui-util"))
-        implementation("androidx.annotation:annotation:1.1.0")
-        implementation("androidx.core:core-ktx:1.1.0")
-        implementation("androidx.test.espresso:espresso-core:3.5.0")
-        implementation("androidx.test:monitor:1.6.0")
-
-        testImplementation(project(":compose:test-utils"))
-        testImplementation(libs.truth)
-        testImplementation(libs.robolectric)
-        testImplementation(libs.mockitoCore4)
-        testImplementation(libs.mockitoKotlin4)
-
-        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoKotlin)
-        androidTestImplementation(libs.kotlinTest)
-
-        samples(project(":compose:ui:ui-test:ui-test-samples"))
-    }
-}
-
-
-if (AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        sourceSets {
-            commonMain.dependencies {
-                api(project(":compose:ui:ui"))
-                api(project(":compose:ui:ui-text"))
-                api(project(":compose:ui:ui-unit"))
-                api(libs.kotlinStdlib)
-
-                implementation(project(":compose:ui:ui-util"))
-            }
-
-            jvmMain.dependencies {
-                api(project(":compose:runtime:runtime"))
-                api(libs.kotlinCoroutinesCore)
-                api(libs.kotlinCoroutinesTest)
-                api(libs.kotlinStdlibCommon)
-            }
-
-            androidMain.dependencies {
-                api(project(":compose:ui:ui-graphics"))
-
-                implementation("androidx.annotation:annotation:1.1.0")
-                implementation("androidx.core:core-ktx:1.2.0")
-                implementation("androidx.test.espresso:espresso-core:3.3.0")
-                implementation(libs.testMonitor)
-            }
-
-            androidCommonTest.dependencies {
-                implementation(project(":compose:test-utils"))
-                implementation(libs.truth)
-            }
-
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.mockitoCore4)
-                implementation(libs.mockitoKotlin4)
-            }
-
-            androidAndroidTest.dependencies {
-                implementation(project(":compose:material:material"))
-                implementation(project(":compose:ui:ui-test-junit4"))
-                implementation("androidx.activity:activity-compose:1.3.1")
-                implementation(libs.mockitoCore)
-                implementation(libs.mockitoKotlin)
-                implementation(libs.dexmakerMockito)
-                implementation(libs.kotlinTest)
-            }
-
-            desktopMain.dependencies {
-                implementation(libs.junit)
-                implementation(libs.truth)
-                implementation(libs.skiko)
-            }
-
-            androidCommonTest.dependsOn(commonTest)
-            androidTest.dependsOn(androidCommonTest)
-            androidAndroidTest.dependsOn(androidCommonTest)
-        }
-    }
-
-    dependencies {
-        // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
-        // leaks into instrumented tests (b/214407011)
-        testImplementation(libs.robolectric)
-
-        samples(project(":compose:ui:ui-test:ui-test-samples"))
-    }
+    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // leaks into instrumented tests (b/214407011)
+    testImplementation(libs.robolectric)
 }
 
 androidx {
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
index f818fcc..5d24c91 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/PrintToStringTest.kt
@@ -129,10 +129,10 @@
                     |-Node #X at (l=X, t=X, r=X, b=X)px, Tag: 'box'
                     | [Disabled]
                     |  |-Node #X at (l=X, t=X, r=X, b=X)px
-                    |    Role = 'Button'
                     |    Focused = 'false'
+                    |    Role = 'Button'
                     |    Text = '[Button]'
-                    |    Actions = [RequestFocus, OnClick, GetTextLayoutResult]
+                    |    Actions = [OnClick, RequestFocus, GetTextLayoutResult]
                     |    MergeDescendants = 'true'
                     |-Node #X at (l=X, t=X, r=X, b=X)px
                       Text = '[Hello]'
diff --git a/compose/ui/ui-text/build.gradle b/compose/ui/ui-text/build.gradle
index c20c781..2903ae2 100644
--- a/compose/ui/ui-text/build.gradle
+++ b/compose/ui/ui-text/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,77 +23,15 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        implementation(libs.kotlinStdlibCommon)
-        implementation(libs.kotlinCoroutinesCore)
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api(project(":compose:ui:ui-graphics"))
-        api(project(":compose:ui:ui-unit"))
-        api("androidx.annotation:annotation:1.1.0")
-
-        // when updating the runtime version please also update the runtime-saveable version
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
-
-        implementation(project(":compose:ui:ui-util"))
-        implementation(libs.kotlinStdlib)
-        implementation("androidx.core:core:1.7.0")
-        implementation('androidx.collection:collection:1.0.0')
-        implementation("androidx.emoji2:emoji2:1.2.0")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.mockitoCore4)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinReflect)
-        testImplementation(libs.kotlinTest)
-        testImplementation(libs.mockitoKotlin4)
-
-        androidTestImplementation(project(":internal-testutils-fonts"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.mockitoKotlin)
-
-        samples(projectOrArtifact(":compose:ui:ui-text:ui-text-samples"))
-    }
-
-    android {
-        sourceSets {
-            main {
-                java.srcDirs += "${supportRootFolder}/text/text/src/main/java"
-            }
-        }
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 implementation(libs.kotlinCoroutinesCore)
 
@@ -107,53 +44,58 @@
 
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        commonTest {
+            dependencies {
             }
+        }
 
+        if (desktopEnabled) {
             skikoMain {
                 dependsOn(commonMain)
                 dependencies {
                     api(libs.skikoCommon)
+                    implementation(project(":compose:runtime:runtime"))
+                    implementation(project(":compose:runtime:runtime-saveable"))
                 }
             }
+        }
 
-            desktopMain {
-                dependsOn(skikoMain)
-                dependsOn(jvmMain)
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+                implementation(libs.kotlinStdlib)
             }
+        }
 
-            androidMain {
-                dependsOn(commonMain)
-            }
 
-            androidMain.dependencies {
+        androidMain {
+            dependsOn(commonMain)
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation("androidx.core:core:1.7.0")
                 implementation("androidx.emoji2:emoji2:1.2.0")
                 implementation('androidx.collection:collection:1.0.0')
             }
+        }
 
-            androidMain.kotlin.srcDirs("${supportRootFolder}/text/text/src/main/java")
-
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(project(":internal-testutils-fonts"))
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.mockitoCore4)
-                implementation(libs.truth)
-                implementation(libs.kotlinReflect)
-                implementation(libs.kotlinTest)
-                implementation(libs.mockitoKotlin4)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
                 implementation(project(":internal-testutils-fonts"))
                 implementation(libs.testRules)
@@ -165,20 +107,50 @@
                 implementation(libs.truth)
                 implementation(libs.mockitoKotlin)
             }
+        }
 
-            desktopTest.dependencies {
-                implementation(libs.truth)
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(project(":internal-testutils-fonts"))
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
                 implementation(libs.junit)
+                implementation(libs.truth)
+                implementation(libs.kotlinReflect)
                 implementation(libs.kotlinTest)
-                implementation(libs.skikoCurrentOs)
-                implementation(project(":compose:foundation:foundation"))
-                implementation(project(":compose:ui:ui-test-junit4"))
             }
         }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependencies {
+                    implementation(libs.truth)
+                    implementation(libs.junit)
+                    implementation(libs.kotlinTest)
+                    implementation(libs.skikoCurrentOs)
+                    implementation(project(":compose:foundation:foundation"))
+                    implementation(project(":compose:ui:ui-test-junit4"))
+                    implementation(project(":internal-testutils-fonts"))
+                }
+            }
+        }
+
+        androidMain.kotlin.srcDirs("${supportRootFolder}/text/text/src/main/java")
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:ui:ui-text:ui-text-samples"))
-    }
+}
+
+dependencies {
+    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // leaks into instrumented tests (b/214407011)
+    testImplementation(libs.mockitoCore4)
+    testImplementation(libs.mockitoKotlin4)
 }
 
 androidx {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
index 386b478..1aef72f 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/AndroidParagraphTest.kt
@@ -1348,7 +1348,7 @@
         val paragraph = simpleParagraph(
             text = "",
             style = TextStyle(brush = brush),
-            width = 0.0f
+            width = 1.0f
         )
 
         assertThat(paragraph.textPaint.shader).isNotNull()
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index 13f1be5..b69203b 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -15,9 +15,8 @@
  */
 
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,79 +24,70 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-
-        implementation(libs.kotlinStdlib)
-
-        api "androidx.annotation:annotation:1.1.0"
-
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api(project(":compose:ui:ui"))
-
-        androidTestImplementation project(":compose:ui:ui-test-junit4")
-
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testRules)
-
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(project(":compose:foundation:foundation-layout"))
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
-
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlib)
 
-                api "androidx.annotation:annotation:1.1.0"
-
-                api("androidx.compose.runtime:runtime:1.2.1")
+                api(project(":compose:runtime:runtime"))
                 api(project(":compose:ui:ui"))
             }
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
-            }
+        }
 
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(kotlin("test-junit"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.truth)
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+                implementation(libs.kotlinStdlib)
             }
-            androidAndroidTest.dependencies {
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
 
                 implementation(libs.junit)
@@ -112,9 +102,23 @@
                 implementation("androidx.activity:activity-compose:1.3.1")
             }
         }
-    }
-    dependencies {
-        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+
+                }
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui-tooling-preview/build.gradle b/compose/ui/ui-tooling-preview/build.gradle
index a9f9236..d6d3a42 100644
--- a/compose/ui/ui-tooling-preview/build.gradle
+++ b/compose/ui/ui-tooling-preview/build.gradle
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -25,47 +23,91 @@
     id("com.android.library")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        implementation(libs.kotlinStdlib)
-        api("androidx.annotation:annotation:1.2.0")
-        api("androidx.compose.runtime:runtime:1.2.1")
-        testImplementation(libs.junit)
-    }
-}
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
 
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:runtime:runtime"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.2.0")
             }
+        }
 
-            androidTest.dependencies {
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+
+                }
+            }
+        }
+
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.junit)
             }
         }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+
+                }
+            }
+        }
     }
 }
 
-
 androidx {
     name = "Compose Tooling API"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 507d5fcf..ea90aee 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,69 +23,51 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        implementation(libs.kotlinStdlib)
-
-        api("androidx.annotation:annotation:1.1.0")
-        implementation(project(":compose:animation:animation"))
-
-        api("androidx.compose.runtime:runtime:1.2.1")
-        api(project(":compose:ui:ui"))
-        api(project(":compose:ui:ui-tooling-preview"))
-        api(project(":compose:ui:ui-tooling-data"))
-        implementation("androidx.savedstate:savedstate-ktx:1.2.1")
-        implementation("androidx.compose.material:material:1.0.0")
-        implementation("androidx.activity:activity-compose:1.7.0")
-        implementation("androidx.lifecycle:lifecycle-common:2.6.1")
-
-        // kotlin-reflect and animation-tooling-internal are provided by Studio at runtime
-        compileOnly(project(":compose:animation:animation-tooling-internal"))
-        compileOnly(libs.kotlinReflect)
-
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(project(":compose:foundation:foundation-layout"))
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.kotlinReflect)
-        androidTestImplementation(project(":compose:animation:animation-tooling-internal"))
-        androidTestImplementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
-        androidTestImplementation(project(":compose:runtime:runtime-livedata"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:ui:ui-tooling-preview"))
                 api(project(":compose:runtime:runtime"))
                 api(project(":compose:ui:ui"))
                 api(project(":compose:ui:ui-tooling-data"))
             }
-            androidMain.dependencies {
+        }
+
+        commonTest {
+            dependencies {
+
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation(project(":compose:animation:animation"))
                 implementation("androidx.savedstate:savedstate-ktx:1.2.1")
-                implementation(project(":compose:material:material"))
+                implementation("androidx.compose.material:material:1.0.0")
                 implementation("androidx.activity:activity-compose:1.7.0")
                 implementation("androidx.lifecycle:lifecycle-common:2.6.1")
 
@@ -94,14 +75,29 @@
                 compileOnly(project(":compose:animation:animation-tooling-internal"))
                 compileOnly(libs.kotlinReflect)
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
-                implementation(project(":compose:runtime:runtime"))
-                implementation(project(":compose:ui:ui"))
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(skikoMain)
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+                    implementation(project(":compose:runtime:runtime"))
+                    implementation(project(":compose:ui:ui"))
+                }
             }
+        }
 
-            androidAndroidTest.dependencies {
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
 
                 implementation(libs.junit)
@@ -117,10 +113,25 @@
                 implementation(project(":compose:runtime:runtime-livedata"))
             }
         }
+
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+
+                }
+            }
+        }
     }
 }
 
-
 androidx {
     name = "Compose Tooling"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
index ae1f769..0ecdc83 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/Utils.kt
@@ -117,8 +117,10 @@
         )
         values[endTimeMs] = this.getValueFromNanos(millisToNanos(endTimeMs))
 
-        for (millis in startTimeMs..endTimeMs step stepMs) {
+        var millis = startTimeMs
+        while (millis <= endTimeMs) {
             values[millis] = this.getValueFromNanos(millisToNanos(millis))
+            millis += stepMs
         }
         values
     }
@@ -145,8 +147,10 @@
         )
         values[endTimeMs] = this.animation.getValueFromNanos(millisToNanos(endTimeMs))
 
-        for (millis in startTimeMs..endTimeMs step stepMs) {
+        var millis = startTimeMs
+        while (millis <= endTimeMs) {
             values[millis] = this.animation.getValueFromNanos(millisToNanos(millis))
+            millis += stepMs
         }
         values
     }
diff --git a/compose/ui/ui-unit/build.gradle b/compose/ui/ui-unit/build.gradle
index bfbfed7..627cd87 100644
--- a/compose/ui/ui-unit/build.gradle
+++ b/compose/ui/ui-unit/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,85 +23,88 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        api(project(":compose:ui:ui-geometry"))
-        api("androidx.annotation:annotation:1.1.0")
-
-        implementation(libs.kotlinStdlib)
-        implementation("androidx.compose.runtime:runtime:1.2.1")
-        implementation(project(":compose:ui:ui-util"))
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testExtJunit)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.kotlinTest)
-
-        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 api(project(":compose:ui:ui-geometry"))
 
                 implementation(project(":compose:runtime:runtime"))
                 implementation(project(":compose:ui:ui-util"))
             }
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-            androidMain.dependencies {
-                api("androidx.annotation:annotation:1.1.0")
-            }
+        }
 
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(kotlin("test-junit"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.truth)
+        jvmMain {
+            dependencies {
+                implementation(libs.kotlinStdlib)
             }
-            androidAndroidTest.dependencies {
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(project(":compose:runtime:runtime"))
+                }
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.testExtJunit)
                 implementation(libs.espressoCore)
             }
         }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.truth)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
-    dependencies {
-        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
-    }
+}
+
+dependencies {
+    samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
 }
 
 androidx {
diff --git a/compose/ui/ui-util/build.gradle b/compose/ui/ui-util/build.gradle
index 8eeb75e..4c19723 100644
--- a/compose/ui/ui-util/build.gradle
+++ b/compose/ui/ui-util/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
@@ -24,59 +23,72 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    dependencies {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-        implementation(libs.kotlinStdlib)
-
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.kotlinTest)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
             }
+        }
 
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-
-            androidMain.dependencies {
-                implementation(libs.kotlinStdlib)
-            }
-
-            commonTest.dependencies {
+        commonTest {
+            dependencies {
                 implementation(kotlin("test-junit"))
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
+        jvmMain {
+            dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+            }
+        }
+
+        jvmTest {
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
+            }
+        }
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.truth)
             }
         }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+            }
+        }
     }
 }
 
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 7222c00..eeeb983 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2379,8 +2379,11 @@
   }
 
   public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
-    method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+    method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public default boolean getShouldClearDescendantSemantics();
+    method public default boolean getShouldMergeDescendantSemantics();
+    property public default boolean shouldClearDescendantSemantics;
+    property public default boolean shouldMergeDescendantSemantics;
   }
 
   public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index bb73f64..6b29580 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2064,6 +2064,8 @@
 
   public interface IntrinsicMeasureScope extends androidx.compose.ui.unit.Density {
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
+    method @androidx.compose.ui.ExperimentalComposeUiApi public default boolean isLookingAhead();
+    property @androidx.compose.ui.ExperimentalComposeUiApi public default boolean isLookingAhead;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
   }
 
@@ -2604,8 +2606,11 @@
   }
 
   public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
-    method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+    method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public default boolean getShouldClearDescendantSemantics();
+    method public default boolean getShouldMergeDescendantSemantics();
+    property public default boolean shouldClearDescendantSemantics;
+    property public default boolean shouldMergeDescendantSemantics;
   }
 
   public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 24148fc..c17196f 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2427,8 +2427,11 @@
   }
 
   public interface SemanticsModifierNode extends androidx.compose.ui.node.DelegatableNode {
-    method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
+    method public void applySemantics(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public default boolean getShouldClearDescendantSemantics();
+    method public default boolean getShouldMergeDescendantSemantics();
+    property public default boolean shouldClearDescendantSemantics;
+    property public default boolean shouldMergeDescendantSemantics;
   }
 
   public final class SemanticsModifierNodeKt {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index 2b2f069..3beea81 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -14,9 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.inspection.gradle.InspectionPluginKt.packageInspector
 
@@ -26,130 +25,16 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-    constraints {
-        // In 1.4.0-alpha02 there was a change made in :compose:ui:ui which fixed an issue where
-        // we were over-invalidating layout. This change caused a corresponding regression in
-        // foundation's CoreText, where it was expecting a layout to happen but with this change
-        // it would not. A corresponding fix for this was added in 1.4.0-alpha02 of
-        // :compose:foundation:foundation. By adding this constraint, we are ensuring that the
-        // if an app has this ui module _and_ the foundation module as a dependency, then the
-        // version of foundation will be at least this version. This will prevent the bug in
-        // foundation from occurring. This does _NOT_ require that the app have foundation as
-        // a dependency.
-        implementation(project(":compose:foundation:foundation")) {
-            because 'prevents a critical bug in Text'
-        }
-    }
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        implementation(libs.kotlinStdlibCommon)
-        implementation(libs.kotlinCoroutinesCore)
 
-        // when updating the runtime version please also update the runtime-saveable version
-        implementation(project(":compose:runtime:runtime"))
-        api(project(":compose:runtime:runtime-saveable"))
-
-        api(project(":compose:ui:ui-geometry"))
-        api(project(":compose:ui:ui-graphics"))
-        api(project(":compose:ui:ui-text"))
-        api(project(":compose:ui:ui-unit"))
-        api("androidx.annotation:annotation:1.5.0")
-
-        // This has stub APIs for access to legacy Android APIs, so we don't want
-        // any dependency on this module.
-        compileOnly(project(":compose:ui:ui-android-stubs"))
-
-        implementation(project(":compose:ui:ui-util"))
-        implementation(libs.kotlinStdlib)
-        implementation("androidx.autofill:autofill:1.0.0")
-        implementation(libs.kotlinCoroutinesAndroid)
-
-        // Used to generate debug information in the layout inspector. If not present,
-        // we may fall back to more limited data.
-        compileOnly(libs.kotlinReflect)
-        testImplementation(libs.kotlinReflect)
-
-        implementation("androidx.activity:activity-ktx:1.7.0")
-        implementation(project(":core:core"))
-        implementation('androidx.collection:collection:1.0.0')
-        implementation("androidx.customview:customview-poolingcontainer:1.0.0")
-        implementation("androidx.savedstate:savedstate-ktx:1.2.1")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
-        implementation("androidx.profileinstaller:profileinstaller:1.3.0")
-        implementation("androidx.emoji2:emoji2:1.2.0")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.kotlinCoroutinesTest)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-        testImplementation(libs.mockitoCore4)
-        testImplementation(libs.mockitoKotlin4)
-        testImplementation(libs.robolectric)
-        testImplementation(project(":compose:ui:ui-test-junit4"))
-        testImplementation(project(":compose:test-utils"))
-
-        androidTestImplementation(libs.testCore)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.testExtJunitKtx)
-        androidTestImplementation(libs.testUiautomator)
-        androidTestImplementation(libs.kotlinCoroutinesTest)
-        androidTestImplementation(libs.kotlinTest)
-        androidTestImplementation(libs.espressoCore)
-        androidTestImplementation(libs.bundles.espressoContrib)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.dexmakerMockito)
-        androidTestImplementation(libs.mockitoCore)
-        androidTestImplementation(libs.truth)
-        androidTestImplementation(libs.mockitoKotlin)
-        androidTestImplementation(libs.material)
-        androidTestImplementation(project(":compose:animation:animation-core"))
-        androidTestImplementation(project(":compose:foundation:foundation"))
-        androidTestImplementation(project(":compose:foundation:foundation-layout"))
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation(project(":compose:test-utils"))
-        androidTestImplementation(project(":internal-testutils-fonts"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":internal-testutils-runtime"))
-        androidTestImplementation(project(":test:screenshot:screenshot"))
-        androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.6.1")
-        androidTestImplementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
-        androidTestImplementation("androidx.core:core-ktx:1.9.0")
-        androidTestImplementation("androidx.activity:activity-compose:1.7.0")
-        androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
-        androidTestImplementation("androidx.fragment:fragment:1.3.0")
-
-        lintChecks(project(":compose:ui:ui-lint"))
-        lintPublish(project(":compose:ui:ui-lint"))
-
-        samples(project(":compose:ui:ui:ui-samples"))
-    }
-}
-
-packageInspector(project, ":compose:ui:ui-inspection")
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
                 implementation(libs.kotlinCoroutinesCore)
 
@@ -163,8 +48,33 @@
                 api project(":compose:ui:ui-unit")
                 implementation(project(":compose:ui:ui-util"))
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+                implementation(libs.kotlinReflect)
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+            }
+        }
+
+        if (desktopEnabled) {
+            skikoMain {
+                dependsOn(commonMain)
+                dependencies {
+                    api(project(":compose:ui:ui-graphics"))
+                    api(libs.skikoCommon)
+                }
+            }
+        }
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 implementation(libs.kotlinStdlib)
                 // This has stub APIs for access to legacy Android APIs, so we don't want
                 // any dependency on this module.
@@ -181,48 +91,32 @@
                 implementation("androidx.lifecycle:lifecycle-runtime:2.6.1")
                 implementation("androidx.lifecycle:lifecycle-viewmodel:2.6.1")
                 implementation("androidx.emoji2:emoji2:1.2.0")
-            }
 
-            jvmMain.dependencies {
-                implementation(libs.kotlinStdlib)
+                implementation("androidx.profileinstaller:profileinstaller:1.3.0")
             }
-            skikoMain {
-                dependsOn(commonMain)
-                dependencies {
-                    api(project(":compose:ui:ui-graphics"))
-                    api(libs.skikoCommon)
-                }
-            }
+        }
+
+        if (desktopEnabled) {
             desktopMain {
                 dependsOn(skikoMain)
+                dependsOn(jvmMain)
                 dependencies {
+                    implementation(libs.kotlinStdlib)
                     implementation(libs.kotlinStdlibJdk8)
                     api(libs.kotlinCoroutinesSwing)
                 }
             }
+        }
 
-            commonTest.dependencies {
-                implementation(libs.kotlinReflect)
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.kotlinCoroutinesTest)
-                implementation(libs.junit)
-                implementation(libs.truth)
-                implementation(libs.mockitoCore4)
-                implementation(libs.mockitoKotlin4)
-                implementation(project(":compose:ui:ui-test-junit4"))
-                implementation(project(":internal-testutils-fonts"))
-                implementation(project(":compose:test-utils"))
-            }
-
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation("androidx.fragment:fragment:1.3.0")
                 implementation("androidx.appcompat:appcompat:1.3.0")
                 implementation(libs.testUiautomator)
@@ -253,27 +147,75 @@
                 implementation("androidx.core:core-ktx:1.2.0")
                 implementation("androidx.activity:activity-compose:1.7.0")
             }
+        }
 
-            desktopTest.dependencies {
-                implementation(libs.truth)
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.kotlinCoroutinesTest)
                 implementation(libs.junit)
-                implementation(libs.mockitoCore)
-                implementation(libs.mockitoKotlin)
-                implementation(libs.skikoCurrentOs)
-                implementation(project(":compose:material:material"))
+                implementation(libs.truth)
                 implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":internal-testutils-fonts"))
+                implementation(project(":compose:test-utils"))
+            }
+        }
+
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                    implementation(libs.truth)
+                    implementation(libs.junit)
+                    implementation(libs.mockitoCore)
+                    implementation(libs.mockitoKotlin)
+                    implementation(libs.skikoCurrentOs)
+                    implementation(project(":compose:material:material"))
+                    implementation(project(":compose:ui:ui-test-junit4"))
+                }
             }
         }
     }
-    dependencies {
-        samples(project(":compose:ui:ui:ui-samples"))
+}
 
-        // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
-        // leaks into instrumented tests (b/214407011)
-        testImplementation(libs.robolectric)
+dependencies {
+
+    constraints {
+        // In 1.4.0-alpha02 there was a change made in :compose:ui:ui which fixed an issue where
+        // we were over-invalidating layout. This change caused a corresponding regression in
+        // foundation's CoreText, where it was expecting a layout to happen but with this change
+        // it would not. A corresponding fix for this was added in 1.4.0-alpha02 of
+        // :compose:foundation:foundation. By adding this constraint, we are ensuring that the
+        // if an app has this ui module _and_ the foundation module as a dependency, then the
+        // version of foundation will be at least this version. This will prevent the bug in
+        // foundation from occurring. This does _NOT_ require that the app have foundation as
+        // a dependency.
+        implementation(project(":compose:foundation:foundation")) {
+            because 'prevents a critical bug in Text'
+        }
     }
 }
 
+packageInspector(project, ":compose:ui:ui-inspection")
+
+dependencies {
+    lintChecks(project(":compose:ui:ui-lint"))
+    lintPublish(project(":compose:ui:ui-lint"))
+
+    // Can't declare this in kotlin { sourceSets { androidTest.dependencies { .. } } } as that
+    // leaks into instrumented tests (b/214407011)
+    testImplementation(libs.robolectric)
+    testImplementation(libs.mockitoCore4)
+    testImplementation(libs.mockitoKotlin4)
+}
+
 androidx {
     name = "Compose UI primitives"
     type = LibraryType.PUBLISHED_LIBRARY
@@ -282,7 +224,7 @@
     legacyDisableKotlinStrictApiMode = true
 }
 
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+if (desktopEnabled) {
     tasks.findByName("desktopTest").configure {
         systemProperties["GOLDEN_PATH"] = project.rootDir.absolutePath + "/../../golden"
     }
@@ -304,24 +246,3 @@
     // the androidx.compose.ui:ui-test library
     testNamespace "androidx.compose.ui.tests"
 }
-
-// Diagnostics for b/188565660
-def verifyKotlinModule(String variant) {
-    project.afterEvaluate {
-        def capitalVariant = variant.capitalize()
-        def moduleFile = new File("${buildDir}/tmp/kotlin-classes/${variant}/META-INF/ui_${variant}.kotlin_module")
-        tasks.named("compile${capitalVariant}Kotlin").configure { t ->
-            t.doLast {
-                // This file should be large, about 3.2K. If this file is short then many symbols will fail to resolve
-                if (moduleFile.length() < 250) {
-                    throw new GradleException("kotlin_module file ($moduleFile) too short! See b/188565660 for more information. File text: ${moduleFile.text}")
-                }
-            }
-        }
-    }
-}
-if (!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    for (variant in ["debug", "release"]) {
-        verifyKotlinModule(variant)
-    }
-}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
index 343c89d..6d4e513 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LookaheadLayoutSample.kt
@@ -40,14 +40,17 @@
 import androidx.compose.runtime.movableContentOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.draw.clipToBounds
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.LookaheadScope
 import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -209,3 +212,53 @@
         }
     }
 }
+
+@Sampled
+@Composable
+fun animateContentSizeAfterLookaheadPass() {
+    var sizeAnim by remember {
+        mutableStateOf<Animatable<IntSize, AnimationVector2D>?>(null)
+    }
+    var lookaheadSize by remember {
+        mutableStateOf<IntSize?>(null)
+    }
+    val coroutineScope = rememberCoroutineScope()
+    LookaheadScope {
+        // The Box is in a LookaheadScope. This means there will be a lookahead measure pass
+        // before the main measure pass.
+        // Here we are creating something similar to the `animateContentSize` modifier.
+        Box(
+            Modifier
+                .clipToBounds()
+                .layout { measurable, constraints ->
+                    val placeable = measurable.measure(constraints)
+
+                    val measuredSize = IntSize(placeable.width, placeable.height)
+                    val (width, height) = if (isLookingAhead) {
+                        // Record lookahead size if we are in lookahead pass. This lookahead size
+                        // will be used for size animation, such that the main measure pass will
+                        // gradually change size until it reaches the lookahead size.
+                        lookaheadSize = measuredSize
+                        measuredSize
+                    } else {
+                        // Since we are in an explicit lookaheadScope, we know the lookahead pass
+                        // is guaranteed to happen, therefore the lookahead size that we recorded is
+                        // not null.
+                        val target = requireNotNull(lookaheadSize)
+                        val anim = sizeAnim?.also {
+                            coroutineScope.launch { it.animateTo(target) }
+                        } ?: Animatable(target, IntSize.VectorConverter)
+                        sizeAnim = anim
+                        // By returning the animated size only during main pass, we are allowing
+                        // lookahead pass to see the future layout past the animation.
+                        anim.value
+                    }
+
+                    layout(width, height) {
+                        placeable.place(0, 0)
+                    }
+                }) {
+            // Some content that changes size
+        }
+    }
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
index a61a41e..020c4b9 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
@@ -51,7 +51,7 @@
 import androidx.compose.ui.node.SemanticsModifierNode
 import androidx.compose.ui.node.requireLayoutDirection
 import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.heading
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.unit.IntSize
@@ -155,13 +155,12 @@
             gesture.onCancelPointerInput()
         }
 
-        override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
-            .apply {
-                onClick {
-                    gesture.onTap()
-                    true
-                }
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            onClick {
+                gesture.onTap()
+                true
             }
+        }
     }
 }
 
@@ -184,13 +183,12 @@
     }
 
     class TapSemanticsNode(var onTap: () -> Unit) : SemanticsModifierNode, Modifier.Node() {
-        override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
-            .apply {
-                onClick {
-                    onTap()
-                    true
-                }
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            onClick {
+                onTap()
+                true
             }
+        }
     }
     class TapGestureWithClickSemantics(onTap: () -> Unit) : DelegatingNode() {
         var onTap: () -> Unit
@@ -250,13 +248,12 @@
     }
 
     class TapSemanticsNode(var onTap: () -> Unit) : SemanticsModifierNode, Modifier.Node() {
-        override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
-            .apply {
-                onClick {
-                    onTap()
-                    true
-                }
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            onClick {
+                onTap()
+                true
             }
+        }
     }
     class TapGestureWithClickSemantics(onTap: () -> Unit) : DelegatingNode() {
         var onTap: () -> Unit
@@ -340,10 +337,9 @@
 @Composable
 fun SemanticsModifierNodeSample() {
     class HeadingNode : SemanticsModifierNode, Modifier.Node() {
-        override val semanticsConfiguration: SemanticsConfiguration = SemanticsConfiguration()
-            .apply {
-                heading()
-            }
+        override fun SemanticsPropertyReceiver.applySemantics() {
+            heading()
+        }
     }
 
     val HeadingElement = object : ModifierNodeElement<HeadingNode>() {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 4e95ffd..337f866 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -29,15 +29,44 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.runtime.structuralEqualityPolicy
+import androidx.compose.ui.autofill.Autofill
+import androidx.compose.ui.autofill.AutofillTree
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.focus.FocusOwner
+import androidx.compose.ui.geometry.MutableRect
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.graphics.RenderEffect
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.toAndroidRect
+import androidx.compose.ui.hapticfeedback.HapticFeedback
+import androidx.compose.ui.input.InputModeManager
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.pointer.PointerIconService
+import androidx.compose.ui.modifier.ModifierLocalManager
+import androidx.compose.ui.node.InternalCoreApi
 import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.LayoutNodeDrawScope
+import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.node.Owner
+import androidx.compose.ui.node.OwnerSnapshotObserver
+import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.platform.AccessibilityManager
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.SemanticsNodeCopy
+import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.LocalClipboardManager
 import androidx.compose.ui.platform.SemanticsNodeWithAdjustedBounds
+import androidx.compose.ui.platform.TextToolbar
+import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
 import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToMap
+import androidx.compose.ui.platform.invertTo
 import androidx.compose.ui.semantics.CustomAccessibilityAction
 import androidx.compose.ui.semantics.EmptySemanticsElement
 import androidx.compose.ui.semantics.LiveRegionMode
@@ -78,11 +107,23 @@
 import androidx.compose.ui.semantics.text
 import androidx.compose.ui.semantics.textSelectionRange
 import androidx.compose.ui.semantics.verticalScrollAxisRange
+import androidx.compose.ui.test.InternalTestApi
 import androidx.compose.ui.test.TestActivity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.input.PlatformTextInputPluginRegistry
+import androidx.compose.ui.text.input.TextInputService
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.toOffset
 import androidx.core.view.ViewCompat
 import androidx.core.view.ViewStructureCompat
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
@@ -106,6 +147,7 @@
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import java.util.concurrent.Executors
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.asCoroutineDispatcher
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -1593,6 +1635,7 @@
         accessibilityDelegate.sendSemanticsPropertyChangeEvents(mapOf(nodeId to newTextNode))
     }
 
+    @OptIn(InternalTestApi::class)
     private fun createSemanticsNodeWithProperties(
         id: Int,
         mergeDescendants: Boolean,
@@ -1602,6 +1645,7 @@
         layoutNode.modifier = Modifier.semantics(mergeDescendants) {
             properties()
         }
+        layoutNode.attach(MockOwner())
         return SemanticsNode(layoutNode, true)
     }
 
@@ -1615,6 +1659,7 @@
         layoutNode.modifier = Modifier.semantics {
             properties()
         }
+        layoutNode.attach(MockOwner())
         return SemanticsNode(layoutNode, true)
     }
 
@@ -1641,3 +1686,228 @@
         return false
     }
 }
+
+internal class MockOwner(
+    private val position: IntOffset = IntOffset.Zero,
+    override val root: LayoutNode = LayoutNode(),
+    override val coroutineContext: CoroutineContext =
+        Executors.newFixedThreadPool(3).asCoroutineDispatcher()
+) : Owner {
+    val onRequestMeasureParams = mutableListOf<LayoutNode>()
+    val onAttachParams = mutableListOf<LayoutNode>()
+    val onDetachParams = mutableListOf<LayoutNode>()
+    var layoutChangeCount = 0
+
+    override val rootForTest: RootForTest
+        get() = TODO("Not yet implemented")
+    override val hapticFeedBack: HapticFeedback
+        get() = TODO("Not yet implemented")
+    override val inputModeManager: InputModeManager
+        get() = TODO("Not yet implemented")
+    override val clipboardManager: ClipboardManager
+        get() = TODO("Not yet implemented")
+    override val accessibilityManager: AccessibilityManager
+        get() = TODO("Not yet implemented")
+    override val textToolbar: TextToolbar
+        get() = TODO("Not yet implemented")
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override val autofillTree: AutofillTree
+        get() = TODO("Not yet implemented")
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override val autofill: Autofill?
+        get() = TODO("Not yet implemented")
+    override val density: Density
+        get() = Density(1f)
+    override val textInputService: TextInputService
+        get() = TODO("Not yet implemented")
+    @OptIn(ExperimentalTextApi::class)
+    override val platformTextInputPluginRegistry: PlatformTextInputPluginRegistry
+        get() = TODO("Not yet implemented")
+    override val pointerIconService: PointerIconService
+        get() = TODO("Not yet implemented")
+    override val focusOwner: FocusOwner
+        get() = TODO("Not yet implemented")
+    override val windowInfo: WindowInfo
+        get() = TODO("Not yet implemented")
+
+    @Deprecated(
+        "fontLoader is deprecated, use fontFamilyResolver",
+        replaceWith = ReplaceWith("fontFamilyResolver")
+    )
+    @Suppress("DEPRECATION")
+    override val fontLoader: Font.ResourceLoader
+        get() = TODO("Not yet implemented")
+    override val fontFamilyResolver: FontFamily.Resolver
+        get() = TODO("Not yet implemented")
+    override val layoutDirection: LayoutDirection
+        get() = LayoutDirection.Ltr
+    @InternalCoreApi
+    override var showLayoutBounds: Boolean = false
+    override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
+    override val modifierLocalManager: ModifierLocalManager = ModifierLocalManager(this)
+
+    override fun onRequestMeasure(
+        layoutNode: LayoutNode,
+        affectsLookahead: Boolean,
+        forceRequest: Boolean,
+        scheduleMeasureAndLayout: Boolean
+    ) {
+        onRequestMeasureParams += layoutNode
+        if (affectsLookahead) {
+            layoutNode.markLookaheadMeasurePending()
+        }
+        layoutNode.markMeasurePending()
+    }
+
+    override fun onRequestRelayout(
+        layoutNode: LayoutNode,
+        affectsLookahead: Boolean,
+        forceRequest: Boolean
+    ) {
+        if (affectsLookahead) {
+            layoutNode.markLookaheadLayoutPending()
+        }
+        layoutNode.markLayoutPending()
+    }
+
+    override fun requestOnPositionedCallback(layoutNode: LayoutNode) {
+    }
+
+    override fun onAttach(node: LayoutNode) {
+        onAttachParams += node
+    }
+
+    override fun onDetach(node: LayoutNode) {
+        onDetachParams += node
+    }
+
+    override fun calculatePositionInWindow(localPosition: Offset): Offset =
+        localPosition + position.toOffset()
+
+    override fun calculateLocalPosition(positionInWindow: Offset): Offset =
+        positionInWindow - position.toOffset()
+
+    override fun requestFocus(): Boolean = false
+
+    override fun measureAndLayout(sendPointerUpdate: Boolean) {
+    }
+
+    override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
+    }
+
+    override fun forceMeasureTheSubtree(layoutNode: LayoutNode, affectsLookahead: Boolean) {
+    }
+
+    override fun registerOnEndApplyChangesListener(listener: () -> Unit) {
+        listener()
+    }
+
+    override fun onEndApplyChanges() {
+    }
+
+    override fun registerOnLayoutCompletedListener(listener: Owner.OnLayoutCompletedListener) {
+        TODO("Not yet implemented")
+    }
+
+    val invalidatedLayers = mutableListOf<OwnedLayer>()
+
+    override fun createLayer(
+        drawBlock: (Canvas) -> Unit,
+        invalidateParentLayer: () -> Unit
+    ): OwnedLayer {
+        val transform = Matrix()
+        val inverseTransform = Matrix()
+        return object : OwnedLayer {
+            override fun updateLayerProperties(
+                scaleX: Float,
+                scaleY: Float,
+                alpha: Float,
+                translationX: Float,
+                translationY: Float,
+                shadowElevation: Float,
+                rotationX: Float,
+                rotationY: Float,
+                rotationZ: Float,
+                cameraDistance: Float,
+                transformOrigin: TransformOrigin,
+                shape: Shape,
+                clip: Boolean,
+                renderEffect: RenderEffect?,
+                ambientShadowColor: Color,
+                spotShadowColor: Color,
+                compositingStrategy: CompositingStrategy,
+                layoutDirection: LayoutDirection,
+                density: Density
+            ) {
+                transform.reset()
+                // This is not expected to be 100% accurate
+                transform.scale(scaleX, scaleY)
+                transform.rotateZ(rotationZ)
+                transform.translate(translationX, translationY)
+                transform.invertTo(inverseTransform)
+            }
+
+            override fun isInLayer(position: Offset) = true
+
+            override fun move(position: IntOffset) {
+            }
+
+            override fun resize(size: IntSize) {
+            }
+
+            override fun drawLayer(canvas: Canvas) {
+                drawBlock(canvas)
+            }
+
+            override fun updateDisplayList() {
+            }
+
+            override fun invalidate() {
+                invalidatedLayers.add(this)
+            }
+
+            override fun destroy() {
+            }
+
+            override fun mapBounds(rect: MutableRect, inverse: Boolean) {
+            }
+
+            override fun reuseLayer(
+                drawBlock: (Canvas) -> Unit,
+                invalidateParentLayer: () -> Unit
+            ) {
+            }
+
+            override fun transform(matrix: Matrix) {
+                matrix.timesAssign(transform)
+            }
+
+            override fun inverseTransform(matrix: Matrix) {
+                matrix.timesAssign(inverseTransform)
+            }
+
+            override fun mapOffset(point: Offset, inverse: Boolean) = point
+        }
+    }
+
+    var semanticsChanged: Boolean = false
+    override fun onSemanticsChange() {
+        semanticsChanged = true
+    }
+
+    override fun onLayoutChange(layoutNode: LayoutNode) {
+        layoutChangeCount++
+    }
+
+    override fun getFocusDirection(keyEvent: KeyEvent): FocusDirection? {
+        TODO("Not yet implemented")
+    }
+
+    override var measureIteration: Long = 0
+    override val viewConfiguration: ViewConfiguration
+        get() = TODO("Not yet implemented")
+
+    override val sharedDrawScope = LayoutNodeDrawScope()
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 32fd60c..f761c28 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -4405,38 +4405,3 @@
 )
 
 internal typealias PointerEventHandler = (PointerEvent, PointerEventPass, IntSize) -> Unit
-
-private fun PointerEventHandler.invokeOverAllPasses(
-    pointerEvent: PointerEvent,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    invokeOverPasses(
-        pointerEvent,
-        listOf(
-            PointerEventPass.Initial,
-            PointerEventPass.Main,
-            PointerEventPass.Final
-        ),
-        size = size
-    )
-}
-
-private fun PointerEventHandler.invokeOverPasses(
-    pointerEvent: PointerEvent,
-    vararg pointerEventPasses: PointerEventPass,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    invokeOverPasses(pointerEvent, pointerEventPasses.toList(), size)
-}
-
-private fun PointerEventHandler.invokeOverPasses(
-    pointerEvent: PointerEvent,
-    pointerEventPasses: List<PointerEventPass>,
-    size: IntSize = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
-) {
-    require(pointerEvent.changes.isNotEmpty())
-    require(pointerEventPasses.isNotEmpty())
-    pointerEventPasses.forEach {
-        this.invoke(pointerEvent, it, size)
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
new file mode 100644
index 0000000..ddf5536
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterCoroutineJobTest.kt
@@ -0,0 +1,110 @@
+/*
+ * 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.ui.input.pointer
+
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.IntSize
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import kotlin.test.assertEquals
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.junit.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SuspendingPointerInputFilterCoroutineJobTest {
+    @OptIn(ExperimentalTestApi::class)
+    @get:Rule
+    val rule = createComposeRule(Dispatchers.Main)
+
+    @Test
+    @LargeTest
+    fun isPointerInputJobStillActive_cancelPointerEvent_assertsTrue() {
+        val latch = CountDownLatch(1)
+
+        var repeatActualNumber = 0
+        val repeatExpectedNumber = 4
+
+        // To create Pointer Events:
+        val emitter = PointerInputChangeEmitter()
+        val change = emitter.nextChange(Offset(5f, 5f))
+
+        // Used to manually trigger a PointerEvent created from our PointerInputChange.
+        val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
+            coroutineScope {
+                awaitEachGesture {
+                    // First down event (triggered)
+                    awaitFirstDown()
+                    // Up event won't ever come since we never trigger the event, times out as
+                    // a cancellation
+                    waitForUpOrCancellation()
+
+                    // By running this code after down/up, we are making sure the block of code
+                    // for handling pointer input events is still active despite the cancel pointer
+                    // input being called later.
+                    launch(start = CoroutineStart.UNDISPATCHED) {
+                        repeat(repeatExpectedNumber) {
+                            repeatActualNumber++
+                            delay(100)
+                        }
+                        latch.countDown()
+                    }
+                }
+            }
+        }
+
+        rule.setContent {
+            Box(
+                modifier = elementFor(
+                    key1 = Unit,
+                    instance = suspendingPointerInputModifierNode as Modifier.Node
+                )
+            )
+        }
+
+        rule.runOnIdle {
+            suspendingPointerInputModifierNode.onPointerEvent(
+                change.toPointerEvent(),
+                PointerEventPass.Main,
+                IntSize(10, 10)
+            )
+        }
+
+        suspendingPointerInputModifierNode.onCancelPointerInput()
+
+        val resultOfLatch = latch.await(3000, TimeUnit.MILLISECONDS)
+        assertTrue("Waiting for coroutine's tasks to finish", resultOfLatch)
+        assertEquals(repeatExpectedNumber, repeatActualNumber)
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
index 1d3593d..c521919 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilterTest.kt
@@ -24,10 +24,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.node.DelegatingNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.ValueElement
-import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.elementFor
@@ -273,7 +270,7 @@
                 )
             }
 
-            // Triggers cancel event
+            // Manually cancels the current pointer input event.
             suspendingPointerInputModifierNode.onCancelPointerInput()
         }
 
@@ -305,15 +302,16 @@
         var currentEventAtEnd: PointerEvent? = null
         val results = Channel<PointerEvent>(Channel.UNLIMITED)
 
-        // Used to manually trigger a PointerEvent(s) created from our PointerInputChange(s).
+        // Used to manually trigger/test PointerEvents and other functionality.
         val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
             awaitPointerEventScope {
                 try {
                     // NOTE: This will never trigger 3 times. There are only two events
-                    // triggered followed by a onCancelPointerInput() call which doesn't trigger
-                    // an event because the previous event has down (press) set to false, so we
-                    // will always get an exception thrown with the last repeat's timeout
-                    // (we expect this).
+                    // triggered (a down [press] event followed by an event with down [press] set to
+                    // false). The resetPointerInputHandler() is called after that, but it won't
+                    // trigger a cancel event if the previous event has the down [press] set to
+                    // false. In this case, it does, so there are only ever two events, so the
+                    // withTimeout() will trigger the timeout (expected).
                     repeat(3) {
                         withTimeout(200) {
                             results.trySend(awaitPointerEvent())
@@ -334,7 +332,6 @@
                 )
             )
         }
-
         val bounds = IntSize(50, 50)
         val emitter1 = PointerInputChangeEmitter(0)
         val emitter2 = PointerInputChangeEmitter(1)
@@ -378,9 +375,9 @@
                 )
             }
 
-            // Manually triggers cancel event.
-            // Note: This will not trigger an event in the customPointerInput block because the
-            // previous events don't have any pressed pointers.
+            // Manually cancels the current pointer input event.
+            // Note: This will not trigger an event in the customPointerInput block because of the
+            // reasons listed above (previous event has down (press) set to false).
             suspendingPointerInputModifierNode.onCancelPointerInput()
         }
 
@@ -411,35 +408,48 @@
 
     @Test
     @MediumTest
-    fun testCancelledHandlerBlock() {
+    fun testCancelPointerInput() {
+        val firstEventCheckpointInfo =
+            Pair(3, "First pointer event triggered to create Job.")
+        val cancelEventCheckpointInfo =
+            Pair(5, "Cancel pointer event triggered (shouldn't cancel Job).")
+        val invalidEventCheckpointNumber =
+            Pair(-1, "Should never execute.")
+
         val counter = TestCounter()
 
-        // Used to manually trigger a PointerEvent(s) created from our PointerInputChange(s).
+        // Used to manually trigger/test PointerEvents and other functionality.
         val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
             try {
                 awaitPointerEventScope {
                     try {
-                        counter.expect(3, "about to call awaitPointerEvent")
+                        counter.expect(2, "Before awaitPointerEvent() call.")
 
-                        // With only one event triggered, this will stay stuck in the repeat
-                        // block until the Job is cancelled via
-                        // SuspendPointerInputModifierNode.resetHandling()
-                        repeat(2) {
+                        // With only two events triggered (press and cancel), this will stay stuck
+                        // in the repeat block until it is torn down, that is, until the
+                        // test is over.
+                        repeat(3) { repeatCount ->
                             awaitPointerEvent()
-                            counter.expect(
-                                4,
-                                "One and only pointer event triggered to create Job."
-                            )
+                            val checkpointInfo = when (repeatCount) {
+                                0 -> { firstEventCheckpointInfo }
+                                1 -> { cancelEventCheckpointInfo }
+                                else -> {
+                                    fail("Should never be three events.")
+                                    invalidEventCheckpointNumber
+                                }
+                            }
+                            counter.expect(checkpointInfo.first, checkpointInfo.second)
                         }
-
-                        fail("awaitPointerEvent returned; should have thrown for cancel")
+                        fail("awaitPointerEvent() run 3+ times in repeat() block, should only " +
+                            "have run twice (one event, one cancel).")
                     } finally {
-                        counter.expect(6, "inner finally block running")
+                        counter.expect(7, "Inner finally block runs after " +
+                            "teardown.")
                     }
                 }
             } finally {
-                counter.expect(7, "outer finally block running; inner " +
-                    "finally should have run")
+                counter.expect(8, "Outer finally block runs; inner finally " +
+                    "block should have already run.")
             }
         }
 
@@ -457,16 +467,13 @@
         val singleEventBounds = IntSize(20, 20)
 
         rule.runOnIdle {
+            // Because the pointer input handler is triggered lazily in
+            // SuspendPointerInputModifierNode, it will not be triggered until the first event
+            // comes in, so this will be the first counter checkpoint.
             counter.expect(
                 1,
-                "Job to handle pointer input not created yet; awaitPointerEvent should " +
-                    "be suspended"
-            )
-
-            counter.expect(
-                2,
-                "Trigger pointer input event to create Job for handing handle pointer" +
-                    " input (done lazily in SuspendPointerInputModifierNode)."
+                "Trigger pointer input handler through first pointer input event " +
+                    "(handler triggered lazily)."
             )
 
             suspendingPointerInputModifierNode.onPointerEvent(
@@ -475,12 +482,96 @@
                 singleEventBounds
             )
 
-            counter.expect(5, "before cancelling handler; awaitPointerEvent " +
+            counter.expect(4, "Before onCancelPointerInput() handler; awaitPointerEvent " +
                 "should be suspended")
 
-            // Cancels Job that manages pointer input events in SuspendPointerInputModifierNode.
+            // Manually cancels the current pointer input event.
+            suspendingPointerInputModifierNode.onCancelPointerInput()
+            counter.expect(6, "After onCancelPointerInput(), end of test, " +
+                " start teardown.")
+        }
+    }
+
+    @Test
+    @MediumTest
+    fun testResetHandlerBlock() {
+        val counter = TestCounter()
+
+        // Used to manually trigger/test PointerEvents and other functionality.
+        val suspendingPointerInputModifierNode = SuspendingPointerInputModifierNode {
+            try {
+                awaitPointerEventScope {
+                    try {
+                        counter.expect(2, "Before awaitPointerEvent() call.")
+
+                        // With only one event triggered (press) to kick start the handler, this
+                        // will stay stuck in the repeat block until it is torn down, that is,
+                        // until the test is over.
+                        repeat(2) { repeatCount ->
+                            awaitPointerEvent()
+                            when (repeatCount) {
+                                0 -> {
+                                    counter.expect(
+                                        3,
+                                        "First/only pointer event triggered."
+                                    )
+                                }
+                                else -> {
+                                    fail("Should never be two or more events.")
+                                    counter.expect(-1, "Should never execute.")
+                                }
+                            }
+                        }
+                        fail("awaitPointerEvent repeated twice; should have only happened once " +
+                            "and stayed suspended in repeat() waiting for a second event (that " +
+                            "should never arrive).")
+                    } finally {
+                        fail("inner finally shouldn't call during teardown since coroutine job " +
+                            "was cancelled with resetPointerInputHandler().")
+                    }
+                }
+            } finally {
+                counter.expect(5, "outer finally block runs after " +
+                    "resetPointerInputHandler().")
+            }
+        }
+
+        rule.setContent {
+            Box(
+                modifier = elementFor(
+                    key1 = Unit,
+                    instance = suspendingPointerInputModifierNode as Modifier.Node
+                )
+            )
+        }
+
+        val emitter = PointerInputChangeEmitter()
+        val singleEvent = emitter.nextChange(Offset(5f, 5f))
+        val singleEventBounds = IntSize(20, 20)
+
+        rule.runOnIdle {
+            // Because the pointer input handler is triggered lazily in
+            // SuspendPointerInputModifierNode, it will not be triggered until the first event
+            // comes in, so this will be the first counter checkpoint.
+            counter.expect(
+                1,
+                "Trigger pointer input handler through first pointer input event " +
+                    "(handler triggered lazily)."
+            )
+
+            suspendingPointerInputModifierNode.onPointerEvent(
+                singleEvent.toPointerEvent(),
+                PointerEventPass.Main,
+                singleEventBounds
+            )
+
+            counter.expect(4, "before resetPointerInputHandler(), handler should" +
+                "be suspended waiting for a second event (that never comes).")
+
+            // Cancels the pointer input handler in SuspendPointerInputModifierNode (and thus the
+            // Coroutine Job associated with it).
             suspendingPointerInputModifierNode.resetPointerInputHandler()
-            counter.expect(8, "after cancelling; finally blocks should have run")
+            counter.expect(6, "after resetPointerInputHandler(), end of test.")
         }
     }
 
@@ -774,72 +865,3 @@
         assertThat(events).hasSize(2)
     }
 }
-
-private fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
-
-private val PointerEvent.firstChange get() = changes.first()
-
-private class PointerInputChangeEmitter(id: Int = 0) {
-    val pointerId = PointerId(id.toLong())
-    var previousTime = 0L
-    var previousPosition = Offset.Zero
-    var previousPressed = false
-
-    fun nextChange(
-        position: Offset = Offset.Zero,
-        down: Boolean = true,
-        time: Long = 0
-    ): PointerInputChange {
-        return PointerInputChange(
-            id = pointerId,
-            time,
-            position,
-            down,
-            previousTime,
-            previousPosition,
-            previousPressed,
-            isInitiallyConsumed = false
-        ).also {
-            previousTime = time
-            previousPosition = position
-            previousPressed = down
-        }
-    }
-}
-
-private class TestCounter {
-    private var count = 0
-
-    fun expect(checkpoint: Int, message: String = "(no message)") {
-        val expected = count + 1
-        if (checkpoint != expected) {
-            fail("out of order event $checkpoint, expected $expected, $message")
-        }
-        count = expected
-    }
-}
-
-private fun elementFor(
-    key1: Any? = null,
-    instance: Modifier.Node
-) = object : ModifierNodeElement<Modifier.Node>() {
-    override fun InspectorInfo.inspectableProperties() {
-        debugInspectorInfo {
-            name = "pointerInput"
-            properties["key1"] = key1
-            properties["instance"] = instance
-        }
-    }
-
-    override fun create() = instance
-    override fun update(node: Modifier.Node) {}
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (other !is SuspendPointerInputElement) return false
-        if (key1 != other.key1) return false
-        return true
-    }
-    override fun hashCode(): Int {
-        return key1?.hashCode() ?: 0
-    }
-}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
index af27c0d..fb92e8f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/TestUtils.kt
@@ -28,13 +28,17 @@
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.NodeCoordinator
 import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.IntSize
 import com.google.common.truth.FailureMetadata
 import com.google.common.truth.Subject
 import com.google.common.truth.Subject.Factory
 import com.google.common.truth.Truth
+import org.junit.Assert
 
 @OptIn(ExperimentalComposeUiApi::class)
 internal fun PointerInputEventData(
@@ -555,3 +559,73 @@
     type = this.type,
     scrollDelta = this.scrollDelta
 )
+
+// SuspendingPointerInputFilter test utilities
+internal fun PointerInputChange.toPointerEvent() = PointerEvent(listOf(this))
+
+internal val PointerEvent.firstChange get() = changes.first()
+
+internal class PointerInputChangeEmitter(id: Int = 0) {
+    val pointerId = PointerId(id.toLong())
+    var previousTime = 0L
+    var previousPosition = Offset.Zero
+    var previousPressed = false
+
+    fun nextChange(
+        position: Offset = Offset.Zero,
+        down: Boolean = true,
+        time: Long = 0
+    ): PointerInputChange {
+        return PointerInputChange(
+            id = pointerId,
+            time,
+            position,
+            down,
+            previousTime,
+            previousPosition,
+            previousPressed,
+            isInitiallyConsumed = false
+        ).also {
+            previousTime = time
+            previousPosition = position
+            previousPressed = down
+        }
+    }
+}
+
+internal class TestCounter {
+    private var count = 0
+
+    fun expect(checkpoint: Int, message: String = "(no message)") {
+        val expected = count + 1
+        if (checkpoint != expected) {
+            Assert.fail("out of order event $checkpoint, expected $expected, $message")
+        }
+        count = expected
+    }
+}
+
+internal fun elementFor(
+    key1: Any? = null,
+    instance: Modifier.Node
+) = object : ModifierNodeElement<Modifier.Node>() {
+    override fun InspectorInfo.inspectableProperties() {
+        debugInspectorInfo {
+            name = "pointerInput"
+            properties["key1"] = key1
+            properties["instance"] = instance
+        }
+    }
+
+    override fun create() = instance
+    override fun update(node: Modifier.Node) {}
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is SuspendPointerInputElement) return false
+        if (key1 != other.key1) return false
+        return true
+    }
+    override fun hashCode(): Int {
+        return key1?.hashCode() ?: 0
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 302768a..378d5b7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -65,8 +65,11 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Matrix
+import androidx.compose.ui.node.LayoutModifierNode
 import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
+import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
 import androidx.compose.ui.unit.Constraints
@@ -83,6 +86,7 @@
 import androidx.test.filters.MediumTest
 import java.lang.Integer.max
 import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertTrue
 import kotlin.math.roundToInt
 import kotlin.random.Random
@@ -1928,6 +1932,218 @@
         }
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    @Test
+    fun testIsLookingAhead() {
+        var iterations by mutableStateOf(0)
+        val size = mutableMapOf<Boolean, IntSize>()
+        rule.setContent {
+            Box(Modifier.fillMaxSize()) {
+                LookaheadScope {
+                    // Fill max size will cause the remeasure requests to go down the
+                    // forceMeasureSubtree code path.
+                    CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                        Column(Modifier.fillMaxSize()) {
+                            // This box will get a remeasure request when `iterations` changes.
+                            // Subsequently this Box's size change will trigger a measurement pass
+                            // from Column.
+                            Box(
+                                Modifier
+                                    .layout { measurable, constraints ->
+                                        measurable
+                                            .measure(constraints)
+                                            .run {
+                                                size[isLookingAhead] = IntSize(width, height)
+                                                layout(width, height) {
+                                                    place(0, 0)
+                                                }
+                                            }
+                                    }
+                                    .intermediateLayout { measurable, _ ->
+                                        // Force a state-read (similar to animation but more
+                                        // reliable)
+                                        measurable
+                                            .measure(Constraints.fixed(200 + 100 * iterations, 200))
+                                            .run {
+                                                layout(width, height) {
+                                                    place(0, 0)
+                                                }
+                                            }
+                                    }) {
+                                Box(Modifier.size(100.dp))
+                            }
+                        }
+                        SubcomposeLayout(
+                            Modifier
+                                .fillMaxSize()
+                                .requiredSize(200.dp),
+                            intermediateMeasurePolicy = { constraints ->
+                                assertFalse(isLookingAhead)
+                                measurablesForSlot(Unit)[0].measure(constraints)
+                                layout(0, 0) {}
+                            }
+                        ) { constraints ->
+                            assertTrue(isLookingAhead)
+                            val placeable = subcompose(Unit) {
+                                Box(Modifier.requiredSize(400.dp, 600.dp))
+                            }[0].measure(constraints)
+                            layout(500, 300) {
+                                placeable.place(0, 0)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        repeat(4) {
+            rule.runOnIdle {
+                assertEquals(IntSize(100, 100), size[true])
+                assertEquals(IntSize(200 + 100 * it, 200), size[false])
+                iterations++
+            }
+        }
+    }
+
+    class TestLayoutModifierNode(
+        var lookaheadIntrinsicResult: MutableMap<String, Int>,
+        var intrinsicResult: MutableMap<String, Int>
+    ) : LayoutModifierNode, Modifier.Node() {
+        override fun MeasureScope.measure(
+            measurable: Measurable,
+            constraints: Constraints
+        ): MeasureResult {
+            return measurable.measure(constraints).run {
+                layout(width, height) {
+                    place(0, 0)
+                }
+            }
+        }
+
+        override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+            measurable: IntrinsicMeasurable,
+            width: Int
+        ): Int = measurable.maxIntrinsicHeight(width).also {
+            if (isLookingAhead) {
+                lookaheadIntrinsicResult["maxHeight"] = it
+            } else {
+                intrinsicResult["maxHeight"] = it
+            }
+        }
+
+        override fun IntrinsicMeasureScope.minIntrinsicHeight(
+            measurable: IntrinsicMeasurable,
+            width: Int
+        ): Int = measurable.minIntrinsicHeight(width).also {
+            if (isLookingAhead) {
+                lookaheadIntrinsicResult["minHeight"] = it
+            } else {
+                intrinsicResult["minHeight"] = it
+            }
+        }
+
+        override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+            measurable: IntrinsicMeasurable,
+            height: Int
+        ): Int = measurable.maxIntrinsicWidth(height).also {
+            if (isLookingAhead) {
+                lookaheadIntrinsicResult["maxWidth"] = it
+            } else {
+                intrinsicResult["maxWidth"] = it
+            }
+        }
+
+        override fun IntrinsicMeasureScope.minIntrinsicWidth(
+            measurable: IntrinsicMeasurable,
+            height: Int
+        ): Int = measurable.minIntrinsicWidth(height).also {
+            if (isLookingAhead) {
+                lookaheadIntrinsicResult["minWidth"] = it
+            } else {
+                intrinsicResult["minWidth"] = it
+            }
+        }
+    }
+
+    data class TestElement(
+        val lookaheadIntrinsicResult: MutableMap<String, Int>,
+        val intrinsicResult: MutableMap<String, Int>
+    ) : ModifierNodeElement<TestLayoutModifierNode>() {
+        override fun create(): TestLayoutModifierNode =
+            TestLayoutModifierNode(lookaheadIntrinsicResult, intrinsicResult)
+
+        override fun update(node: TestLayoutModifierNode) {
+            node.lookaheadIntrinsicResult = lookaheadIntrinsicResult
+            node.intrinsicResult = intrinsicResult
+        }
+
+        override fun InspectorInfo.inspectableProperties() {
+            name = "TestElement"
+            properties["lookaheadIntrinsicResult"] = lookaheadIntrinsicResult
+            properties["intrinsicResult"] = intrinsicResult
+        }
+    }
+
+    @Test
+    fun testIsLookingAheadWithIntrinsics() {
+        val lookaheadIntrinsicsResult = mutableMapOf<String, Int>()
+        val intrinsicsResult = mutableMapOf<String, Int>()
+        val modifierList = listOf(
+            Modifier.width(IntrinsicSize.Max),
+            Modifier.width(IntrinsicSize.Min),
+            Modifier.height(IntrinsicSize.Max),
+            Modifier.height(IntrinsicSize.Min),
+        )
+        var iteration by mutableStateOf(0)
+        rule.setContent {
+            LookaheadScope {
+                CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                    Row(Modifier.width(IntrinsicSize.Max)) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .then(modifierList[iteration])
+                                .then(
+                                    TestElement(
+                                        lookaheadIntrinsicsResult, intrinsicsResult
+                                    )
+                                )
+                                .layout { measurable, constraints ->
+                                    measurable
+                                        .measure(constraints)
+                                        .run {
+                                            if (isLookingAhead) {
+                                                layout(200, 250) {
+                                                    place(0, 0)
+                                                }
+                                            } else {
+                                                layout(100, 150) {
+                                                    place(0, 0)
+                                                }
+                                            }
+                                        }
+                                }) {
+                            Box(Modifier.size(10.dp))
+                        }
+                    }
+                }
+            }
+        }
+        repeat(3) {
+            rule.waitForIdle()
+            iteration++
+        }
+        rule.runOnIdle {
+            assertEquals(250, lookaheadIntrinsicsResult["maxHeight"])
+            assertEquals(250, lookaheadIntrinsicsResult["minHeight"])
+            assertEquals(200, lookaheadIntrinsicsResult["maxWidth"])
+            assertEquals(200, lookaheadIntrinsicsResult["minWidth"])
+            assertEquals(150, intrinsicsResult["maxHeight"])
+            assertEquals(150, intrinsicsResult["minHeight"])
+            assertEquals(100, intrinsicsResult["maxWidth"])
+            assertEquals(100, intrinsicsResult["minWidth"])
+        }
+    }
+
     @Test
     fun forceMeasureSubtreeWhileLookaheadMeasureRequestedFromSubtree() {
         var iterations by mutableStateOf(0)
@@ -1960,13 +2176,17 @@
                                     if (iterations % 2 == 0)
                                         Modifier.size(100.dp)
                                     else
-                                        Modifier.intermediateLayout { measurable, constraints ->
-                                            measurable.measure(constraints).run {
-                                                layout(width, height) {
-                                                    place(5, 5)
-                                                }
+                                        Modifier
+                                            .intermediateLayout { measurable, constraints ->
+                                                measurable
+                                                    .measure(constraints)
+                                                    .run {
+                                                        layout(width, height) {
+                                                            place(5, 5)
+                                                        }
+                                                    }
                                             }
-                                        }.padding(5.dp)
+                                            .padding(5.dp)
                                 )
                             }
                         }
@@ -2099,6 +2319,7 @@
         with(scope) {
             this@composed
                 .intermediateLayout { measurable, constraints ->
+                    assertFalse(isLookingAhead)
                     lookaheadSize = this.lookaheadSize
                     measureWithLambdas(
                         prePlacement = {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
index 5575793..e550f57 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidUiDispatcherTest.kt
@@ -32,10 +32,18 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.test.fail
 import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Runnable
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withTimeoutOrNull
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotEquals
@@ -174,4 +182,60 @@
             }
         )
     }
+
+    /**
+     * Test that an AndroidUiDispatcher can be wrapped by another ContinuationInterceptor
+     * without breaking the MonotonicFrameClock's ability to coordinate with its
+     * original dispatcher.
+     *
+     * Construct a situation where the Choreographer contains three frame callbacks:
+     * 1) checkpoint 1
+     * 2) the AndroidUiDispatcher awaiting-frame callback
+     * 3) checkpoint 2
+     * Confirm that a call to withFrameNanos made *after* these three frame callbacks
+     * are enqueued runs *before* checkpoint 2, indicating that it ran with the original
+     * dispatcher's awaiting-frame callback, even though we wrapped the dispatcher.
+     */
+    @Test
+    fun wrappedDispatcherPostsToDispatcherFrameClock() = runBlocking(Dispatchers.Main) {
+        val uiDispatcherContext = AndroidUiDispatcher.Main
+        val uiDispatcher = uiDispatcherContext[ContinuationInterceptor] as CoroutineDispatcher
+        val wrapperDispatcher = object : CoroutineDispatcher() {
+            override fun dispatch(context: CoroutineContext, block: Runnable) {
+                uiDispatcher.dispatch(context, block)
+            }
+        }
+
+        val choreographer = Choreographer.getInstance()
+
+        val expectCount = AtomicInteger(1)
+        fun expect(value: Int) {
+            while (true) {
+                val old = expectCount.get()
+                if (old != value) fail("expected sequence $old but encountered $value")
+                if (expectCount.compareAndSet(value, value + 1)) break
+            }
+        }
+
+        choreographer.postFrameCallback {
+            expect(1)
+        }
+
+        launch(uiDispatcherContext, start = CoroutineStart.UNDISPATCHED) {
+            withFrameNanos {
+                expect(2)
+            }
+        }
+
+        choreographer.postFrameCallback {
+            expect(4)
+        }
+
+        withContext(uiDispatcherContext + wrapperDispatcher) {
+            withFrameNanos {
+                expect(3)
+            }
+            expect(5)
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 5731018..2c1d8dd 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -106,22 +106,6 @@
     }
 
     @Test
-    fun valueSemanticsAreEqual() {
-        assertEquals(
-            Modifier.semantics {
-                text = AnnotatedString("text")
-                contentDescription = "foo"
-                popup()
-            },
-            Modifier.semantics {
-                text = AnnotatedString("text")
-                contentDescription = "foo"
-                popup()
-            }
-        )
-    }
-
-    @Test
     fun isTraversalGroupProperty() {
         rule.setContent {
             Surface(
@@ -1083,10 +1067,9 @@
     properties: SemanticsPropertyReceiver.() -> Unit
 ): CoreSemanticsModifierNode {
     return CoreSemanticsModifierNode(
-        SemanticsConfiguration().apply {
-            isMergingSemanticsOfDescendants = mergeDescendants
-            properties()
-        }
+        mergeDescendants = mergeDescendants,
+        isClearingSemantics = false,
+        properties = properties,
     )
 }
 
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 497be6e..da63070 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
@@ -911,13 +911,20 @@
         }
     }
 
-    private fun convertMeasureSpec(measureSpec: Int): Pair<Int, Int> {
+    @Suppress("NOTHING_TO_INLINE")
+    private inline operator fun ULong.component1() = (this shr 32).toInt()
+    @Suppress("NOTHING_TO_INLINE")
+    private inline operator fun ULong.component2() = (this and 0xFFFFFFFFUL).toInt()
+
+    private fun pack(a: Int, b: Int) = (a.toULong() shl 32 or b.toULong())
+
+    private fun convertMeasureSpec(measureSpec: Int): ULong {
         val mode = MeasureSpec.getMode(measureSpec)
         val size = MeasureSpec.getSize(measureSpec)
         return when (mode) {
-            MeasureSpec.EXACTLY -> size to size
-            MeasureSpec.UNSPECIFIED -> 0 to Constraints.Infinity
-            MeasureSpec.AT_MOST -> 0 to size
+            MeasureSpec.EXACTLY -> pack(size, size)
+            MeasureSpec.UNSPECIFIED -> pack(0, Constraints.Infinity)
+            MeasureSpec.AT_MOST -> pack(0, size)
             else -> throw IllegalStateException()
         }
     }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index ad8183c..3baff06 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -74,7 +74,6 @@
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.semantics.SemanticsProperties
 import androidx.compose.ui.semantics.SemanticsPropertiesAndroid
-import androidx.compose.ui.semantics.collapsedSemantics
 import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.state.ToggleableState
 import androidx.compose.ui.text.AnnotatedString
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
index 5be4139..7dce6f5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiDispatcher.android.kt
@@ -131,7 +131,7 @@
      * A [MonotonicFrameClock] associated with this [AndroidUiDispatcher]'s [choreographer]
      * that may be used to await [Choreographer] frame dispatch.
      */
-    val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer)
+    val frameClock: MonotonicFrameClock = AndroidUiFrameClock(choreographer, this)
 
     override fun dispatch(context: CoroutineContext, block: Runnable) {
         synchronized(lock) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
index fbc7b21..a9fd7ed 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
@@ -21,13 +21,20 @@
 import kotlin.coroutines.ContinuationInterceptor
 import kotlin.coroutines.coroutineContext
 
-class AndroidUiFrameClock(
-    val choreographer: Choreographer
+class AndroidUiFrameClock internal constructor(
+    val choreographer: Choreographer,
+    private val dispatcher: AndroidUiDispatcher?
 ) : androidx.compose.runtime.MonotonicFrameClock {
+
+    constructor(
+        choreographer: Choreographer
+    ) : this(choreographer, null)
+
     override suspend fun <R> withFrameNanos(
         onFrame: (Long) -> R
     ): R {
-        val uiDispatcher = coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
+        val uiDispatcher = dispatcher
+            ?: coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
         return suspendCancellableCoroutine { co ->
             // Important: this callback won't throw, and AndroidUiDispatcher counts on it.
             val callback = Choreographer.FrameCallback { frameTimeNanos ->
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
index 562f991..0e155ef 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/Vector.kt
@@ -19,7 +19,6 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.geometry.Size.Companion.Unspecified
 import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.Brush
@@ -49,12 +48,6 @@
 
 val EmptyPath = emptyList<PathNode>()
 
-inline fun PathData(block: PathBuilder.() -> Unit): List<PathNode> =
-    with(PathBuilder()) {
-        block()
-        getNodes()
-    }
-
 const val DefaultPathName = ""
 const val DefaultStrokeLineWidth = 0.0f
 const val DefaultStrokeLineMiter = 4.0f
@@ -68,15 +61,18 @@
 val DefaultTintColor = Color.Transparent
 val DefaultFillType = PathFillType.NonZero
 
-fun addPathNodes(pathStr: String?): List<PathNode> =
-    if (pathStr == null) {
-        EmptyPath
-    } else {
-        PathParser().parsePathString(pathStr).toNodes()
-    }
+inline fun PathData(block: PathBuilder.() -> Unit) = with(PathBuilder()) {
+    block()
+    getNodes()
+}
+
+fun addPathNodes(pathStr: String?) = if (pathStr == null) {
+    EmptyPath
+} else {
+    PathParser().parsePathString(pathStr).toNodes()
+}
 
 sealed class VNode {
-
     /**
      * Callback invoked whenever the node in the vector tree is modified in a way that would
      * change the output of the Vector
@@ -91,7 +87,6 @@
 }
 
 internal class VectorComponent : VNode() {
-
     val root = GroupComponent().apply {
         pivotX = 0.0f
         pivotY = 0.0f
@@ -111,7 +106,7 @@
         invalidateCallback.invoke()
     }
 
-    private var isDirty: Boolean = true
+    private var isDirty = true
 
     private val cacheDrawScope = DrawCache()
 
@@ -119,7 +114,7 @@
 
     internal var intrinsicColorFilter: ColorFilter? by mutableStateOf(null)
 
-    var viewportWidth: Float = 0f
+    var viewportWidth = 0f
         set(value) {
             if (field != value) {
                 field = value
@@ -127,7 +122,7 @@
             }
         }
 
-    var viewportHeight: Float = 0f
+    var viewportHeight = 0f
         set(value) {
             if (field != value) {
                 field = value
@@ -135,7 +130,7 @@
             }
         }
 
-    private var previousDrawSize: Size = Unspecified
+    private var previousDrawSize = Unspecified
 
     /**
      * Cached lambda used to avoid allocating the lambda on each draw invocation
@@ -178,8 +173,7 @@
 }
 
 internal class PathComponent : VNode() {
-
-    var name: String = DefaultPathName
+    var name = DefaultPathName
         set(value) {
             field = value
             invalidate()
@@ -191,33 +185,33 @@
             invalidate()
         }
 
-    var fillAlpha: Float = 1.0f
+    var fillAlpha = 1.0f
         set(value) {
             field = value
             invalidate()
         }
 
-    var pathData: List<PathNode> = EmptyPath
+    var pathData = EmptyPath
         set(value) {
             field = value
             isPathDirty = true
             invalidate()
         }
 
-    var pathFillType: PathFillType = DefaultFillType
+    var pathFillType = DefaultFillType
         set(value) {
             field = value
             renderPath.fillType = value
             invalidate()
         }
 
-    var strokeAlpha: Float = 1.0f
+    var strokeAlpha = 1.0f
         set(value) {
             field = value
             invalidate()
         }
 
-    var strokeLineWidth: Float = DefaultStrokeLineWidth
+    var strokeLineWidth = DefaultStrokeLineWidth
         set(value) {
             field = value
             invalidate()
@@ -229,28 +223,28 @@
             invalidate()
         }
 
-    var strokeLineCap: StrokeCap = DefaultStrokeLineCap
+    var strokeLineCap = DefaultStrokeLineCap
         set(value) {
             field = value
             isStrokeDirty = true
             invalidate()
         }
 
-    var strokeLineJoin: StrokeJoin = DefaultStrokeLineJoin
+    var strokeLineJoin = DefaultStrokeLineJoin
         set(value) {
             field = value
             isStrokeDirty = true
             invalidate()
         }
 
-    var strokeLineMiter: Float = DefaultStrokeLineMiter
+    var strokeLineMiter = DefaultStrokeLineMiter
         set(value) {
             field = value
             isStrokeDirty = true
             invalidate()
         }
 
-    var trimPathStart: Float = DefaultTrimPathStart
+    var trimPathStart = DefaultTrimPathStart
         set(value) {
             if (field != value) {
                 field = value
@@ -259,7 +253,7 @@
             }
         }
 
-    var trimPathEnd: Float = DefaultTrimPathEnd
+    var trimPathEnd = DefaultTrimPathEnd
         set(value) {
             if (field != value) {
                 field = value
@@ -268,7 +262,7 @@
             }
         }
 
-    var trimPathOffset: Float = DefaultTrimPathOffset
+    var trimPathOffset = DefaultTrimPathOffset
         set(value) {
             if (field != value) {
                 field = value
@@ -279,13 +273,12 @@
 
     private var isPathDirty = true
     private var isStrokeDirty = true
-    private var isTrimPathDirty = true
+    private var isTrimPathDirty = false
 
     private var strokeStyle: Stroke? = null
 
     private val path = Path()
-
-    private val renderPath = Path()
+    private var renderPath = path
 
     private val pathMeasure: PathMeasure by lazy(LazyThreadSafetyMode.NONE) { PathMeasure() }
 
@@ -296,14 +289,18 @@
     }
 
     private fun updateRenderPath() {
-        // Rewind unsets the filltype so reset it here
-        val fillType = renderPath.fillType
-        renderPath.rewind()
-        renderPath.fillType = fillType
-
         if (trimPathStart == DefaultTrimPathStart && trimPathEnd == DefaultTrimPathEnd) {
-            renderPath.addPath(path)
+            renderPath = path
         } else {
+            if (renderPath == path) {
+                renderPath = Path()
+            } else {
+                // Rewind unsets the fill type so reset it here
+                val fillType = renderPath.fillType
+                renderPath.rewind()
+                renderPath.fillType = fillType
+            }
+
             pathMeasure.setPath(path, false)
             val length = pathMeasure.length
             val start = ((trimPathStart + trimPathOffset) % 1f) * length
@@ -339,18 +336,15 @@
         }
     }
 
-    override fun toString(): String {
-        return path.toString()
-    }
+    override fun toString() = path.toString()
 }
 
 internal class GroupComponent : VNode() {
-
     private var groupMatrix: Matrix? = null
 
     private val children = mutableListOf<VNode>()
 
-    var clipPathData: List<PathNode> = EmptyPath
+    var clipPathData = EmptyPath
         set(value) {
             field = value
             isClipPathDirty = true
@@ -387,61 +381,64 @@
 
     // If the name changes we should re-draw as individual nodes could
     // be modified based off of this name parameter.
-    var name: String = DefaultGroupName
+    var name = DefaultGroupName
         set(value) {
             field = value
             invalidate()
         }
 
-    var rotation: Float = DefaultRotation
+    var rotation = DefaultRotation
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var pivotX: Float = DefaultPivotX
+    var pivotX = DefaultPivotX
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var pivotY: Float = DefaultPivotY
+    var pivotY = DefaultPivotY
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var scaleX: Float = DefaultScaleX
+    var scaleX = DefaultScaleX
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var scaleY: Float = DefaultScaleY
+    var scaleY = DefaultScaleY
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var translationX: Float = DefaultTranslationX
+    var translationX = DefaultTranslationX
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
-    var translationY: Float = DefaultTranslationY
+    var translationY = DefaultTranslationY
         set(value) {
             field = value
             isMatrixDirty = true
             invalidate()
         }
 
+    val numChildren: Int
+        get() = children.size
+
     private var isMatrixDirty = true
 
     private fun updateMatrix() {
@@ -528,9 +525,6 @@
         }
     }
 
-    val numChildren: Int
-        get() = children.size
-
     override fun toString(): String {
         val sb = StringBuilder().append("VGroup: ").append(name)
         children.fastForEach { node ->
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 9a7ddac..abd9b6a 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
@@ -376,12 +376,21 @@
 
         @OptIn(ExperimentalComposeUiApi::class)
         for ((key, change) in changes) {
-            // Filter for changes that are associated with pointer ids that are relevant to this
-            // node
-            if (key in pointerIds) {
+            val keyValue = key.value
+
+            // Using for (key in pointerIds) causes key to be boxed and create allocations
+            var keyInPointerIds = false
+            for (i in 0..pointerIds.lastIndex) {
+                if (pointerIds[i].value == keyValue) {
+                    keyInPointerIds = true
+                    break
+                }
+            }
+
+            if (keyInPointerIds) {
                 // And translate their position relative to the parent coordinates, to give us a
                 // change local to the PointerInputFilter's coordinates
-                val historical = mutableListOf<HistoricalChange>()
+                val historical = ArrayList<HistoricalChange>(change.historical.size)
                 change.historical.fastForEach {
                     historical.add(
                         HistoricalChange(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index a3d34a1..eab488c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -497,7 +497,7 @@
     override fun resetPointerInputHandler() {
         val localJob = pointerInputJob
         if (localJob != null) {
-            localJob.cancel(CancellationException())
+            localJob.cancel()
             pointerInputJob = null
         }
     }
@@ -602,9 +602,6 @@
         dispatchPointerEvent(cancelEvent, PointerEventPass.Final)
 
         lastPointerEvent = null
-
-        // Cancels existing coroutine (Job) handling events.
-        resetPointerInputHandler()
     }
 
     override suspend fun <R> awaitPointerEventScope(
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
index 9b4c8f9..e0ad52d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntermediateLayoutModifierNode.kt
@@ -317,6 +317,10 @@
             }
         }
 
+        // Intermediate layout pass is post-lookahead. Therefore return false here.
+        override val isLookingAhead: Boolean
+            get() = false
+
         override val layoutDirection: LayoutDirection
             get() = coordinator!!.layoutDirection
         override val density: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
index 5c29e6c..657e31d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasureScope.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.layout
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.LayoutDirection
 
@@ -28,4 +29,17 @@
      * to measure their children.
      */
     val layoutDirection: LayoutDirection
+
+    /**
+     * This indicates whether the ongoing measurement is for lookahead pass.
+     * [IntrinsicMeasureScope] implementations, especially [MeasureScope] implementations should
+     * override this flag to reflect whether the measurement is intended for lookahead pass.
+     *
+     * @sample androidx.compose.ui.samples.animateContentSizeAfterLookaheadPass
+     */
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:ExperimentalComposeUiApi
+    @ExperimentalComposeUiApi
+    val isLookingAhead: Boolean
+        get() = false
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index 2b45ead..813a516 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -32,7 +32,6 @@
 import androidx.compose.ui.node.ComposeUiNode
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -64,7 +63,8 @@
  */
 @Suppress("ComposableLambdaParameterPosition")
 @UiComposable
-@Composable inline fun Layout(
+@Composable
+inline fun Layout(
     content: @Composable @UiComposable () -> Unit,
     modifier: Modifier = Modifier,
     measurePolicy: MeasurePolicy
@@ -322,6 +322,6 @@
  * call.
  */
 internal class IntrinsicsMeasureScope(
-    density: Density,
-    override val layoutDirection: LayoutDirection
-) : MeasureScope, Density by density
+    intrinsicMeasureScope: IntrinsicMeasureScope,
+    override val layoutDirection: LayoutDirection,
+) : MeasureScope, IntrinsicMeasureScope by intrinsicMeasureScope
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 6ebfd55..d95d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -1000,6 +1000,9 @@
         override var layoutDirection: LayoutDirection = LayoutDirection.Rtl
         override var density: Float = 0f
         override var fontScale: Float = 0f
+        override val isLookingAhead: Boolean
+            get() = root.layoutState == LayoutState.LookaheadLayingOut ||
+                root.layoutState == LayoutState.LookaheadMeasuring
 
         override fun subcompose(slotId: Any?, content: @Composable () -> Unit) =
             this@LayoutNodeSubcompositionsState.subcompose(slotId, content)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index 88b94ea..533ae5f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -59,6 +59,7 @@
 import androidx.compose.ui.modifier.modifierLocalMapOf
 import androidx.compose.ui.semantics.SemanticsConfiguration
 import androidx.compose.ui.semantics.SemanticsModifier
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
@@ -352,8 +353,11 @@
         }
     }
 
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = (element as SemanticsModifier).semanticsConfiguration
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        val config = (element as SemanticsModifier).semanticsConfiguration
+        val toMergeInto = (this as SemanticsConfiguration)
+        toMergeInto.collapsePeer(config)
+    }
 
     override fun onPointerEvent(
         pointerEvent: PointerEvent,
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index e42df1f..aa215e9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.simpleIdentityToString
+import androidx.compose.ui.semantics.SemanticsConfiguration
 import androidx.compose.ui.semantics.generateSemanticsId
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -65,6 +66,8 @@
  */
 private const val DebugChanges = false
 
+private val DefaultDensity = Density(1f)
+
 /**
  * An element in the layout hierarchy, built with compose UI.
  */
@@ -392,6 +395,37 @@
         invalidateMeasurements()
     }
 
+    private var _collapsedSemantics: SemanticsConfiguration? = null
+    internal fun invalidateSemantics() {
+        _collapsedSemantics = null
+        // TODO(lmr): this ends up scheduling work that diffs the entire tree, but we should
+        //  eventually move to marking just this node as invalidated since we are invalidating
+        //  on a per-node level. This should preserve current behavior for now.
+        requireOwner().onSemanticsChange()
+    }
+    internal val collapsedSemantics: SemanticsConfiguration?
+        get() {
+            if (!nodes.has(Nodes.Semantics) || _collapsedSemantics != null) {
+                return _collapsedSemantics
+            }
+
+            var config = SemanticsConfiguration()
+            requireOwner().snapshotObserver.observeSemanticsReads(this) {
+                nodes.tailToHead(Nodes.Semantics) {
+                    if (it.shouldClearDescendantSemantics) {
+                        config = SemanticsConfiguration()
+                        config.isClearingSemantics = true
+                    }
+                    if (it.shouldMergeDescendantSemantics) {
+                        config.isMergingSemanticsOfDescendants = true
+                    }
+                    with(config) { with(it) { applySemantics() } }
+                }
+            }
+            _collapsedSemantics = config
+            return config
+        }
+
     /**
      * Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached.
      * [owner] must match its [parent].[owner].
@@ -419,7 +453,7 @@
         this.owner = owner
         this.depth = (parent?.depth ?: -1) + 1
         if (nodes.has(Nodes.Semantics)) {
-            owner.onSemanticsChange()
+            invalidateSemantics()
         }
         owner.onAttach(this)
 
@@ -469,7 +503,7 @@
         onDetach?.invoke(owner)
 
         if (nodes.has(Nodes.Semantics)) {
-            owner.onSemanticsChange()
+            invalidateSemantics()
         }
         nodes.detach()
         owner.onDetach(this)
@@ -595,7 +629,7 @@
     /**
      * The screen density to be used by this layout.
      */
-    override var density: Density = Density(1f)
+    override var density: Density = DefaultDensity
         set(value) {
             if (field != value) {
                 field = value
@@ -1139,10 +1173,8 @@
     fun invalidateSubtree(isRootOfInvalidation: Boolean = true) {
         if (isRootOfInvalidation) {
             parent?.invalidateLayer()
-            // Invalidate semantics. We can do this once because there isn't a node-by-node
-            // invalidation mechanism.
-            requireOwner().onSemanticsChange()
         }
+        invalidateSemantics()
         requestRemeasure()
         nodes.headToTail(Nodes.Layout) {
             it.requireCoordinator(Nodes.Layout).layer?.invalidate()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index b34b523..ba59c04 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.node
 
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.LayoutCoordinates
@@ -76,6 +77,10 @@
             alignmentLinesOwner.parentAlignmentLinesOwner?.alignmentLines?.onAlignmentsChanged()
         }
     }
+
+    @OptIn(ExperimentalComposeUiApi::class)
+    override val isLookingAhead: Boolean
+        get() = false
 }
 
 internal abstract class LookaheadDelegate(
@@ -91,6 +96,8 @@
         get() = _measureResult ?: error(
             "LookaheadDelegate has not been measured yet when measureResult is requested."
         )
+    override val isLookingAhead: Boolean
+        get() = true
     override val layoutDirection: LayoutDirection
         get() = coordinator.layoutDirection
     override val density: Float
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 2b2847f..9043a07 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -743,7 +743,8 @@
 private fun Modifier.fillVector(
     result: MutableVector<Modifier.Element>
 ): MutableVector<Modifier.Element> {
-    val stack = MutableVector<Modifier>(result.size).also { it.add(this) }
+    val capacity = result.size.coerceAtLeast(16)
+    val stack = MutableVector<Modifier>(capacity).also { it.add(this) }
     while (stack.isNotEmpty()) {
         when (val next = stack.removeAt(stack.size - 1)) {
             is CombinedModifier -> {
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 b0abc9f..6519bf8 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
@@ -39,7 +39,6 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.findRootCoordinates
 import androidx.compose.ui.layout.positionInRoot
-import androidx.compose.ui.semantics.collapsedSemantics
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
index 22391a49..689bc64 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnerSnapshotObserver.kt
@@ -39,6 +39,12 @@
         }
     }
 
+    private val onCommitAffectingSemantics: (LayoutNode) -> Unit = { layoutNode ->
+        if (layoutNode.isValidOwnerScope) {
+            layoutNode.invalidateSemantics()
+        }
+    }
+
     private val onCommitAffectingLayout: (LayoutNode) -> Unit = { layoutNode ->
         if (layoutNode.isValidOwnerScope) {
             layoutNode.requestRelayout()
@@ -108,6 +114,13 @@
         }
     }
 
+    internal fun observeSemanticsReads(
+        node: LayoutNode,
+        block: () -> Unit
+    ) {
+        observeReads(node, onCommitAffectingSemantics, block)
+    }
+
     /**
      * Observe snapshot reads for any target, allowing consumers to determine how to respond
      * to state changes.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
index 8c80e15..1cc3317 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/SemanticsModifierNode.kt
@@ -21,6 +21,10 @@
 import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.semantics.SemanticsActions
 import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.semantics.SemanticsOwner
+import androidx.compose.ui.semantics.SemanticsPropertyKey
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.semantics.getOrNull
 
 /**
@@ -32,31 +36,64 @@
  */
 interface SemanticsModifierNode : DelegatableNode {
     /**
-     * The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
-     * such as (label -> "buttonName").
+     * Clears the semantics of all the descendant nodes and sets new semantics.
+     *
+     * In the merged semantics tree, this clears the semantic information provided
+     * by the node's descendants (but not those of the layout node itself, if any)
+     * In the unmerged tree, the semantics node is marked with
+     * "[SemanticsConfiguration.isClearingSemantics]", but nothing is actually cleared.
+     *
+     * Compose's default semantics provide baseline usability for screen-readers, but this can be
+     * used to provide a more polished screen-reader experience: for example, clearing the
+     * semantics of a group of tiny buttons, and setting equivalent actions on the card
+     * containing them.
      */
-    val semanticsConfiguration: SemanticsConfiguration
+    @get:Suppress("GetterSetterNames")
+    val shouldClearDescendantSemantics: Boolean
+        get() = false
+
+    /**
+     * Whether the semantic information provided by this node and
+     * its descendants should be treated as one logical entity.
+     * Most commonly set on screen-reader-focusable items such as buttons or form fields.
+     * In the merged semantics tree, all descendant nodes (except those themselves marked
+     * [shouldMergeDescendantSemantics]) will disappear from the tree, and their properties
+     * will get merged into the parent's configuration (using a merging algorithm that varies based
+     * on the type of property -- for example, text properties will get concatenated, separated
+     * by commas). In the unmerged semantics tree, the node is simply marked with
+     * [SemanticsConfiguration.isMergingSemanticsOfDescendants].
+     */
+    @get:Suppress("GetterSetterNames")
+    val shouldMergeDescendantSemantics: Boolean
+        get() = false
+
+    /**
+     * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
+     *
+     * The [SemanticsPropertyReceiver] provides "key = value"-style setters for any
+     * [SemanticsPropertyKey]. Additionally, chaining multiple semantics modifiers is
+     * also a supported style.
+     *
+     * The resulting semantics produce two [SemanticsNode] trees:
+     *
+     * The "unmerged tree" rooted at [SemanticsOwner.unmergedRootSemanticsNode] has one
+     * [SemanticsNode] per layout node which has any [SemanticsModifierNode] on it.  This
+     * [SemanticsNode] contains all the properties set in all the [SemanticsModifierNode]s on
+     * that node.
+     *
+     * The "merged tree" rooted at [SemanticsOwner.rootSemanticsNode] has equal-or-fewer nodes: it
+     * simplifies the structure based on [shouldMergeDescendantSemantics] and
+     * [shouldClearDescendantSemantics].  For most purposes (especially accessibility, or the
+     * testing of accessibility), the merged semantics tree should be used.
+     */
+    fun SemanticsPropertyReceiver.applySemantics()
 }
 
-fun SemanticsModifierNode.invalidateSemantics() = requireOwner().onSemanticsChange()
-
-internal val SemanticsModifierNode.useMinimumTouchTarget: Boolean
-    get() = semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
+fun SemanticsModifierNode.invalidateSemantics() = requireLayoutNode().invalidateSemantics()
 
 internal val SemanticsConfiguration.useMinimumTouchTarget: Boolean
     get() = getOrNull(SemanticsActions.OnClick) != null
 
-internal fun SemanticsModifierNode.touchBoundsInRoot(): Rect {
-    if (!node.isAttached) {
-        return Rect.Zero
-    }
-    if (!useMinimumTouchTarget) {
-        return requireCoordinator(Nodes.Semantics).boundsInRoot()
-    }
-
-    return requireCoordinator(Nodes.Semantics).touchBoundsInRoot()
-}
-
 internal fun Modifier.Node.touchBoundsInRoot(useMinimumTouchTarget: Boolean): Rect {
     if (!node.isAttached) {
         return Rect.Zero
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
index 0ca7a3dd..922af3f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsConfiguration.kt
@@ -59,7 +59,15 @@
     }
 
     override fun <T> set(key: SemanticsPropertyKey<T>, value: T) {
-        props[key] = value
+        if (value is AccessibilityAction<*> && contains(key)) {
+            val prev = props[key] as AccessibilityAction<*>
+            props[key] = AccessibilityAction(
+                value.label ?: prev.label,
+                value.action ?: prev.action
+            )
+        } else {
+            props[key] = value
+        }
     }
 
     operator fun <T> contains(key: SemanticsPropertyKey<T>): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 723d099..688113e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -47,16 +47,10 @@
 }
 
 internal object EmptySemanticsElement :
-    ModifierNodeElement<CoreSemanticsModifierNode>() {
+    ModifierNodeElement<EmptySemanticsModifier>() {
+    override fun create() = EmptySemanticsModifier()
 
-    private val semanticsConfiguration = SemanticsConfiguration().apply {
-        isMergingSemanticsOfDescendants = false
-        isClearingSemantics = false
-    }
-
-    override fun create() = CoreSemanticsModifierNode(semanticsConfiguration)
-
-    override fun update(node: CoreSemanticsModifierNode) {}
+    override fun update(node: EmptySemanticsModifier) {}
 
     override fun InspectorInfo.inspectableProperties() {
         // Nothing to inspect.
@@ -67,8 +61,22 @@
 }
 
 internal class CoreSemanticsModifierNode(
-    override var semanticsConfiguration: SemanticsConfiguration
-) : Modifier.Node(), SemanticsModifierNode
+    var mergeDescendants: Boolean,
+    var isClearingSemantics: Boolean,
+    var properties: SemanticsPropertyReceiver.() -> Unit
+) : Modifier.Node(), SemanticsModifierNode {
+    override val shouldClearDescendantSemantics: Boolean
+        get() = isClearingSemantics
+    override val shouldMergeDescendantSemantics: Boolean
+        get() = mergeDescendants
+    override fun SemanticsPropertyReceiver.applySemantics() {
+        properties()
+    }
+}
+
+internal class EmptySemanticsModifier : Modifier.Node(), SemanticsModifierNode {
+    override fun SemanticsPropertyReceiver.applySemantics() {}
+}
 
 /**
  * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc.
@@ -110,30 +118,33 @@
 
 // Implement SemanticsModifier to allow tooling to inspect the semantics configuration
 internal data class AppendedSemanticsElement(
-    override val semanticsConfiguration: SemanticsConfiguration
+    val mergeDescendants: Boolean,
+    val properties: (SemanticsPropertyReceiver.() -> Unit)
 ) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
 
-    constructor(
-        mergeDescendants: Boolean,
-        properties: (SemanticsPropertyReceiver.() -> Unit)
-    ) : this(
-        SemanticsConfiguration().apply {
+    // This should only ever be called by layout inspector
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() = SemanticsConfiguration().apply {
             isMergingSemanticsOfDescendants = mergeDescendants
             properties()
         }
-    )
 
     override fun create(): CoreSemanticsModifierNode {
-        return CoreSemanticsModifierNode(semanticsConfiguration)
+        return CoreSemanticsModifierNode(
+            mergeDescendants = mergeDescendants,
+            isClearingSemantics = false,
+            properties = properties
+        )
     }
 
     override fun update(node: CoreSemanticsModifierNode) {
-        node.semanticsConfiguration = semanticsConfiguration
+        node.mergeDescendants = mergeDescendants
+        node.properties = properties
     }
 
     override fun InspectorInfo.inspectableProperties() {
         name = "semantics"
-        properties["mergeDescendants"] = semanticsConfiguration.isMergingSemanticsOfDescendants
+        properties["mergeDescendants"] = mergeDescendants
         addSemanticsPropertiesFrom(semanticsConfiguration)
     }
 }
@@ -159,24 +170,27 @@
 
 // Implement SemanticsModifier to allow tooling to inspect the semantics configuration
 internal data class ClearAndSetSemanticsElement(
-    override val semanticsConfiguration: SemanticsConfiguration
+    val properties: SemanticsPropertyReceiver.() -> Unit
 ) : ModifierNodeElement<CoreSemanticsModifierNode>(), SemanticsModifier {
 
-    init {
-        semanticsConfiguration.isMergingSemanticsOfDescendants = false
-        semanticsConfiguration.isClearingSemantics = true
-    }
-
-    constructor(properties: (SemanticsPropertyReceiver.() -> Unit)) : this(
-        SemanticsConfiguration().apply(properties)
-    )
+    // This should only ever be called by layout inspector
+    override val semanticsConfiguration: SemanticsConfiguration
+        get() = SemanticsConfiguration().apply {
+            isMergingSemanticsOfDescendants = false
+            isClearingSemantics = true
+            properties()
+        }
 
     override fun create(): CoreSemanticsModifierNode {
-        return CoreSemanticsModifierNode(semanticsConfiguration)
+        return CoreSemanticsModifierNode(
+            mergeDescendants = false,
+            isClearingSemantics = true,
+            properties = properties
+        )
     }
 
     override fun update(node: CoreSemanticsModifierNode) {
-        node.semanticsConfiguration = semanticsConfiguration
+        node.properties = properties
     }
 
     override fun InspectorInfo.inspectableProperties() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 41cdc4f..bb78b7e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -402,7 +402,9 @@
         }
         val fakeNode = SemanticsNode(
             outerSemanticsNode = object : SemanticsModifierNode, Modifier.Node() {
-                override val semanticsConfiguration = configuration
+                override fun SemanticsPropertyReceiver.applySemantics() {
+                    properties()
+                }
             },
             mergingEnabled = false,
             layoutNode = LayoutNode(
@@ -427,25 +429,9 @@
     }
 }
 
-internal val LayoutNode.collapsedSemantics: SemanticsConfiguration?
-    get() {
-        var result: SemanticsConfiguration? = null
-        nodes.tailToHead(Nodes.Semantics) {
-            val current = result
-            if (current == null || it.semanticsConfiguration.isClearingSemantics) {
-                result = it.semanticsConfiguration
-            } else {
-                result = it.semanticsConfiguration.copy().also {
-                    it.collapsePeer(current)
-                }
-            }
-        }
-        return result
-    }
-
 internal val LayoutNode.outerMergingSemantics: SemanticsModifierNode?
     get() = nodes.firstFromHead(Nodes.Semantics) {
-        it.semanticsConfiguration.isMergingSemanticsOfDescendants
+        it.shouldMergeDescendantSemantics
     }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
index a5d43a5..7a39405 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsOwner.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.node.LayoutNode
+import androidx.compose.ui.node.Nodes
 import androidx.compose.ui.util.fastForEach
 
 /**
@@ -37,7 +38,16 @@
 
     val unmergedRootSemanticsNode: SemanticsNode
         get() {
-            return SemanticsNode(rootNode, mergingEnabled = false)
+            return SemanticsNode(
+                outerSemanticsNode = rootNode.nodes.head(Nodes.Semantics)!!.node,
+                layoutNode = rootNode,
+                mergingEnabled = false,
+                // Forcing an empty SemanticsConfiguration here since the root node will always
+                // have an empty config, but if we don't pass this in explicitly here it will try
+                // to call `rootNode.collapsedSemantics` which will fail because the LayoutNode
+                // is not yet attached when this getter is first called.
+                unmergedConfig = SemanticsConfiguration()
+            )
         }
 }
 
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index 2fc9795..698d042 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.semantics.SemanticsConfiguration
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.unit.Constraints
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -830,8 +830,7 @@
 }
 
 class SemanticsMod(val id: String = "") : SemanticsModifierNode, Modifier.Node() {
-    override val semanticsConfiguration: SemanticsConfiguration
-        get() = SemanticsConfiguration()
+    override fun SemanticsPropertyReceiver.applySemantics() { }
     override fun toString(): String {
         return "SemanticsMod($id)"
     }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index 586d600..df004df 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -61,6 +61,7 @@
 import androidx.compose.ui.platform.invertTo
 import androidx.compose.ui.semantics.SemanticsConfiguration
 import androidx.compose.ui.semantics.SemanticsModifier
+import androidx.compose.ui.semantics.SemanticsPropertyReceiver
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.font.FontFamily
@@ -1382,12 +1383,11 @@
 
     @Test
     fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
-        val semanticsConfiguration = SemanticsConfiguration()
         val semanticsNode1 = object : SemanticsModifierNode, Modifier.Node() {
-            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+            override fun SemanticsPropertyReceiver.applySemantics() { }
         }
         val semanticsNode2 = object : SemanticsModifierNode, Modifier.Node() {
-            override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
+            override fun SemanticsPropertyReceiver.applySemantics() { }
         }
         data class TestSemanticsElement(
             private val node: Modifier.Node
diff --git a/constraintlayout/constraintlayout-compose/build.gradle b/constraintlayout/constraintlayout-compose/build.gradle
index 639aa60..51a3271 100644
--- a/constraintlayout/constraintlayout-compose/build.gradle
+++ b/constraintlayout/constraintlayout-compose/build.gradle
@@ -14,9 +14,7 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
-import androidx.build.Publish
 
 plugins {
     id("AndroidXPlugin")
@@ -24,77 +22,49 @@
     id("AndroidXComposePlugin")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+androidXMultiplatform {
+    android()
 
-dependencies {
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        implementation(project(":compose:ui:ui"))
-        implementation(project(":compose:ui:ui-unit"))
-        implementation(project(":compose:ui:ui-util"))
-        implementation(project(":compose:foundation:foundation"))
-        implementation(project(":compose:foundation:foundation-layout"))
-
-        implementation(project(":constraintlayout:constraintlayout-core"))
-
-        androidTestImplementation(project(":compose:material:material"))
-        androidTestImplementation(project(":compose:ui:ui-test"))
-        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-        androidTestImplementation(project(":compose:ui:ui-test-manifest"))
-        androidTestImplementation(project(":activity:activity"))
-
-        androidTestImplementation(libs.kotlinTest)
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-
-        lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
-//                implementation(libs.kotlinStdlibCommon)
-
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(project(":compose:ui:ui"))
-                implementation("androidx.compose.ui:ui-unit:1.4.0-beta02")
-                implementation("androidx.compose.ui:ui-util:1.4.0-beta02")
-                implementation("androidx.compose.foundation:foundation:1.4.0-beta02")
-                implementation("androidx.compose.foundation:foundation-layout:1.4.0-beta02")
+                implementation(project(":compose:ui:ui-unit"))
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":compose:foundation:foundation"))
+                implementation(project(":compose:foundation:foundation-layout"))
                 implementation(project(":constraintlayout:constraintlayout-core"))
-
             }
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependencies {
+            }
+        }
+
+
+        androidMain {
+            dependsOn(commonMain)
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
                 implementation("androidx.core:core-ktx:1.5.0")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        jvmTest {
+            dependencies {
             }
+        }
 
-            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
-            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
-            //  level dependencies block instead:
-            //  `dependencies { testImplementation(libs.robolectric) }`
-            androidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-            }
-
-            androidAndroidTest.dependencies {
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.kotlinTest)
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
@@ -107,9 +77,27 @@
                 implementation(project(":compose:test-utils"))
             }
         }
+
+
+        // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+        //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+        //  level dependencies block instead:
+        //  `dependencies { testImplementation(libs.robolectric) }`
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+            }
+        }
     }
 }
 
+dependencies {
+    lintPublish(project(":constraintlayout:constraintlayout-compose-lint"))
+}
+
 androidx {
     name = "Android ConstraintLayout Compose Library"
     type = LibraryType.PUBLISHED_LIBRARY
diff --git a/core/core-telecom/build.gradle b/core/core-telecom/build.gradle
index 595ce5c..2e308881 100644
--- a/core/core-telecom/build.gradle
+++ b/core/core-telecom/build.gradle
@@ -31,6 +31,7 @@
     implementation(libs.kotlinCoroutinesCore)
     implementation(libs.kotlinCoroutinesGuava)
     // Test dependencies
+    androidTestImplementation(project(":internal-testutils-common"))
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
new file mode 100644
index 0000000..ee9f215
--- /dev/null
+++ b/core/core-telecom/src/androidTest/java/androidx/core/telecom/JetpackConnectionServiceTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.core.telecom
+
+import android.content.Context
+import android.net.Uri
+import android.os.Build.VERSION_CODES
+import android.telecom.Connection
+import android.telecom.ConnectionRequest
+import androidx.annotation.RequiresApi
+import androidx.core.telecom.internal.CallChannels
+import androidx.core.telecom.internal.JetpackConnectionService
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import androidx.testutils.TestExecutor
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.asCoroutineDispatcher
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@RequiresApi(VERSION_CODES.O)
+@SdkSuppress(minSdkVersion = VERSION_CODES.O /* api=26 */)
+class JetpackConnectionServiceTest {
+
+    private val mContext: Context = ApplicationProvider.getApplicationContext()
+    private val mCallsManager = CallsManager(mContext)
+    private val mConnectionService = mCallsManager.mConnectionService
+    private val mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+    private val workerExecutor = TestExecutor()
+    private val workerContext: CoroutineContext = workerExecutor.asCoroutineDispatcher()
+    private val callChannels = CallChannels()
+    private val TEST_CALL_ATTRIB_NAME = "Elon Musk"
+    private val TEST_CALL_ATTRIB_NUMBER = Uri.parse("tel:6506959001")
+
+    @After
+    fun onDestroy() {
+        callChannels.closeAllChannels()
+        JetpackConnectionService.mPendingConnectionRequests.clear()
+    }
+
+    /**
+     * Ensure an outgoing Connection object has its properties set before sending it off to the
+     * platform.  The properties should reflect everything that is set in CallAttributes.
+     */
+    @SmallTest
+    @Test
+    fun testConnectionServicePropertiesAreSet_outgoingCall() {
+        // create the CallAttributes
+        val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_OUTGOING)
+        // simulate the connection being created
+        val connection = mConnectionService.createSelfManagedConnection(
+            createConnectionRequest(attributes),
+            CallAttributesCompat.DIRECTION_OUTGOING
+        )
+        // verify / assert connection properties
+        verifyConnectionPropertiesBasics(connection)
+        assertEquals(Connection.STATE_DIALING, connection!!.state)
+    }
+
+    /**
+     * Ensure an incoming Connection object has its properties set before sending it off to the
+     * platform.  The properties should reflect everything that is set in CallAttributes.
+     */
+    @SmallTest
+    @Test
+    fun testConnectionServicePropertiesAreSet_incomingCall() {
+        // create the CallAttributes
+        val attributes = createCallAttributes(CallAttributesCompat.DIRECTION_INCOMING)
+        // simulate the connection being created
+        val connection = mConnectionService.createSelfManagedConnection(
+            createConnectionRequest(attributes),
+            CallAttributesCompat.DIRECTION_INCOMING
+        )
+        // verify / assert connection properties
+        verifyConnectionPropertiesBasics(connection)
+        assertEquals(Connection.STATE_RINGING, connection!!.state)
+    }
+
+    private fun verifyConnectionPropertiesBasics(connection: Connection?) {
+        // assert it's not null
+        assertNotNull(connection)
+        // unwrap for testing
+        val unwrappedConnection = connection!!
+        // assert all the properties are the same
+        assertEquals(TEST_CALL_ATTRIB_NAME, unwrappedConnection.callerDisplayName)
+        assertEquals(TEST_CALL_ATTRIB_NUMBER, unwrappedConnection.address)
+        assertEquals(
+            Connection.CAPABILITY_HOLD,
+            unwrappedConnection.connectionCapabilities
+                and Connection.CAPABILITY_HOLD
+        )
+        assertEquals(
+            Connection.CAPABILITY_SUPPORT_HOLD,
+            unwrappedConnection.connectionCapabilities
+                and Connection.CAPABILITY_SUPPORT_HOLD
+        )
+        assertEquals(0, JetpackConnectionService.mPendingConnectionRequests.size)
+    }
+
+    private fun createCallAttributes(
+        callDirection: Int,
+        callType: Int? = CallAttributesCompat.CALL_TYPE_AUDIO_CALL
+    ): CallAttributesCompat {
+
+        val attributes: CallAttributesCompat = if (callType != null) {
+            CallAttributesCompat(
+                TEST_CALL_ATTRIB_NAME,
+                TEST_CALL_ATTRIB_NUMBER,
+                callDirection, callType
+            )
+        } else {
+            CallAttributesCompat(
+                TEST_CALL_ATTRIB_NAME,
+                TEST_CALL_ATTRIB_NUMBER,
+                callDirection
+            )
+        }
+
+        attributes.mHandle = mCallsManager.getPhoneAccountHandleForPackage()
+
+        return attributes
+    }
+
+    private fun createConnectionRequest(callAttributesCompat: CallAttributesCompat):
+        ConnectionRequest {
+        // wrap in PendingRequest
+        val pr = JetpackConnectionService.PendingConnectionRequest(
+            callAttributesCompat, callChannels, workerContext, null
+        )
+        // add to the list of pendingRequests
+        JetpackConnectionService.mPendingConnectionRequests.add(pr)
+        // create a ConnectionRequest
+        return ConnectionRequest(mHandle, TEST_CALL_ATTRIB_NUMBER, null)
+    }
+}
\ No newline at end of file
diff --git a/core/core-telecom/src/main/AndroidManifest.xml b/core/core-telecom/src/main/AndroidManifest.xml
index c904673..8ea6753 100644
--- a/core/core-telecom/src/main/AndroidManifest.xml
+++ b/core/core-telecom/src/main/AndroidManifest.xml
@@ -19,7 +19,6 @@
 
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
     <uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
-    <uses-permission android:name="android.permission.CALL_PHONE" />
 
     <application>
         <service
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
index 86cb668..e3d3610 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallAttributesCompat.kt
@@ -158,7 +158,7 @@
         }
     }
 
-    private fun hasSupportsSetInactiveCapability(): Boolean {
+    internal fun hasSupportsSetInactiveCapability(): Boolean {
         return Utils.hasCapability(SUPPORTS_SET_INACTIVE, callCapabilities)
     }
 
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
index 61e13a3..25e9287 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/CallsManager.kt
@@ -60,7 +60,7 @@
     private var mPhoneAccount: PhoneAccount? = null
     private val mTelecomManager: TelecomManager =
         mContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
-    private val mConnectionService: JetpackConnectionService = JetpackConnectionService()
+    internal val mConnectionService: JetpackConnectionService = JetpackConnectionService()
 
     // A single declared constant for a direct [Executor], since the coroutines primitives we invoke
     // from the associated callbacks will perform their own dispatch as needed.
diff --git a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
index 628c2c0..d219910 100644
--- a/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
+++ b/core/core-telecom/src/main/java/androidx/core/telecom/internal/JetpackConnectionService.kt
@@ -22,6 +22,7 @@
 import android.telecom.ConnectionService
 import android.telecom.PhoneAccountHandle
 import android.telecom.TelecomManager
+import android.telecom.VideoProfile
 import android.util.Log
 import androidx.annotation.RequiresApi
 import androidx.annotation.RequiresPermission
@@ -47,7 +48,7 @@
         val callAttributes: CallAttributesCompat,
         val callChannel: CallChannels,
         val coroutineContext: CoroutineContext,
-        val completableDeferred: CompletableDeferred<CallSessionLegacy>
+        val completableDeferred: CompletableDeferred<CallSessionLegacy>?
     )
 
     companion object {
@@ -89,7 +90,7 @@
         // create a job that times out if the connection cannot be created in x amount of time
         CoroutineScope(pendingConnectionRequest.coroutineContext).launch {
             delay(CONNECTION_CREATION_TIMEOUT)
-            if (!pendingConnectionRequest.completableDeferred.isCompleted) {
+            if (!pendingConnectionRequest.completableDeferred!!.isCompleted) {
                 Log.i(
                     TAG, "The request to create a connection timed out. Cancelling the" +
                         "request to add the call to Telecom."
@@ -155,22 +156,55 @@
         mPendingConnectionRequests.remove(pendingRequest)
     }
 
-    private fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
+    internal fun createSelfManagedConnection(request: ConnectionRequest, direction: Int):
         Connection? {
-        var jetpackConnection: CallSessionLegacy? = null
-        val targetRequest: PendingConnectionRequest? =
-            findTargetPendingConnectionRequest(request, direction)
+        val targetRequest: PendingConnectionRequest =
+            findTargetPendingConnectionRequest(request, direction) ?: return null
 
-        if (targetRequest != null) {
-            jetpackConnection = CallSessionLegacy(
-                ParcelUuid.fromString(UUID.randomUUID().toString()),
-                targetRequest.callChannel,
-                targetRequest.coroutineContext
-            )
-            targetRequest.completableDeferred.complete(jetpackConnection)
-            mPendingConnectionRequests.remove(targetRequest)
+        val jetpackConnection = CallSessionLegacy(
+            ParcelUuid.fromString(UUID.randomUUID().toString()),
+            targetRequest.callChannel,
+            targetRequest.coroutineContext
+        )
+
+        // set display name
+        jetpackConnection.setCallerDisplayName(
+            targetRequest.callAttributes.displayName.toString(),
+            TelecomManager.PRESENTATION_ALLOWED
+        )
+
+        // set address
+        jetpackConnection.setAddress(
+            targetRequest.callAttributes.address,
+            TelecomManager.PRESENTATION_ALLOWED
+        )
+
+        // set the call state for the given direction
+        if (direction == CallAttributesCompat.DIRECTION_OUTGOING) {
+            jetpackConnection.setDialing()
+        } else {
+            jetpackConnection.setRinging()
         }
 
+        // set the callType
+        if (targetRequest.callAttributes.callType
+            == CallAttributesCompat.CALL_TYPE_VIDEO_CALL
+        ) {
+            jetpackConnection.setVideoState(VideoProfile.STATE_BIDIRECTIONAL)
+        } else {
+            jetpackConnection.setVideoState(VideoProfile.STATE_AUDIO_ONLY)
+        }
+
+        // set the call capabilities
+        if (targetRequest.callAttributes.hasSupportsSetInactiveCapability()) {
+            jetpackConnection.setConnectionCapabilities(
+                Connection.CAPABILITY_HOLD or Connection.CAPABILITY_SUPPORT_HOLD
+            )
+        }
+
+        targetRequest.completableDeferred?.complete(jetpackConnection)
+        mPendingConnectionRequests.remove(targetRequest)
+
         return jetpackConnection
     }
 
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e79b24a..3ff9e96 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1089,6 +1089,7 @@
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
     method public static java.io.File? getDataDir(android.content.Context);
+    method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
     method public static java.io.File![] getExternalCacheDirs(android.content.Context);
     method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index eeb990f..ed6f417 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1089,6 +1089,7 @@
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
     method public static java.io.File? getDataDir(android.content.Context);
+    method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
     method public static java.io.File![] getExternalCacheDirs(android.content.Context);
     method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index a861a92..f1f4d49 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1206,6 +1206,7 @@
     method @ColorInt public static int getColor(android.content.Context, @ColorRes int);
     method public static android.content.res.ColorStateList? getColorStateList(android.content.Context, @ColorRes int);
     method public static java.io.File? getDataDir(android.content.Context);
+    method public static android.view.Display getDisplay(@DisplayContext android.content.Context);
     method public static android.graphics.drawable.Drawable? getDrawable(android.content.Context, @DrawableRes int);
     method public static java.io.File![] getExternalCacheDirs(android.content.Context);
     method public static java.io.File![] getExternalFilesDirs(android.content.Context, String?);
diff --git a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
index 9629356..508f281 100644
--- a/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/ContextCompatTest.java
@@ -66,6 +66,7 @@
 import static android.content.Context.WIFI_P2P_SERVICE;
 import static android.content.Context.WIFI_SERVICE;
 import static android.content.Context.WINDOW_SERVICE;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -138,6 +139,7 @@
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
 import android.util.DisplayMetrics;
+import android.view.Display;
 import android.view.LayoutInflater;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -147,8 +149,10 @@
 
 import androidx.annotation.OptIn;
 import androidx.core.app.NotificationManagerCompat;
+import androidx.core.hardware.display.DisplayManagerCompat;
 import androidx.core.os.BuildCompat;
 import androidx.core.test.R;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -606,4 +610,58 @@
                             Manifest.permission.POST_NOTIFICATIONS));
         }
     }
+
+    @Test
+    public void testGetDisplayFromActivity() {
+        final Display actualDisplay = ContextCompat.getDisplay(mContext);
+        if (Build.VERSION.SDK_INT >= 30) {
+            assertEquals(mContext.getDisplay(), actualDisplay);
+        } else {
+            final WindowManager windowManager =
+                    (WindowManager) mContext.getSystemService(WINDOW_SERVICE);
+            assertEquals(actualDisplay, windowManager.getDefaultDisplay());
+        }
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 17)
+    public void testGetDisplayFromDisplayContext() {
+        final DisplayManagerCompat displayManagerCompat = DisplayManagerCompat
+                .getInstance(mContext);
+        final Display defaultDisplay =  displayManagerCompat.getDisplay(Display.DEFAULT_DISPLAY);
+        final Context displayContext = mContext.createDisplayContext(defaultDisplay);
+
+        assertEquals(ContextCompat.getDisplay(displayContext), defaultDisplay);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 30)
+    public void testGetDisplayFromWindowContext() {
+        final Context windowContext = mContext.createWindowContext(TYPE_APPLICATION_OVERLAY, null);
+
+        assertEquals(ContextCompat.getDisplay(windowContext), windowContext.getDisplay());
+    }
+
+    @Test
+    public void testGetDisplayFromApplication() {
+        final Context applicationContext = ApplicationProvider.getApplicationContext();
+        final Context spyContext = spy(applicationContext);
+        final Display actualDisplay = ContextCompat.getDisplay(spyContext);
+
+        if (Build.VERSION.SDK_INT >= 30) {
+            verify(spyContext).getSystemService(eq(DisplayManager.class));
+
+            final Display defaultDisplay = DisplayManagerCompat.getInstance(spyContext)
+                    .getDisplay(Display.DEFAULT_DISPLAY);
+            assertEquals(defaultDisplay, actualDisplay);
+        } else {
+            final WindowManager windowManager =
+                    (WindowManager) spyContext.getSystemService(WINDOW_SERVICE);
+            // Don't verify if the returned display is the same instance because Application is
+            // not a DisplayContext and the framework always create a fallback Display for
+            // the Context that not associated with a Display.
+            assertEquals(windowManager.getDefaultDisplay().getDisplayId(),
+                    actualDisplay.getDisplayId());
+        }
+    }
 }
diff --git a/core/core/src/main/java/androidx/core/content/ContextCompat.java b/core/core/src/main/java/androidx/core/content/ContextCompat.java
index cd4e613..172b452 100644
--- a/core/core/src/main/java/androidx/core/content/ContextCompat.java
+++ b/core/core/src/main/java/androidx/core/content/ContextCompat.java
@@ -70,6 +70,7 @@
 
 import android.accounts.AccountManager;
 import android.annotation.SuppressLint;
+import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.AlarmManager;
 import android.app.AppOpsManager;
@@ -131,6 +132,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.TypedValue;
+import android.view.Display;
 import android.view.LayoutInflater;
 import android.view.WindowManager;
 import android.view.accessibility.AccessibilityManager;
@@ -140,6 +142,7 @@
 
 import androidx.annotation.ColorInt;
 import androidx.annotation.ColorRes;
+import androidx.annotation.DisplayContext;
 import androidx.annotation.DoNotInline;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.IntDef;
@@ -753,6 +756,28 @@
     }
 
     /**
+     * Get the display this context is associated with.
+     * <p>
+     * Applications must use this method with {@link Activity} or a context associated with a
+     * {@link Display} via {@link Context#createDisplayContext(Display)} or
+     * {@link Context#createWindowContext(Display, int, Bundle)}, or the reported {@link Display}
+     * instance is not reliable. </p>
+     *
+     * @param context Context to obtain the associated display
+     * @return The display associated with the Context.
+     */
+    @NonNull
+    public static Display getDisplay(@NonNull @DisplayContext Context context) {
+        if (Build.VERSION.SDK_INT >= 30) {
+            return Api30Impl.getDisplayNoCrash(context);
+        } else {
+            final WindowManager windowManager =
+                    (WindowManager) context.getSystemService(WINDOW_SERVICE);
+            return windowManager.getDefaultDisplay();
+        }
+    }
+
+    /**
      * Return the handle to a system-level service by class.
      *
      * @param context      Context to retrieve service from.
@@ -1113,6 +1138,19 @@
         static String getAttributionTag(Context obj) {
             return obj.getAttributionTag();
         }
+
+        @DoNotInline
+        static Display getDisplayNoCrash(Context obj) {
+            try {
+                return obj.getDisplay();
+            } catch (UnsupportedOperationException e) {
+                // Provide a fallback display if the context is not associated with any display.
+                Log.w(TAG, "The context:" + obj + " is not associated with any display. Return a "
+                        + "fallback display instead.");
+                return obj.getSystemService(DisplayManager.class)
+                        .getDisplay(Display.DEFAULT_DISPLAY);
+            }
+        }
     }
 
     @RequiresApi(33)
diff --git a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
index a43c2af..5ccc392 100644
--- a/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
+++ b/core/core/src/main/java/androidx/core/view/SoftwareKeyboardControllerCompat.java
@@ -207,24 +207,23 @@
                 insetsController = mView.getWindowInsetsController();
             }
             if (insetsController != null) {
-                if (SDK_INT <= 33) {
-                    final AtomicBoolean isImeInsetsControllable = new AtomicBoolean(false);
-                    final WindowInsetsController.OnControllableInsetsChangedListener listener =
-                            (windowInsetsController, typeMask) -> isImeInsetsControllable.set(
-                                    (typeMask & WindowInsetsCompat.Type.IME) != 0);
-                    // Register the OnControllableInsetsChangedListener would synchronously
-                    // callback current controllable insets. Adding the listener here to check if
-                    // ime inset is controllable.
-                    insetsController.addOnControllableInsetsChangedListener(listener);
-                    if (!isImeInsetsControllable.get()) {
-                        final InputMethodManager imm = (InputMethodManager) mView.getContext()
-                                .getSystemService(Context.INPUT_METHOD_SERVICE);
-                        // This is a backport when the app is in multi-windowing mode, it cannot
-                        // control the ime insets. Use the InputMethodManager instead.
-                        imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
-                    }
-                    insetsController.removeOnControllableInsetsChangedListener(listener);
+                final AtomicBoolean isImeInsetsControllable = new AtomicBoolean(false);
+                final WindowInsetsController.OnControllableInsetsChangedListener listener =
+                        (windowInsetsController, typeMask) -> isImeInsetsControllable.set(
+                                (typeMask & WindowInsetsCompat.Type.IME) != 0);
+                // Register the OnControllableInsetsChangedListener would synchronously
+                // callback current controllable insets. Adding the listener here to check if
+                // ime inset is controllable.
+                insetsController.addOnControllableInsetsChangedListener(listener);
+                if (!isImeInsetsControllable.get()) {
+                    final InputMethodManager imm = (InputMethodManager) mView.getContext()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+                    // This is a backport when the app is in multi-windowing mode, it cannot
+                    // control the ime insets. Use the InputMethodManager instead.
+                    // TODO(b/280532442): Fix this in the platform side.
+                    imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
                 }
+                insetsController.removeOnControllableInsetsChangedListener(listener);
                 insetsController.hide(WindowInsets.Type.ime());
             } else {
                 // Couldn't find an insets controller, fallback to old implementation
diff --git a/development/project-creator/compose-template/groupId/artifactId/build.gradle b/development/project-creator/compose-template/groupId/artifactId/build.gradle
index a47774d..e09ec31 100644
--- a/development/project-creator/compose-template/groupId/artifactId/build.gradle
+++ b/development/project-creator/compose-template/groupId/artifactId/build.gradle
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
+import androidx.build.KmpPlatformsKt
 
 plugins {
     id("AndroidXPlugin")
@@ -24,68 +24,75 @@
     id("org.jetbrains.kotlin.android")
 }
 
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
 
-dependencies {
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 
-
-    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block below
-         */
-        implementation(libs.kotlinStdlibCommon)
-
-        api("androidx.annotation:annotation:1.1.0")
-
-        testImplementation(libs.testRules)
-        testImplementation(libs.testRunner)
-        testImplementation(libs.junit)
-        testImplementation(libs.truth)
-
-        androidTestImplementation(libs.testRules)
-        androidTestImplementation(libs.testRunner)
-        androidTestImplementation(libs.junit)
-        androidTestImplementation(libs.truth)
-    }
-}
-
-if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
-    androidXComposeMultiplatform {
-        android()
-        desktop()
-    }
-
-    kotlin {
-        /*
-         * When updating dependencies, make sure to make the an an analogous update in the
-         * corresponding block above
-         */
-        sourceSets {
-            commonMain.dependencies {
+    sourceSets {
+        commonMain {
+            dependencies {
                 implementation(libs.kotlinStdlibCommon)
             }
+        }
+        androidMain.dependencies {
+        }
 
-            androidMain.dependencies {
+        commonTest {
+            dependencies {
+            }
+        }
+
+        jvmMain {
+            dependsOn(commonMain)
+            dependencies {
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.junit)
+                implementation(libs.truth)
+            }
+        }
+
+
+        androidMain {
+            dependsOn(jvmMain)
+            dependencies {
                 api("androidx.annotation:annotation:1.1.0")
             }
+        }
 
-            desktopMain.dependencies {
-                implementation(libs.kotlinStdlib)
+        if (desktopEnabled) {
+            desktopMain {
+                dependsOn(jvmMain)
+                dependencies {
+                    implementation(libs.kotlinStdlib)
+                }
             }
+        }
 
-            test.dependencies {
+        jvmTest {
+            dependsOn(commonTest)
+            dependencies {
+            }
+        }
+
+        androidAndroidTest {
+            dependsOn(jvmTest)
+            dependencies {
                 implementation(libs.testRules)
                 implementation(libs.testRunner)
                 implementation(libs.junit)
                 implementation(libs.truth)
             }
+        }
 
-            androidAndroidTest.dependencies {
-                implementation(libs.testRules)
-                implementation(libs.testRunner)
-                implementation(libs.junit)
-                implementation(libs.truth)
+        if (desktopEnabled) {
+            desktopTest {
+                dependsOn(jvmTest)
+                dependsOn(desktopMain)
+                dependencies {
+                }
             }
         }
     }
diff --git a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
index e75969a..5130a65 100644
--- a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
+++ b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiInputFilterTest.java
@@ -30,13 +30,19 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.text.Spannable;
 import android.text.SpannableString;
+import android.widget.EditText;
 import android.widget.TextView;
 
 import androidx.emoji.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -126,4 +132,31 @@
         verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
         verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void initCallback_doesntCrashWhenNotAttached() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        EditText editText = new EditText(context);
+        EmojiInputFilter subject = new EmojiInputFilter(editText);
+        subject.getInitCallback().onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 29)
+    public void initCallback_sendsToNonMainHandler_beforeSetText() {
+        // this is just testing that onInitialized dispatches to editText.getHandler before setText
+        EditText mockEditText = mock(EditText.class);
+        HandlerThread thread = new HandlerThread("random thread");
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        thread.quitSafely();
+        when(mockEditText.getHandler()).thenReturn(handler);
+        EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
+        EmojiInputFilter.InitCallbackImpl initCallback =
+                (EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
+        initCallback.onInitialized();
+
+        handler.hasCallbacks(initCallback);
+    }
 }
diff --git a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
index fe3da64..50b0169 100644
--- a/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
+++ b/emoji/emoji/src/androidTest/java/androidx/emoji/widget/EmojiTextWatcherTest.java
@@ -27,13 +27,18 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.widget.EditText;
 
 import androidx.emoji.text.EmojiCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -41,6 +46,7 @@
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
+@SdkSuppress(minSdkVersion = 19)
 public class EmojiTextWatcherTest {
 
     private EmojiTextWatcher mTextWatcher;
@@ -120,4 +126,31 @@
         verify(mEmojiCompat, times(0)).process(any(Spannable.class), anyInt(), anyInt());
         verify(mEmojiCompat, times(1)).registerInitCallback(any(EmojiCompat.InitCallback.class));
     }
+
+    @Test
+    public void initCallback_doesntCrashWhenNotAttached() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        EditText editText = new EditText(context);
+        EmojiTextWatcher subject = new EmojiTextWatcher(editText);
+        subject.getInitCallback().onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 29)
+    public void initCallback_sendsToNonMainHandler_beforeSetText() {
+        // this is just testing that onInitialized dispatches to editText.getHandler before setText
+        EditText mockEditText = mock(EditText.class);
+        HandlerThread thread = new HandlerThread("random thread");
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        thread.quitSafely();
+        when(mockEditText.getHandler()).thenReturn(handler);
+        EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText);
+        EmojiTextWatcher.InitCallbackImpl initCallback =
+                (EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
+        initCallback.onInitialized();
+
+        handler.hasCallbacks(initCallback);
+    }
 }
+
diff --git a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
index 27d1aa0..25f9478 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiInputFilter.java
@@ -15,8 +15,10 @@
  */
 package androidx.emoji.widget;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.os.Handler;
 import android.text.Selection;
 import android.text.Spannable;
 import android.text.Spanned;
@@ -88,14 +90,16 @@
         }
     }
 
-    private InitCallback getInitCallback() {
+    @RestrictTo(LIBRARY)
+    InitCallback getInitCallback() {
         if (mInitCallback == null) {
             mInitCallback = new InitCallbackImpl(mTextView);
         }
         return mInitCallback;
     }
 
-    private static class InitCallbackImpl extends InitCallback {
+    @RestrictTo(LIBRARY)
+    static class InitCallbackImpl extends InitCallback implements Runnable {
         private final Reference<TextView> mViewRef;
 
         InitCallbackImpl(TextView textView) {
@@ -106,7 +110,23 @@
         public void onInitialized() {
             super.onInitialized();
             final TextView textView = mViewRef.get();
-            if (textView != null && textView.isAttachedToWindow()) {
+            if (textView == null) {
+                return;
+            }
+            // we need to move to the actual thread this view is using as main
+            Handler handler = textView.getHandler();
+            if (handler != null) {
+                handler.post(this);
+            }
+        }
+
+        @Override
+        public void run() {
+            final TextView textView = mViewRef.get();
+            if (textView == null) {
+                return;
+            }
+            if (textView.isAttachedToWindow()) {
                 final CharSequence result = EmojiCompat.get().process(textView.getText());
 
                 final int selectionStart = Selection.getSelectionStart(result);
diff --git a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
index 2a4572f..6e6fcf3 100644
--- a/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
+++ b/emoji/emoji/src/main/java/androidx/emoji/widget/EmojiTextWatcher.java
@@ -15,8 +15,10 @@
  */
 package androidx.emoji.widget;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
 
+import android.os.Handler;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -99,14 +101,16 @@
         // do nothing
     }
 
-    private InitCallback getInitCallback() {
+    @RestrictTo(LIBRARY)
+    InitCallback getInitCallback() {
         if (mInitCallback == null) {
             mInitCallback = new InitCallbackImpl(mEditText);
         }
         return mInitCallback;
     }
 
-    private static class InitCallbackImpl extends InitCallback {
+    @RestrictTo(LIBRARY)
+    static class InitCallbackImpl extends InitCallback implements Runnable {
         private final Reference<EditText> mViewRef;
 
         InitCallbackImpl(EditText editText) {
@@ -117,6 +121,19 @@
         public void onInitialized() {
             super.onInitialized();
             final EditText editText = mViewRef.get();
+            if (editText == null) {
+                return;
+            }
+            Handler handler = editText.getHandler();
+            if (handler == null) {
+                return;
+            }
+            handler.post(this);
+        }
+
+        @Override
+        public void run() {
+            final EditText editText = mViewRef.get();
             if (editText != null && editText.isAttachedToWindow()) {
                 final Editable text = editText.getEditableText();
 
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
index 3349e6d..a21adf7 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiInputFilterTest.java
@@ -30,9 +30,13 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.text.InputFilter;
 import android.text.Spannable;
 import android.text.SpannableString;
+import android.widget.EditText;
 import android.widget.TextView;
 
 import androidx.emoji2.text.EmojiCompat;
@@ -40,6 +44,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -191,7 +196,7 @@
 
         when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
         // trigger initialized
-        captor.getValue().onInitialized();
+        ((Runnable) captor.getValue()).run();
 
         verify(mEmojiCompat).process(eq(testString));
     }
@@ -223,7 +228,7 @@
         when(mEmojiCompat.process(eq(testString))).thenReturn(testString);
         when(mEmojiCompat.getLoadState()).thenReturn(EmojiCompat.LOAD_STATE_SUCCEEDED);
         // trigger initialized
-        captor.getValue().onInitialized();
+        ((Runnable) captor.getValue()).run();
 
         // validate interactions don't do anything except check for update
         verify(mTextView).getFilters();
@@ -233,4 +238,31 @@
         // if you add a safe interaction please update test
         verifyNoMoreInteractions(mTextView);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 19)
+    public void initCallback_doesntCrashWhenNotAttached() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        EditText editText = new EditText(context);
+        EmojiInputFilter subject = new EmojiInputFilter(editText);
+        subject.getInitCallback().onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 29)
+    public void initCallback_sendsToNonMainHandler_beforeSetText() {
+        // this is just testing that onInitialized dispatches to editText.getHandler before setText
+        EditText mockEditText = mock(EditText.class);
+        HandlerThread thread = new HandlerThread("random thread");
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        thread.quitSafely();
+        when(mockEditText.getHandler()).thenReturn(handler);
+        EmojiInputFilter subject = new EmojiInputFilter(mockEditText);
+        EmojiInputFilter.InitCallbackImpl initCallback =
+                (EmojiInputFilter.InitCallbackImpl) subject.getInitCallback();
+        initCallback.onInitialized();
+
+        handler.hasCallbacks(initCallback);
+    }
 }
diff --git a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
index 8f149da..5c3c5de 100644
--- a/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
+++ b/emoji2/emoji2-views-helper/src/androidTest/java/androidx/emoji2/viewsintegration/EmojiTextWatcherTest.java
@@ -27,6 +27,9 @@
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -37,6 +40,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -149,4 +153,30 @@
         mTextWatcher.onTextChanged(expected, 0, 0, 1);
         assertTrue(TextUtils.equals(expected, "abc"));
     }
+
+    @Test
+    public void initCallback_doesntCrashWhenNotAttached() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        EditText editText = new EditText(context);
+        EmojiTextWatcher subject = new EmojiTextWatcher(editText, false);
+        subject.getInitCallback().onInitialized();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 29)
+    public void initCallback_sendsToNonMainHandler_beforeSetText() {
+        // this is just testing that onInitialized dispatches to editText.getHandler before setText
+        EditText mockEditText = mock(EditText.class);
+        HandlerThread thread = new HandlerThread("random thread");
+        thread.start();
+        Handler handler = new Handler(thread.getLooper());
+        thread.quitSafely();
+        when(mockEditText.getHandler()).thenReturn(handler);
+        EmojiTextWatcher subject = new EmojiTextWatcher(mockEditText, false);
+        EmojiTextWatcher.InitCallbackImpl initCallback =
+                (EmojiTextWatcher.InitCallbackImpl) subject.getInitCallback();
+        initCallback.onInitialized();
+
+        handler.hasCallbacks(initCallback);
+    }
 }
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
index 530fc52..9c9b485 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiInputFilter.java
@@ -15,6 +15,9 @@
  */
 package androidx.emoji2.viewsintegration;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.Handler;
 import android.text.InputFilter;
 import android.text.Selection;
 import android.text.Spannable;
@@ -39,7 +42,7 @@
  * effects.
  *
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(LIBRARY)
 @RequiresApi(19)
 final class EmojiInputFilter implements android.text.InputFilter {
     private final TextView mTextView;
@@ -88,15 +91,17 @@
         }
     }
 
-    private InitCallback getInitCallback() {
+    @RestrictTo(LIBRARY)
+    InitCallback getInitCallback() {
         if (mInitCallback == null) {
             mInitCallback = new InitCallbackImpl(mTextView, this);
         }
         return mInitCallback;
     }
 
+    @RestrictTo(LIBRARY)
     @RequiresApi(19)
-    private static class InitCallbackImpl extends InitCallback {
+    static class InitCallbackImpl extends InitCallback implements Runnable  {
         private final Reference<TextView> mViewRef;
         private final Reference<EmojiInputFilter> mEmojiInputFilterReference;
 
@@ -109,6 +114,19 @@
         @Override
         public void onInitialized() {
             super.onInitialized();
+            final TextView textView = mViewRef.get();
+            if (textView == null) {
+                return;
+            }
+            // we need to move to the actual thread this view is using as main
+            Handler handler = textView.getHandler();
+            if (handler != null) {
+                handler.post(this);
+            }
+        }
+
+        @Override
+        public void run() {
             @Nullable final TextView textView = mViewRef.get();
             @Nullable final InputFilter myInputFilter = mEmojiInputFilterReference.get();
             if (!isInputFilterCurrentlyRegisteredOnTextView(textView, myInputFilter)) return;
diff --git a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
index 55c37da..691f995 100644
--- a/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
+++ b/emoji2/emoji2-views-helper/src/main/java/androidx/emoji2/viewsintegration/EmojiTextWatcher.java
@@ -15,6 +15,9 @@
  */
 package androidx.emoji2.viewsintegration;
 
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.os.Handler;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -34,7 +37,7 @@
  * TextWatcher used for an EditText.
  *
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RestrictTo(LIBRARY)
 @RequiresApi(19)
 final class EmojiTextWatcher implements android.text.TextWatcher {
     private final EditText mEditText;
@@ -107,7 +110,11 @@
         // do nothing
     }
 
-    private InitCallback getInitCallback() {
+    /**
+     * @return
+     */
+    @RestrictTo(LIBRARY)
+    InitCallback getInitCallback() {
         if (mInitCallback == null) {
             mInitCallback = new InitCallbackImpl(mEditText);
         }
@@ -130,8 +137,9 @@
         }
     }
 
+    @RestrictTo(LIBRARY)
     @RequiresApi(19)
-    private static class InitCallbackImpl extends InitCallback {
+    static class InitCallbackImpl extends InitCallback implements Runnable {
         private final Reference<EditText> mViewRef;
 
         InitCallbackImpl(EditText editText) {
@@ -142,6 +150,19 @@
         public void onInitialized() {
             super.onInitialized();
             final EditText editText = mViewRef.get();
+            if (editText == null) {
+                return;
+            }
+            Handler handler = editText.getHandler();
+            if (handler == null) {
+                return;
+            }
+            handler.post(this);
+        }
+
+        @Override
+        public void run() {
+            final EditText editText = mViewRef.get();
             processTextOnEnablingEvent(editText, EmojiCompat.LOAD_STATE_SUCCEEDED);
         }
     }
diff --git a/graphics/graphics-path/build.gradle b/graphics/graphics-path/build.gradle
index 44d4e0c..392c246 100644
--- a/graphics/graphics-path/build.gradle
+++ b/graphics/graphics-path/build.gradle
@@ -26,7 +26,7 @@
     api(libs.kotlinStdlib)
 
     implementation('androidx.appcompat:appcompat:1.6.1')
-    implementation('androidx.core:core:1.5.0-beta01')
+    implementation(project(':core:core'))
 
     androidTestImplementation("androidx.annotation:annotation:1.4.0")
     androidTestImplementation("androidx.core:core-ktx:1.8.0")
diff --git a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
index 8a7e238..495d89f 100644
--- a/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
+++ b/javascriptengine/javascriptengine/src/androidTest/java/androidx/javascriptengine/WebViewJavaScriptSandboxTest.java
@@ -17,9 +17,12 @@
 package androidx.javascriptengine;
 
 import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.webkit.WebView;
 
 import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.core.content.pm.PackageInfoCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
@@ -53,6 +56,18 @@
         Assume.assumeTrue(JavaScriptSandbox.isSupported());
     }
 
+    // Get the current WebView provider version. In a versionCode of AAAABBBCD, AAAA is the build
+    // number and BBB is the patch number. C and D may usually be ignored.
+    //
+    // Strongly prefer using feature flags over version checks if possible.
+    public long getWebViewVersion() {
+        PackageInfo systemWebViewPackage = WebView.getCurrentWebViewPackage();
+        if (systemWebViewPackage == null) {
+            Assert.fail("No current WebView provider");
+        }
+        return PackageInfoCompat.getLongVersionCode(systemWebViewPackage);
+    }
+
     @Test
     @MediumTest
     public void testSimpleJsEvaluation() throws Throwable {
@@ -571,6 +586,14 @@
     @Test
     @LargeTest
     public void testHeapSizeEnforced() throws Throwable {
+        // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
+        // instead crash the whole sandbox process. This change is not tracked in a feature flag.
+        // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
+        // of interest for this test.
+        // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
+        Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
+                getWebViewVersion() >= 5438_000_00L);
+
         final long maxHeapSize = REASONABLE_HEAP_SIZE;
         // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
         // that we're allocating an array of elements - not bytes. Filling will ensure that the
@@ -587,6 +610,7 @@
         try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
             Assume.assumeTrue(jsSandbox.isFeatureSupported(
                     JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
+
             Assume.assumeTrue(
                     jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
             IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
@@ -657,6 +681,14 @@
     @Test
     @LargeTest
     public void testIsolateCreationAfterCrash() throws Throwable {
+        // WebView versions < 110.0.5438.0 do not contain OOM crashes to a single isolate and
+        // instead crash the whole sandbox process. This change is not tracked in a feature flag.
+        // Versions < 110.0.5438.0 are not considered to be broken, but their behavior is not
+        // of interest for this test.
+        // See Chromium change: https://chromium-review.googlesource.com/c/chromium/src/+/4047785
+        Assume.assumeTrue("WebView version does not support per-isolate OOM handling",
+                getWebViewVersion() >= 5438_000_00L);
+
         final long maxHeapSize = REASONABLE_HEAP_SIZE;
         // We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
         // that we're allocating an array of elements - not bytes. Filling will ensure that the
diff --git a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
index 00afeb8..a4432f0 100644
--- a/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
+++ b/javascriptengine/javascriptengine/src/main/java/androidx/javascriptengine/JavaScriptIsolate.java
@@ -162,13 +162,23 @@
         @Override
         public void reportResult(String result) {
             Objects.requireNonNull(result);
-            handleEvaluationResult(mCompleter, result);
+            final long identityToken = Binder.clearCallingIdentity();
+            try {
+                handleEvaluationResult(mCompleter, result);
+            } finally {
+                Binder.restoreCallingIdentity(identityToken);
+            }
         }
 
         @Override
         public void reportError(@ExecutionErrorTypes int type, String error) {
             Objects.requireNonNull(error);
-            handleEvaluationError(mCompleter, type, error);
+            final long identityToken = Binder.clearCallingIdentity();
+            try {
+                handleEvaluationError(mCompleter, type, error);
+            } finally {
+                Binder.restoreCallingIdentity(identityToken);
+            }
         }
     }
 
diff --git a/lifecycle/lifecycle-runtime-compose/api/current.txt b/lifecycle/lifecycle-runtime-compose/api/current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/current.txt
@@ -10,11 +10,31 @@
 
   public final class LifecycleEffectKt {
     method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+    method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+    method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
   }
 
   public final class LifecycleExtKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
   }
 
+  public interface LifecyclePauseEffectResult {
+    method public void runPauseEffect();
+  }
+
+  public final class LifecycleResumePauseEffectScope {
+    ctor public LifecycleResumePauseEffectScope();
+    method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+  }
+
+  public final class LifecycleStartStopEffectScope {
+    ctor public LifecycleStartStopEffectScope();
+    method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+  }
+
+  public interface LifecycleStopEffectResult {
+    method public void runStopEffect();
+  }
+
 }
 
diff --git a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
@@ -10,11 +10,31 @@
 
   public final class LifecycleEffectKt {
     method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+    method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+    method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
   }
 
   public final class LifecycleExtKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
   }
 
+  public interface LifecyclePauseEffectResult {
+    method public void runPauseEffect();
+  }
+
+  public final class LifecycleResumePauseEffectScope {
+    ctor public LifecycleResumePauseEffectScope();
+    method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+  }
+
+  public final class LifecycleStartStopEffectScope {
+    ctor public LifecycleStartStopEffectScope();
+    method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+  }
+
+  public interface LifecycleStopEffectResult {
+    method public void runStopEffect();
+  }
+
 }
 
diff --git a/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt b/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
index 5b19f2b..41aee87c 100644
--- a/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/restricted_current.txt
@@ -10,11 +10,31 @@
 
   public final class LifecycleEffectKt {
     method @androidx.compose.runtime.Composable public static void LifecycleEventEffect(androidx.lifecycle.Lifecycle.Event event, optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function0<kotlin.Unit> onEvent);
+    method @androidx.compose.runtime.Composable public static void LifecycleResumeEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleResumePauseEffectScope,? extends androidx.lifecycle.compose.LifecyclePauseEffectResult> effects);
+    method @androidx.compose.runtime.Composable public static void LifecycleStartEffect(optional androidx.lifecycle.LifecycleOwner lifecycleOwner, kotlin.jvm.functions.Function1<? super androidx.lifecycle.compose.LifecycleStartStopEffectScope,? extends androidx.lifecycle.compose.LifecycleStopEffectResult> effects);
   }
 
   public final class LifecycleExtKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<androidx.lifecycle.Lifecycle.State> currentStateAsState(androidx.lifecycle.Lifecycle);
   }
 
+  public interface LifecyclePauseEffectResult {
+    method public void runPauseEffect();
+  }
+
+  public final class LifecycleResumePauseEffectScope {
+    ctor public LifecycleResumePauseEffectScope();
+    method public inline androidx.lifecycle.compose.LifecyclePauseEffectResult onPause(kotlin.jvm.functions.Function0<kotlin.Unit> onPauseEffect);
+  }
+
+  public final class LifecycleStartStopEffectScope {
+    ctor public LifecycleStartStopEffectScope();
+    method public inline androidx.lifecycle.compose.LifecycleStopEffectResult onStop(kotlin.jvm.functions.Function0<kotlin.Unit> onStopEffect);
+  }
+
+  public interface LifecycleStopEffectResult {
+    method public void runStopEffect();
+  }
+
 }
 
diff --git a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt b/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
index 3d7e9fe..482c851 100644
--- a/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/androidTest/java/androidx/lifecycle/compose/LifecycleEffectTest.kt
@@ -110,4 +110,78 @@
                 .isEqualTo(1)
         }
     }
+
+    @Test
+    fun lifecycleStartEffectTest() {
+        lifecycleOwner = TestLifecycleOwner(
+            Lifecycle.State.INITIALIZED,
+            dispatcher
+        )
+        var startCount = 0
+        var stopCount = 0
+
+        composeTestRule.waitForIdle()
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
+                LifecycleStartEffect {
+                    startCount++
+
+                    onStop {
+                        stopCount++
+                    }
+                }
+            }
+        }
+
+        composeTestRule.runOnIdle {
+            assertWithMessage("Lifecycle should not be started (or stopped)")
+                .that(startCount)
+                .isEqualTo(0)
+
+            lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START)
+            assertWithMessage("Lifecycle should have been started")
+                .that(startCount)
+                .isEqualTo(1)
+
+            lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
+            assertWithMessage("Lifecycle should have been stopped")
+                .that(stopCount)
+                .isEqualTo(1)
+        }
+    }
+
+    @Test
+    fun lifecycleResumeEffectTest() {
+        var resumeCount = 0
+        var pauseCount = 0
+
+        composeTestRule.waitForIdle()
+        composeTestRule.setContent {
+            CompositionLocalProvider(LocalLifecycleOwner provides lifecycleOwner) {
+                LifecycleResumeEffect {
+                    resumeCount++
+
+                    onPause {
+                        pauseCount++
+                    }
+                }
+            }
+        }
+
+        composeTestRule.runOnIdle {
+            assertWithMessage("Lifecycle should not be resumed (or paused)")
+                .that(resumeCount)
+                .isEqualTo(0)
+
+            lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
+            assertWithMessage("Lifecycle should have been resumed")
+                .that(resumeCount)
+                .isEqualTo(1)
+
+            lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+            assertWithMessage("Lifecycle should have been paused")
+                .that(pauseCount)
+                .isEqualTo(1)
+        }
+    }
 }
\ No newline at end of file
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
index c9e47ae..245b1a5 100644
--- a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/LifecycleEffect.kt
@@ -75,4 +75,176 @@
             lifecycleOwner.lifecycle.removeObserver(observer)
         }
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Schedule a pair of effects to run when the [Lifecycle] receives either a
+ * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP]. The ON_START effect will
+ * be the body of the [effects] block and the ON_STOP effect will be within the
+ * (onStop clause)[LifecycleStartStopEffectScope.onStop]:
+ *
+ * LifecycleStartEffect(lifecycleOwner) {
+ *     // add ON_START effect work here
+ *
+ *     onStop {
+ *         // add ON_STOP effect work here
+ *     }
+ * }
+ *
+ * A [LifecycleStartEffect] **must** include an [onStop][LifecycleStartStopEffectScope.onStop]
+ * clause as the final statement in its [effects] block. If your operation does not require
+ * an effect for both ON_START and ON_STOP, a [LifecycleEventEffect] should be used instead.
+ *
+ * This function uses a [LifecycleEventObserver] to listen for when [LifecycleStartEffect]
+ * enters the composition and the effects will be launched when receiving a
+ * [Lifecycle.Event.ON_START] or [Lifecycle.Event.ON_STOP] event, respectively.
+ *
+ * This function should **not** be used to launch tasks in response to callback
+ * events by way of storing callback data as a [Lifecycle.State] in a [MutableState].
+ * Instead, see [currentStateAsState] to obtain a [State<Lifecycle.State>][State]
+ * that may be used to launch jobs in response to state changes.
+ *
+ * @param lifecycleOwner The lifecycle owner to attach an observer
+ * @param effects The effects to be launched when we receive the respective event callbacks
+ */
+@Composable
+fun LifecycleStartEffect(
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    effects: LifecycleStartStopEffectScope.() -> LifecycleStopEffectResult
+) {
+    val lifecycleStartStopEffectScope = LifecycleStartStopEffectScope()
+    // Safely update the current `onStart` lambda when a new one is provided
+    val currentEffects by rememberUpdatedState(effects)
+    DisposableEffect(lifecycleOwner) {
+        val observer = LifecycleEventObserver { _, event ->
+            when (event) {
+                Lifecycle.Event.ON_START ->
+                    lifecycleStartStopEffectScope.currentEffects()
+
+                Lifecycle.Event.ON_STOP ->
+                    lifecycleStartStopEffectScope.currentEffects().runStopEffect()
+
+                else -> {}
+            }
+        }
+
+        lifecycleOwner.lifecycle.addObserver(observer)
+
+        onDispose {
+            lifecycleOwner.lifecycle.removeObserver(observer)
+        }
+    }
+}
+
+/**
+ * Interface used for [LifecycleStartEffect] to run the effect within the onStop
+ * clause when an (ON_STOP)[Lifecycle.Event.ON_STOP] event is received.
+ */
+interface LifecycleStopEffectResult {
+    fun runStopEffect()
+}
+
+/**
+ * Receiver scope for [LifecycleStartEffect] that offers the [onStop] clause to
+ * couple the ON_START effect. This should be the last statement in any call to
+ * [LifecycleStartEffect].
+ */
+class LifecycleStartStopEffectScope {
+    /**
+     * Provide the [onStopEffect] to the [LifecycleStartEffect] to run when the observer
+     * receives an (ON_STOP)[Lifecycle.Event.ON_STOP] event.
+     */
+    inline fun onStop(
+        crossinline onStopEffect: () -> Unit
+    ): LifecycleStopEffectResult = object : LifecycleStopEffectResult {
+        override fun runStopEffect() {
+            onStopEffect()
+        }
+    }
+}
+
+/**
+ * Schedule a pair of effects to run when the [Lifecycle] receives either a
+ * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE]. The ON_RESUME effect
+ * will be the body of the [effects] block and the ON_PAUSE effect will be within the
+ * (onPause clause)[LifecycleResumePauseEffectScope.onPause]:
+ *
+ * LifecycleResumeEffect(lifecycleOwner) {
+ *     // add ON_RESUME effect work here
+ *
+ *     onPause {
+ *         // add ON_PAUSE effect work here
+ *     }
+ * }
+ *
+ * A [LifecycleResumeEffect] **must** include an [onPause][LifecycleResumePauseEffectScope.onPause]
+ * clause as the final statement in its [effects] block. If your operation does not require
+ * an effect for both ON_RESUME and ON_PAUSE, a [LifecycleEventEffect] should be used instead.
+ *
+ * This function uses a [LifecycleEventObserver] to listen for when [LifecycleResumeEffect]
+ * enters the composition and the effects will be launched when receiving a
+ * [Lifecycle.Event.ON_RESUME] or [Lifecycle.Event.ON_PAUSE] event, respectively.
+ *
+ * This function should **not** be used to launch tasks in response to callback
+ * events by way of storing callback data as a [Lifecycle.State] in a [MutableState].
+ * Instead, see [currentStateAsState] to obtain a [State<Lifecycle.State>][State]
+ * that may be used to launch jobs in response to state changes.
+ *
+ * @param lifecycleOwner The lifecycle owner to attach an observer
+ * @param effects The effects to be launched when we receive the respective event callbacks
+ */
+@Composable
+fun LifecycleResumeEffect(
+    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
+    effects: LifecycleResumePauseEffectScope.() -> LifecyclePauseEffectResult
+) {
+    val lifecycleResumePauseEffectScope = LifecycleResumePauseEffectScope()
+    // Safely update the current `onResume` lambda when a new one is provided
+    val currentEffects by rememberUpdatedState(effects)
+    DisposableEffect(lifecycleOwner) {
+        val observer = LifecycleEventObserver { _, event ->
+            when (event) {
+                Lifecycle.Event.ON_RESUME ->
+                    lifecycleResumePauseEffectScope.currentEffects()
+
+                Lifecycle.Event.ON_PAUSE ->
+                    lifecycleResumePauseEffectScope.currentEffects().runPauseEffect()
+
+                else -> {}
+            }
+        }
+
+        lifecycleOwner.lifecycle.addObserver(observer)
+
+        onDispose {
+            lifecycleOwner.lifecycle.removeObserver(observer)
+        }
+    }
+}
+
+/**
+ * Interface used for [LifecycleResumeEffect] to run the effect within the onPause
+ * clause when an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event is received.
+ */
+interface LifecyclePauseEffectResult {
+    fun runPauseEffect()
+}
+
+/**
+ * Receiver scope for [LifecycleResumeEffect] that offers the [onPause] clause to
+ * couple the ON_RESUME effect. This should be the last statement in any call to
+ * [LifecycleResumeEffect].
+ */
+class LifecycleResumePauseEffectScope {
+    /**
+     * Provide the [onPauseEffect] to the [LifecycleResumeEffect] to run when the observer
+     * receives an (ON_PAUSE)[Lifecycle.Event.ON_PAUSE] event.
+     */
+    inline fun onPause(
+        crossinline onPauseEffect: () -> Unit
+    ): LifecyclePauseEffectResult = object : LifecyclePauseEffectResult {
+        override fun runPauseEffect() {
+            onPauseEffect()
+        }
+    }
+}
diff --git a/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt b/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
index a365b00..16e56b7 100644
--- a/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/aidl/AidlDefinitionDetector.kt
@@ -44,19 +44,32 @@
     override fun getApplicableFiles() = Scope.OTHER_SCOPE
 
     override fun beforeCheckEachProject(context: Context) {
-        LanguageParserDefinitions.INSTANCE.apply {
+        // Neither LanguageParserDefinitions nor CoreFileTypeRegistry are thread-safe, so this is a
+        // best-effort to avoid a race condition during our own access across multiple lint worker
+        // threads.
+        synchronized(intellijCoreLock) {
+            val aidlFileType = AidlFileType.INSTANCE
+
             // When we run from CLI, the IntelliJ parser (which does not support lexing AIDL) will
             // already be set. Only the first parser will be used, so we need to remove that parser
             // before we add our own.
-            allForLanguage(AidlFileType.INSTANCE.language).forEach { parser ->
-                removeExplicitExtension(AidlFileType.INSTANCE.language, parser)
+            val languageParserDefinitions = LanguageParserDefinitions.INSTANCE
+            languageParserDefinitions.apply {
+                allForLanguage(aidlFileType.language).forEach { parser ->
+                    removeExplicitExtension(aidlFileType.language, parser)
+                }
+                addExplicitExtension(aidlFileType.language, AidlParserDefinition())
             }
-            addExplicitExtension(AidlFileType.INSTANCE.language, AidlParserDefinition())
+
+            // Register our parser for the AIDL file type. Files may be registered more than once to
+            // overwrite the associated extension, but only the first call to `registerFileType`
+            // will associate the file with the name returned by `FileType.getName()`.
+            val coreFileTypeRegistry = CoreFileTypeRegistry.getInstance() as CoreFileTypeRegistry
+            coreFileTypeRegistry.registerFileType(
+                aidlFileType,
+                aidlFileType.defaultExtension
+            )
         }
-        (CoreFileTypeRegistry.getInstance() as CoreFileTypeRegistry).registerFileType(
-            AidlFileType.INSTANCE,
-            AidlFileType.INSTANCE.defaultExtension
-        )
     }
 
     override fun run(context: Context) {
@@ -125,4 +138,10 @@
     containingFile.text,
     textRange.startOffset,
     textRange.endOffset
-)
\ No newline at end of file
+)
+
+/**
+ * Lock object used to synchronize access to IntelliJ registries which are not thread-safe,
+ * including [LanguageParserDefinitions] and [CoreFileTypeRegistry].
+ */
+private val intellijCoreLock = Any()
diff --git a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
index 083d17d..e820bfe 100644
--- a/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
+++ b/navigation/integration-tests/testapp/src/main/java/androidx/navigation/testapp/MainFragment.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.Color
 import android.os.Bundle
+import android.view.Gravity
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
@@ -27,6 +28,7 @@
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.FragmentNavigatorExtras
 import androidx.navigation.fragment.findNavController
+import androidx.transition.Slide
 
 /**
  * Fragment used to show how to navigate to another destination
@@ -38,6 +40,8 @@
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View? {
+        enterTransition = Slide(Gravity.RIGHT)
+        exitTransition = Slide(Gravity.LEFT)
         return inflater.inflate(R.layout.main_fragment, container, false)
     }
 
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
index cea98bd..4db4d1d 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/nav_main.xml
@@ -23,31 +23,19 @@
                   android:name=".MainFragment"
                   android:label="@string/home">
             <argument android:name="myarg" android:defaultValue="Home" />
-            <action android:id="@+id/next" app:destination="@+id/first_screen"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/first_screen"/>
         </fragment>
         <fragment android:id="@+id/first_screen"
             android:name="androidx.navigation.testapp.MainFragment"
             android:label="@string/first">
             <argument android:name="myarg" android:defaultValue="one" />
-            <action android:id="@+id/next" app:destination="@+id/next_fragment"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/next_fragment"/>
         </fragment>
         <fragment android:id="@+id/next_fragment"
             android:name="androidx.navigation.testapp.MainFragment"
             android:label="@string/second">
             <argument android:name="myarg" android:defaultValue="two" />
-            <action android:id="@+id/next" app:destination="@+id/first_screen"
-                app:enterAnim="@anim/nav_default_enter_anim"
-                app:exitAnim="@anim/nav_default_exit_anim"
-                app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-                app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+            <action android:id="@+id/next" app:destination="@+id/first_screen"/>
         </fragment>
     </navigation>
     <dialog
diff --git a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
index f7eb4da..63261cbf 100644
--- a/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
+++ b/navigation/integration-tests/testapp/src/main/res/navigation/two_pane_navigation.xml
@@ -20,51 +20,31 @@
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/first">
         <argument android:name="myarg" android:defaultValue="one" />
-        <action android:id="@+id/next" app:destination="@+id/second_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/second_fragment"/>
     </fragment>
     <fragment android:id="@+id/second_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/second">
         <argument android:name="myarg" android:defaultValue="two" />
-        <action android:id="@+id/next" app:destination="@+id/third_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/third_fragment"/>
     </fragment>
     <fragment android:id="@+id/third_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/third">
         <argument android:name="myarg" android:defaultValue="three" />
-        <action android:id="@+id/next" app:destination="@+id/fourth_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/fourth_fragment"/>
     </fragment>
     <fragment android:id="@+id/fourth_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/fourth">
         <argument android:name="myarg" android:defaultValue="four" />
-        <action android:id="@+id/next" app:destination="@+id/fifth_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/fifth_fragment"/>
     </fragment>
     <fragment android:id="@+id/fifth_fragment"
         android:name="androidx.navigation.testapp.MainFragment"
         android:label="@string/fifth">
         <argument android:name="myarg" android:defaultValue="five" />
-        <action android:id="@+id/next" app:destination="@+id/first_fragment"
-            app:enterAnim="@anim/nav_default_enter_anim"
-            app:exitAnim="@anim/nav_default_exit_anim"
-            app:popEnterAnim="@anim/nav_default_pop_enter_anim"
-            app:popExitAnim="@anim/nav_default_pop_exit_anim"/>
+        <action android:id="@+id/next" app:destination="@+id/first_fragment"/>
     </fragment>
     <dialog
         android:id="@+id/learn_more"
diff --git a/paging/paging-common/api/current.txt b/paging/paging-common/api/current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/current.txt
+++ b/paging/paging-common/api/current.txt
@@ -46,7 +46,7 @@
     method @AnyThread public void onInvalidated();
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 07e6471..a5d3d01 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -49,7 +49,7 @@
   @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -415,6 +415,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/api/restricted_current.txt b/paging/paging-common/api/restricted_current.txt
index 10ec13b..2070710 100644
--- a/paging/paging-common/api/restricted_current.txt
+++ b/paging/paging-common/api/restricted_current.txt
@@ -46,7 +46,7 @@
     method @AnyThread public void onInvalidated();
   }
 
-  public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+  public final class InvalidatingPagingSourceFactory<Key, Value> implements androidx.paging.PagingSourceFactory<Key,Value> {
     ctor public InvalidatingPagingSourceFactory(kotlin.jvm.functions.Function0<? extends androidx.paging.PagingSource<Key,Value>> pagingSourceFactory);
     method public void invalidate();
     method public androidx.paging.PagingSource<Key,Value> invoke();
@@ -411,6 +411,10 @@
   public static final class PagingSource.LoadResult.Page.Companion {
   }
 
+  public fun interface PagingSourceFactory<Key, Value> extends kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
+    method public operator androidx.paging.PagingSource<Key,Value> invoke();
+  }
+
   public final class PagingState<Key, Value> {
     ctor public PagingState(java.util.List<androidx.paging.PagingSource.LoadResult.Page<Key,Value>> pages, Integer? anchorPosition, androidx.paging.PagingConfig config, @IntRange(from=0L) int leadingPlaceholderCount);
     method public Value? closestItemToPosition(int anchorPosition);
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
index 93ddb2d..3f74d54 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/InvalidatingPagingSourceFactory.kt
@@ -32,7 +32,7 @@
  */
 public class InvalidatingPagingSourceFactory<Key : Any, Value : Any>(
     private val pagingSourceFactory: () -> PagingSource<Key, Value>
-) : () -> PagingSource<Key, Value> {
+) : PagingSourceFactory<Key, Value> {
 
     @VisibleForTesting
     internal val pagingSources = CopyOnWriteArrayList<PagingSource<Key, Value>>()
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
new file mode 100644
index 0000000..4ed3c7f
--- /dev/null
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/PagingSourceFactory.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.paging
+
+/**
+ * Interface for a factory that generates [PagingSource].
+ *
+ * The factory extending this interface can be used to instantiate a [Pager] as the
+ * pagingSourceFactory.
+ */
+public fun interface PagingSourceFactory<Key : Any, Value : Any> : () -> PagingSource<Key, Value> {
+    /**
+     * Returns a new PagingSource instance.
+     *
+     * This function can be invoked by calling pagingSourceFactory() or pagingSourceFactory::invoke.
+     */
+    public override operator fun invoke(): PagingSource<Key, Value>
+}
\ No newline at end of file
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index 066939a..f10ecc1 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -26,7 +26,7 @@
   }
 
   public final class StaticListPagingSourceFactoryKt {
-    method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
+    method public static <Value> androidx.paging.PagingSourceFactory<java.lang.Integer,Value> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
 
   public final class TestPager<Key, Value> {
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
index e44216e..3cf9165 100644
--- a/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/StaticListPagingSourceFactory.kt
@@ -19,6 +19,7 @@
 import androidx.paging.InvalidatingPagingSourceFactory
 import androidx.paging.PagingSource
 import androidx.paging.Pager
+import androidx.paging.PagingSourceFactory
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
@@ -41,7 +42,7 @@
  */
 public fun <Value : Any> Flow<@JvmSuppressWildcards List<Value>>.asPagingSourceFactory(
     coroutineScope: CoroutineScope
-): () -> PagingSource<Int, Value> {
+): PagingSourceFactory<Int, Value> {
 
     var data: List<Value>? = null
 
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
index 42aaa0b5..312d95b 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -20,6 +20,7 @@
 import androidx.paging.PagingConfig
 import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadParams
+import androidx.paging.PagingSourceFactory
 import androidx.paging.PagingState
 import androidx.paging.cachedIn
 import androidx.paging.insertSeparators
@@ -2355,9 +2356,9 @@
 }
 
 private class WrappedPagingSourceFactory(
-    private val factory: () -> PagingSource<Int, Int>,
+    private val factory: PagingSourceFactory<Int, Int>,
     private val loadDelay: Long,
-) : () -> PagingSource<Int, Int> {
+) : PagingSourceFactory<Int, Int> {
     override fun invoke(): PagingSource<Int, Int> = TestPagingSource(factory(), loadDelay)
 }
 
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
index 6a2f2c1..eb28936 100644
--- a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/StaticListPagingSourceFactoryTest.kt
@@ -17,8 +17,8 @@
 package androidx.paging.testing
 
 import androidx.paging.PagingConfig
-import androidx.paging.PagingSource
 import androidx.paging.PagingSource.LoadResult.Page
+import androidx.paging.PagingSourceFactory
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
@@ -47,7 +47,7 @@
 
     @Test
     fun emptyFlow() {
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flowOf<List<Int>>().asPagingSourceFactory(testScope)
         val pagingSource = factory()
         val pager = TestPager(pagingSource, CONFIG)
@@ -64,7 +64,7 @@
             List(20) { it }
         )
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flow.asPagingSourceFactory(testScope)
         val pagingSource = factory()
         val pager = TestPager(pagingSource, CONFIG)
@@ -85,7 +85,7 @@
             emit(List(15) { it + 30 }) // second gen
         }
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             flow.asPagingSourceFactory(testScope)
 
         advanceTimeBy(1000)
@@ -117,7 +117,7 @@
         val mutableFlow = MutableSharedFlow<List<Int>>()
         val collectionScope = this.backgroundScope
 
-        val factory: () -> PagingSource<Int, Int> =
+        val factory: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(collectionScope)
 
         mutableFlow.emit(List(10) { it })
@@ -146,10 +146,10 @@
     fun multipleFactories_fromSameFlow() = testScope.runTest {
         val mutableFlow = MutableSharedFlow<List<Int>>()
 
-        val factory1: () -> PagingSource<Int, Int> =
+        val factory1: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
 
-        val factory2: () -> PagingSource<Int, Int> =
+        val factory2: PagingSourceFactory<Int, Int> =
             mutableFlow.asPagingSourceFactory(testScope.backgroundScope)
 
         mutableFlow.emit(List(10) { it })
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 470c9af..213eeb9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -22,10 +22,10 @@
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSNode
 import com.google.devtools.ksp.symbol.KSType
+import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
-import com.google.devtools.ksp.symbol.Modifier
 
 /**
  * Root package comes as <root> instead of "" so we work around it here.
@@ -47,10 +47,12 @@
 }
 
 internal fun KSTypeReference.isTypeParameterReference(): Boolean {
-    return this.resolve().declaration is KSTypeParameter
+    return this.resolve().isTypeParameter()
 }
 
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+internal fun KSType.isTypeParameter(): Boolean {
+    return declaration is KSTypeParameter
+}
 
 internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
     XNullability.NULLABLE -> makeNullable()
@@ -59,14 +61,15 @@
 }
 
 private fun KSAnnotated.hasAnnotation(qName: String) =
-    annotations.any { it.hasQualifiedName(qName) }
+    annotations.any { it.hasQualifiedNameOrAlias(qName) }
 
-private fun KSAnnotation.hasQualifiedName(qName: String): Boolean {
-    return annotationType.resolve().hasQualifiedName(qName)
+private fun KSAnnotation.hasQualifiedNameOrAlias(qName: String): Boolean {
+    return annotationType.resolve().hasQualifiedNameOrAlias(qName)
 }
 
-private fun KSType.hasQualifiedName(qName: String): Boolean {
-    return declaration.qualifiedName?.asString() == qName
+private fun KSType.hasQualifiedNameOrAlias(qName: String): Boolean {
+    return declaration.qualifiedName?.asString() == qName ||
+        (declaration as? KSTypeAlias)?.type?.resolve()?.hasQualifiedNameOrAlias(qName) ?: false
 }
 
 internal fun KSAnnotated.hasJvmWildcardAnnotation() =
@@ -75,20 +78,55 @@
 internal fun KSAnnotated.hasSuppressJvmWildcardAnnotation() =
     hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
 
-private fun KSType.hasAnnotation(qName: String) = annotations.any { it.hasQualifiedName(qName) }
-
-internal fun KSType.hasJvmWildcardAnnotation() =
-    hasAnnotation(JvmWildcard::class.java.canonicalName!!)
+// TODO(bcorso): There's a bug in KSP where, after using KSType#asMemberOf() or KSType#replace(),
+//  the annotations are removed from the resulting type. However, it turns out that the annotation
+//  information is still available in the underlying KotlinType, so we use reflection to get them.
+//  See https://github.com/google/ksp/issues/1376.
+private fun KSType.hasAnnotation(qName: String): Boolean {
+    fun String.toFqName(): Any {
+        return Class.forName("org.jetbrains.kotlin.name.FqName")
+            .getConstructor(String::class.java)
+            .newInstance(this)
+    }
+    fun hasAnnotationViaReflection(qName: String): Boolean {
+        val ksType = if (
+            // Note: Technically, we could just make KSTypeWrapper internal and cast to get the
+            // delegate, but since we need to use reflection anyway, just get it via reflection.
+            this.javaClass.canonicalName == "androidx.room.compiler.processing.ksp.KSTypeWrapper") {
+            this.javaClass.methods.find { it.name == "getDelegate" }?.invoke(this)
+        } else {
+            this
+        }
+        val kotlinType =
+            ksType?.javaClass?.methods?.find { it.name == "getKotlinType" }?.invoke(ksType)
+        val kotlinAnnotations =
+            kotlinType?.javaClass
+                ?.methods
+                ?.find { it.name == "getAnnotations" }
+                ?.invoke(kotlinType)
+        return kotlinAnnotations?.javaClass
+            ?.methods
+            ?.find { it.name == "hasAnnotation" }
+            ?.invoke(kotlinAnnotations, qName.toFqName()) == true
+    }
+    return if (annotations.toList().isEmpty()) {
+        // If there are no annotations but KSType#toString() shows annotations, check the underlying
+        // KotlinType for annotations using reflection.
+        toString().startsWith("[") && hasAnnotationViaReflection(qName)
+    } else {
+        annotations.any { it.annotationType.resolve().hasQualifiedNameOrAlias(qName) }
+    }
+}
 
 internal fun KSType.hasSuppressJvmWildcardAnnotation() =
     hasAnnotation(JvmSuppressWildcards::class.java.canonicalName!!)
 
  internal fun KSNode.hasSuppressWildcardsAnnotationInHierarchy(): Boolean {
-     (this as? KSAnnotated)?.let {
-         if (hasSuppressJvmWildcardAnnotation()) {
-             return true
-         }
-     }
-     val parent = parent ?: return false
-     return parent.hasSuppressWildcardsAnnotationInHierarchy()
- }
\ No newline at end of file
+    (this as? KSAnnotated)?.let {
+        if (hasSuppressJvmWildcardAnnotation()) {
+            return true
+        }
+    }
+    val parent = parent ?: return false
+    return parent.hasSuppressWildcardsAnnotationInHierarchy()
+}
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
index bdade61..d797d50 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolver.kt
@@ -25,6 +25,7 @@
 import com.google.devtools.ksp.symbol.KSTypeAlias
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Variance
 
@@ -59,21 +60,57 @@
             return type
         }
 
-        val resolvedType = if (hasTypeVariables(scope.declarationType())) {
+        // First, replace any type aliases in the type with their actual types
+        return type.replaceTypeAliases()
+            // Next, resolve wildcards based on the scope of the type
+            .resolveWildcards(scope)
+            // Next, apply any additional variance changes based on the @JvmSuppressWildcards or
+            // @JvmWildcard annotations on the resolved type.
+            .applyJvmWildcardAnnotations()
+            // Finally, unwrap any delegate types. (Note: as part of resolving wildcards, we wrap
+            // types/type arguments in delegates to avoid loosing annotation information. However,
+            // those delegates may cause issues later if KSP tries to cast the type/argument to a
+            // particular implementation, so we unwrap them here.
+            .removeWrappers()
+    }
+
+    private fun KSType.replaceTypeAliases(typeStack: ReferenceStack = ReferenceStack()): KSType {
+        if (isError || typeStack.queue.contains(this)) {
+            return this
+        }
+        if (declaration is KSTypeAlias) {
+            return (declaration as KSTypeAlias).type.resolve().replaceTypeAliases(typeStack)
+        }
+        return typeStack.withReference(this) {
+            createWrapper(arguments.map { it.replaceTypeAliases(typeStack) })
+        }
+    }
+
+    private fun KSTypeArgument.replaceTypeAliases(typeStack: ReferenceStack): KSTypeArgument {
+        val type = type?.resolve()
+        if (
+            type == null ||
+            type.isError ||
+            variance == Variance.STAR ||
+            typeStack.queue.contains(type)
+        ) {
+            return this
+        }
+        return createWrapper(type.replaceTypeAliases(typeStack), variance)
+    }
+
+    private fun KSType.resolveWildcards(scope: KSTypeVarianceResolverScope): KSType {
+        return if (hasTypeVariables(scope.declarationType())) {
             // If the associated declared type contains type variables that were resolved, e.g.
             // using "asMemberOf", then it has special rules about how to resolve the types.
             getJavaWildcardWithTypeVariables(
-                type = type,
+                type = this,
                 declarationType = getJavaWildcard(scope.declarationType(), scope),
                 scope = scope,
             )
         } else {
-            getJavaWildcard(type, scope)
+            getJavaWildcard(this, scope)
         }
-
-        // As a final pass, we apply variance from any @JvmSuppressWildcards or @JvmWildcard
-        // annotations on the resolved type.
-        return applyJvmWildcardAnnotations(resolvedType)
     }
 
     private fun hasTypeVariables(
@@ -99,15 +136,6 @@
         if (type.isError || typeStack.queue.contains(type)) {
             return type
         }
-        if (type.declaration is KSTypeAlias) {
-            return getJavaWildcard(
-                type = (type.declaration as KSTypeAlias).type.resolve(),
-                scope = scope,
-                typeStack = typeStack,
-                typeArgStack = typeArgStack,
-                typeParamStack = typeParamStack,
-            )
-        }
         return typeStack.withReference(type) {
             val resolvedTypeArgs =
                 type.arguments.indices.map { i ->
@@ -120,7 +148,7 @@
                         typeParamStack = typeParamStack,
                     )
                 }
-            type.replace(resolvedTypeArgs)
+            type.createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -158,10 +186,10 @@
                 if (typeParamStack.indices.none { i ->
                         (typeParamStack[i].variance == Variance.CONTRAVARIANT ||
                             typeArgStack[i].variance == Variance.CONTRAVARIANT) &&
-                        // The declaration and use site variance is ignored when using @JvmWildcard
-                        // explicitly on a type.
-                        !typeArgStack[i].hasJvmWildcardAnnotation()
-                }) {
+                            // The declaration and use site variance is ignored when using @JvmWildcard
+                            // explicitly on a type.
+                            !typeArgStack[i].hasJvmWildcardAnnotation()
+                    }) {
                     return false
                 }
             } else {
@@ -217,7 +245,7 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
     private fun getJavaWildcardWithTypeVariables(
@@ -252,7 +280,7 @@
                         )
                     }
                 }
-            type.replace(resolvedTypeArgs)
+            type.createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -293,7 +321,7 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
     private fun getJavaWildcardWithTypeVariablesForOuterType(
@@ -322,26 +350,27 @@
         } else {
             typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
-    private fun applyJvmWildcardAnnotations(
-        type: KSType,
+    private fun KSType.applyJvmWildcardAnnotations(
         typeStack: ReferenceStack = ReferenceStack(),
+        typeArgStack: List<KSTypeArgument> = emptyList(),
     ): KSType {
-        if (type.isError || typeStack.queue.contains(type)) {
-            return type
+        if (isError || typeStack.queue.contains(this)) {
+            return this
         }
-        return typeStack.withReference(type) {
+        return typeStack.withReference(this) {
             val resolvedTypeArgs =
-                type.arguments.indices.map { i ->
+                arguments.indices.map { i ->
                     applyJvmWildcardAnnotations(
-                        typeArg = type.arguments[i],
-                        typeParameter = type.declaration.typeParameters[i],
+                        typeArg = arguments[i],
+                        typeParameter = declaration.typeParameters[i],
+                        typeArgStack = typeArgStack,
                         typeStack = typeStack,
                     )
                 }
-            type.replace(resolvedTypeArgs)
+            createWrapper(resolvedTypeArgs)
         }
     }
 
@@ -349,6 +378,7 @@
         typeArg: KSTypeArgument,
         typeParameter: KSTypeParameter,
         typeStack: ReferenceStack,
+        typeArgStack: List<KSTypeArgument>,
     ): KSTypeArgument {
         val type = typeArg.type?.resolve()
         if (
@@ -359,28 +389,107 @@
         ) {
             return typeArg
         }
-        val resolvedType = applyJvmWildcardAnnotations(
-            type = type,
-            typeStack = typeStack,
-        )
+        val resolvedType = type.applyJvmWildcardAnnotations(typeStack, typeArgStack + typeArg)
         val resolvedVariance = when {
             typeParameter.variance == Variance.INVARIANT &&
                 typeArg.variance != Variance.INVARIANT -> typeArg.variance
             typeArg.hasJvmWildcardAnnotation() -> typeParameter.variance
-                typeStack.queue.any { it.hasSuppressJvmWildcardAnnotation() } ||
+            // We only need to check the first type in the stack for @JvmSuppressWildcards.
+            // Any other @JvmSuppressWildcards usages will be placed on the type arguments rather
+            // than the types, so no need to check the rest of the types.
+            typeStack.queue.first().hasSuppressJvmWildcardAnnotation() ||
                 typeArg.hasSuppressWildcardsAnnotationInHierarchy() ||
+                typeArgStack.any { it.hasSuppressJvmWildcardAnnotation() } ||
                 typeParameter.hasSuppressWildcardsAnnotationInHierarchy() -> Variance.INVARIANT
             else -> typeArg.variance
         }
-        return createTypeArgument(resolvedType, resolvedVariance)
+        return typeArg.createWrapper(resolvedType, resolvedVariance)
     }
 
-    private fun KSType.isTypeParameter(): Boolean {
-        return createTypeReference().isTypeParameterReference()
+    private fun KSTypeArgument.createWrapper(
+        newType: KSType,
+        newVariance: Variance
+    ): KSTypeArgument {
+        return KSTypeArgumentWrapper(
+            delegate = (this as? KSTypeArgumentWrapper)?.delegate ?: this,
+            type = newType.createTypeReference(),
+            variance = newVariance
+        )
     }
 
-    private fun createTypeArgument(type: KSType, variance: Variance): KSTypeArgument {
-        return resolver.getTypeArgument(type.createTypeReference(), variance)
+    private fun KSType.createWrapper(newArguments: List<KSTypeArgument>): KSType {
+        return KSTypeWrapper(
+            delegate = (this as? KSTypeWrapper)?.delegate ?: this,
+            arguments = newArguments
+        )
+    }
+
+    private fun KSType.removeWrappers(typeStack: ReferenceStack = ReferenceStack()): KSType {
+        if (isError || typeStack.queue.contains(this)) {
+            return this
+        }
+        return typeStack.withReference(this) {
+            val delegateType = (this as? KSTypeWrapper)?.delegate ?: this
+            delegateType.replace(arguments.map { it.removeWrappers(typeStack) })
+        }
+    }
+
+    private fun KSTypeArgument.removeWrappers(
+        typeStack: ReferenceStack = ReferenceStack()
+    ): KSTypeArgument {
+        val type = type?.resolve()
+        if (
+            type == null ||
+            type.isError ||
+            variance == Variance.STAR ||
+            typeStack.queue.contains(type)
+        ) {
+            return this
+        }
+        return resolver.getTypeArgument(
+            type.removeWrappers(typeStack).createTypeReference(),
+            variance
+        )
+    }
+}
+
+/**
+ * A wrapper for creating a new [KSType] that allows arguments of type [KSTypeArgumentWrapper].
+ *
+ * Note: This wrapper acts similar to [KSType#replace(KSTypeArgument)]. However, we can't call
+ * [KSType#replace(KSTypeArgument)] directly when using [KSTypeArgumentWrapper] or we'll get an
+ * [IllegalStateException] since KSP tries to cast to its own implementation of [KSTypeArgument].
+ */
+private class KSTypeWrapper(
+    val delegate: KSType,
+    override val arguments: List<KSTypeArgument>
+) : KSType by delegate {
+    override fun toString() = if (arguments.isNotEmpty()) {
+        "${delegate.toString().substringBefore("<")}<${arguments.joinToString(",")}>"
+    } else {
+        delegate.toString()
+    }
+}
+
+/**
+ * A wrapper for creating a new [KSTypeArgument] that delegates to the original argument for
+ * annotations.
+ *
+ * Note: This wrapper acts similar to [Resolver#getTypeArgument(KSTypeReference, Variance)].
+ * However, we can't call [Resolver#getTypeArgument(KSTypeReference, Variance)] directly because
+ * we'll lose information about annotations (e.g. `@JvmSuppressWildcards`) that were on the original
+ * type argument.
+ */
+private class KSTypeArgumentWrapper(
+    val delegate: KSTypeArgument,
+    override val type: KSTypeReference,
+    override val variance: Variance,
+) : KSTypeArgument by delegate {
+    override fun toString() = when (variance) {
+        Variance.INVARIANT -> "${type.resolve()}"
+        Variance.CONTRAVARIANT -> "in ${type.resolve()}"
+        Variance.COVARIANT -> "out ${type.resolve()}"
+        Variance.STAR -> "*"
     }
 }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
index f39ab11..19f546a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeVarianceResolverScope.kt
@@ -27,7 +27,7 @@
  * Provides KSType resolution scope for a type.
  */
 internal sealed class KSTypeVarianceResolverScope(
-    private val annotated: KSAnnotated,
+    val annotated: KSAnnotated,
     private val container: KSDeclaration?,
     private val asMemberOf: KspType?
 ) {
@@ -36,8 +36,15 @@
      * parameter is in kotlin or the containing class, which inherited the method, is in kotlin.
      */
     val needsWildcardResolution: Boolean by lazy {
+        fun nodeForSuppressionCheck(): KSAnnotated? = when (this) {
+            // For property setter and getter methods skip to the enclosing class to check for
+            // suppression annotations to match KAPT.
+            is PropertySetterParameterType,
+            is PropertyGetterMethodReturnType -> annotated.parent?.parent as? KSAnnotated
+            else -> annotated
+        }
         (annotated.isInKotlinCode() || container?.isInKotlinCode() == true) &&
-            !annotated.hasSuppressWildcardsAnnotationInHierarchy()
+            nodeForSuppressionCheck()?.hasSuppressWildcardsAnnotationInHierarchy() != true
     }
 
     private fun KSAnnotated.isInKotlinCode(): Boolean {
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
index 2bd5ed4..be3f4bf 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeNamesGoldenTest.kt
@@ -159,6 +159,11 @@
                 class MyGenericIn<in T>
                 class MyGenericOut<out T>
                 class MyGenericMultipleParameters<T1: MyGeneric<*>, T2: MyGeneric<T1>>
+                interface MyInterface
+                typealias MyInterfaceAlias = MyInterface
+                typealias MyGenericAlias = MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>
+                typealias JSW = JvmSuppressWildcards
+                typealias JW = JvmWildcard
                 typealias MyLambdaTypeAlias = (@JvmWildcard MyType) -> @JvmWildcard MyType
                 enum class MyEnum {
                     VAL1,
@@ -341,6 +346,14 @@
                     sealedListChild: List<GrandParentSealed.Parent2.Child1>,
                     jvmWildcard: List<@JvmWildcard String>,
                     suppressJvmWildcard: List<@JvmSuppressWildcards Number>,
+                    suppressJvmWildcardsGeneric1: @JvmSuppressWildcards List<MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric2: List<@JvmSuppressWildcards MyGenericOut<MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric3: List<MyGenericOut<@JvmSuppressWildcards MyGenericIn<MyGeneric<MyType>>>>,
+                    suppressJvmWildcardsGeneric4: List<MyGenericOut<MyGenericIn<@JvmSuppressWildcards MyGeneric<MyType>>>>,
+                    interfaceAlias: List<MyInterfaceAlias>,
+                    genericAlias: List<MyGenericAlias>,
+                    jvmWildcardTypeAlias: List<@JW String>,
+                    suppressJvmWildcardTypeAlias: List<@JSW Number>,
                 ) {
                     var propWithFinalType: String = ""
                     var propWithOpenType: Number = 3
@@ -353,6 +366,16 @@
                     var propSealedListChild: List<GrandParentSealed.Parent2.Child1> = TODO()
                     @JvmSuppressWildcards
                     var propWithOpenTypeButSuppressAnnotation: Number = 3
+                    var genericVar: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    @JvmSuppressWildcards var suppressJvmWildcardsGenericVar1: List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar2: @JvmSuppressWildcards List<MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar3: List<@JvmSuppressWildcards MyGenericIn<MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar4: List<MyGenericIn<@JvmSuppressWildcards MyGenericOut<MyGenericOut<MyType>>>> = TODO()
+                    var suppressJvmWildcardsGenericVar5: List<MyGenericIn<MyGenericOut<@JvmSuppressWildcards MyGenericOut<MyType>>>> = TODO()
+                    var interfaceAlias: List<MyInterfaceAlias> = TODO()
+                    var genericAlias: List<MyGenericAlias> = TODO()
+                    var jvmWildcardTypeAlias: List<@JW String> = TODO()
+                    var suppressJvmWildcardTypeAlias: List<@JSW Number> = TODO()
                     fun list(list: List<*>): List<*> { TODO() }
                     fun listTypeArg(list: List<R>): List<R> { TODO() }
                     fun listTypeArgNumber(list: List<Number>): List<Number> { TODO() }
@@ -393,6 +416,43 @@
                     fun suspendExplicitJvmSuppressWildcard_OnType2(
                         list: @JvmSuppressWildcards List<Number>
                     ): @JvmSuppressWildcards List<Number> { TODO() }
+                    fun interfaceAlias(
+                        param: List<MyInterfaceAlias>
+                    ): List<MyInterfaceAlias> = TODO()
+                    fun explicitJvmSuppressWildcardsOnAlias(
+                        param: List<@JvmSuppressWildcards MyInterfaceAlias>,
+                    ): List<@JvmSuppressWildcards MyInterfaceAlias> = TODO()
+                    fun genericAlias(param: List<MyGenericAlias>): List<MyGenericAlias> = TODO()
+                    fun explicitJvmSuppressWildcardsOnGenericAlias(
+                        param: List<@JvmSuppressWildcards MyGenericAlias>,
+                    ): List<@JvmSuppressWildcards MyGenericAlias> = TODO()
+                    fun explicitOutOnInvariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW MyGeneric<out MyGeneric<MyType>>
+                    ): @JSW MyGeneric<out MyGeneric<MyType>> { TODO() }
+                    fun explicitOutOnInvariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW MyGeneric<MyGeneric<out MyType>>
+                    ): @JSW MyGeneric<MyGeneric<out MyType>> { TODO() }
+                    fun explicitOutOnVariant_onType1(
+                        list: List<out List<Number>>
+                    ): List<out List<Number>> { TODO() }
+                    fun explicitOutOnVariant_onType2(
+                        list: List<List<out Number>>
+                    ): List<List<out Number>> { TODO() }
+                    fun explicitOutOnVariant_onType1_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW List<out List<Number>>
+                    ): @JSW List<out List<Number>> { TODO() }
+                    fun explicitOutOnVariant_onType2_WithExplicitJvmSuppressWildcardAlias(
+                        list: @JSW List<List<out Number>>
+                    ): @JSW List<List<out Number>> { TODO() }
+                    fun explicitJvmWildcardTypeAlias(
+                        list: List<@JW String>
+                    ): List<@JW String> { TODO() }
+                    fun explicitJvmSuppressWildcardTypeAlias_OnType(
+                        list: List<@JSW Number>
+                    ): List<@JSW Number> { TODO() }
+                    fun explicitJvmSuppressWildcardTypeAlias_OnType2(
+                        list: @JSW List<Number>
+                    ): @JSW List<Number> { TODO() }
                 }
                 """.trimIndent()
             ), listOf(className)
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
index 54717f6..aa246e1 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiDeviceTest.java
@@ -59,7 +59,6 @@
 public class UiDeviceTest extends BaseTest {
 
     private static final long TIMEOUT_MS = 5_000;
-    private static final int GESTURE_MARGIN = 50;
     private static final String PACKAGE_NAME = "androidx.test.uiautomator.testapp";
     // Defined in 'AndroidManifest.xml'.
     private static final String APP_NAME = "UiAutomator Test App";
@@ -300,7 +299,6 @@
         assertEquals("I've been clicked!", button.getText());
     }
 
-    @Ignore // b/266617096
     @Test
     public void testSwipe() {
         launchTestActivity(SwipeTestActivity.class);
@@ -309,7 +307,7 @@
 
         int width = mDevice.getDisplayWidth();
         int height = mDevice.getDisplayHeight();
-        mDevice.swipe(GESTURE_MARGIN, height / 2, width - GESTURE_MARGIN, height / 2, 10);
+        mDevice.swipe(width / 10, height / 2, 9 * width / 10, height / 2, 10);
 
         assertTrue(swipeRegion.wait(Until.textEquals("swipe_right"), TIMEOUT_MS));
     }
@@ -330,7 +328,6 @@
         assertTrue(dragDestination.wait(Until.textEquals("drag_received"), TIMEOUT_MS));
     }
 
-    @Ignore // b/266617096
     @Test
     public void testSwipe_withPointArray() {
         launchTestActivity(SwipeTestActivity.class);
@@ -340,9 +337,9 @@
         int width = mDevice.getDisplayWidth();
         int height = mDevice.getDisplayHeight();
 
-        Point point1 = new Point(GESTURE_MARGIN, height / 2);
+        Point point1 = new Point(width / 10, height / 2);
         Point point2 = new Point(width / 2, height / 2);
-        Point point3 = new Point(width - GESTURE_MARGIN, height / 2);
+        Point point3 = new Point(9 * width / 10, height / 2);
 
         mDevice.swipe(new Point[]{point1, point2, point3}, 10);
 
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
index c91c791..bb9f70b 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObject2Test.java
@@ -551,7 +551,6 @@
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch > 1f);
     }
 
-    @Ignore // b/266617335
     @Test
     public void testSwipe() {
         launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
index 36aaeb6..f74a13b 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiObjectTest.java
@@ -24,12 +24,10 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 
-import androidx.test.filters.FlakyTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.uiautomator.UiObject;
 import androidx.test.uiautomator.UiSelector;
 
-import org.junit.Ignore;
 import org.junit.Test;
 
 public class UiObjectTest extends BaseTest {
@@ -119,7 +117,6 @@
         assertTrue(expectedDragDest.waitForExists(TIMEOUT_MS));
     }
 
-    @Ignore // b/266617747
     @Test
     public void testSwipeUp() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -141,7 +138,6 @@
         assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
     }
 
-    @Ignore // b/266617747
     @Test
     public void testSwipeDown() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -161,7 +157,6 @@
         assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
     }
 
-    @FlakyTest(bugId = 242761733)
     @Test
     public void testSwipeLeft() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -181,7 +176,6 @@
         assertTrue(expectedSwipeRegion.waitForExists(TIMEOUT_MS));
     }
 
-    @Ignore // b/266617747
     @Test
     public void testSwipeRight() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
index 765aab1..3384042 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/UiScrollableTest.java
@@ -273,7 +273,6 @@
         assertUiObjectNotFound(noNode::flingBackward);
     }
 
-    @Ignore // b/266965027
     @Test
     public void testScrollBackward_vertical() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
@@ -285,7 +284,6 @@
         assertEquals("swipe_down", scrollRegion.getText());
     }
 
-    @Ignore // b/266965027
     @Test
     public void testScrollBackward_horizontal() throws Exception {
         launchTestActivity(SwipeTestActivity.class);
diff --git a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
index fda840d..9a1fe18 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
+++ b/test/uiautomator/integration-tests/testapp/src/main/java/androidx/test/uiautomator/testapp/SwipeTestActivity.java
@@ -22,6 +22,7 @@
 import android.view.MotionEvent;
 import android.widget.TextView;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 public class SwipeTestActivity extends Activity {
@@ -31,23 +32,20 @@
     @Override
     public void onCreate(@Nullable Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
         setContentView(R.layout.swipe_test_activity);
 
         TextView swipeRegion = findViewById(R.id.swipe_region);
 
         mGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
             @Override
-            public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
-                // Swipe is using the same logic as fling, except that their directions are
-                // opposite under the same finger movement.
-                boolean horizontal = Math.abs(vX) > Math.abs(vY);
+            public boolean onScroll(@Nullable MotionEvent e1, @NonNull MotionEvent e2,
+                    float distanceX, float distanceY) {
+                boolean horizontal = Math.abs(distanceX) > Math.abs(distanceY);
                 if (horizontal) {
-                    swipeRegion.setText(vX < 0 ? "swipe_left" : "swipe_right");
+                    swipeRegion.setText(distanceX > 0 ? "swipe_left" : "swipe_right");
                 } else {
-                    swipeRegion.setText(vY < 0 ? "swipe_up" : "swipe_down");
+                    swipeRegion.setText(distanceY > 0 ? "swipe_up" : "swipe_down");
                 }
-
                 return true;
             }
         });
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 63a0a49..b818481 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -16,6 +16,8 @@
 
 package androidx.test.uiautomator;
 
+import static java.util.Objects.requireNonNull;
+
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 
@@ -106,8 +108,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector clazz(@NonNull String className) {
-        checkNotNull(className, "className cannot be null");
-
+        requireNonNull(className, "className cannot be null");
         // If className starts with a period, assume the package is 'android.widget'
         if (className.charAt(0) == '.') {
             return clazz("android.widget", className.substring(1));
@@ -126,9 +127,8 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector clazz(@NonNull String packageName, @NonNull String className) {
-        checkNotNull(packageName, "packageName cannot be null");
-        checkNotNull(className, "className cannot be null");
-
+        requireNonNull(packageName, "packageName cannot be null");
+        requireNonNull(className, "className cannot be null");
         return clazz(Pattern.compile(Pattern.quote(
                 String.format("%s.%s", packageName, className))));
     }
@@ -141,8 +141,7 @@
      * @return A reference to this {@link BySelector}
      */
     public @NonNull BySelector clazz(@NonNull Class clazz) {
-        checkNotNull(clazz, "clazz cannot be null");
-
+        requireNonNull(clazz, "clazz cannot be null");
         return clazz(Pattern.compile(Pattern.quote(clazz.getName())));
     }
 
@@ -155,8 +154,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector clazz(@NonNull Pattern className) {
-        checkNotNull(className, "className cannot be null");
-
+        requireNonNull(className, "className cannot be null");
         if (mClazz != null) {
             throw new IllegalStateException("Class selector is already defined");
         }
@@ -173,8 +171,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector desc(@NonNull String contentDescription) {
-        checkNotNull(contentDescription, "contentDescription cannot be null");
-
+        requireNonNull(contentDescription, "contentDescription cannot be null");
         return desc(Pattern.compile(Pattern.quote(contentDescription)));
     }
 
@@ -187,8 +184,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descContains(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return desc(RegexHelper.getPatternContains(substring));
     }
 
@@ -201,8 +197,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descStartsWith(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return desc(RegexHelper.getPatternStartsWith(substring));
     }
 
@@ -215,8 +210,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descEndsWith(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return desc(RegexHelper.getPatternEndsWith(substring));
     }
 
@@ -229,8 +223,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector desc(@NonNull Pattern contentDescription) {
-        checkNotNull(contentDescription, "contentDescription cannot be null");
-
+        requireNonNull(contentDescription, "contentDescription cannot be null");
         if (mDesc != null) {
             throw new IllegalStateException("Description selector is already defined");
         }
@@ -247,8 +240,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector pkg(@NonNull String applicationPackage) {
-        checkNotNull(applicationPackage, "applicationPackage cannot be null");
-
+        requireNonNull(applicationPackage, "applicationPackage cannot be null");
         return pkg(Pattern.compile(Pattern.quote(applicationPackage)));
     }
 
@@ -261,8 +253,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector pkg(@NonNull Pattern applicationPackage) {
-        checkNotNull(applicationPackage, "applicationPackage cannot be null");
-
+        requireNonNull(applicationPackage, "applicationPackage cannot be null");
         if (mPkg != null) {
             throw new IllegalStateException("Package selector is already defined");
         }
@@ -279,8 +270,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector res(@NonNull String resourceName) {
-        checkNotNull(resourceName, "resourceName cannot be null");
-
+        requireNonNull(resourceName, "resourceName cannot be null");
         return res(Pattern.compile(Pattern.quote(resourceName)));
     }
 
@@ -294,9 +284,8 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector res(@NonNull String resourcePackage, @NonNull String resourceId) {
-        checkNotNull(resourcePackage, "resourcePackage cannot be null");
-        checkNotNull(resourceId, "resourceId cannot be null");
-
+        requireNonNull(resourcePackage, "resourcePackage cannot be null");
+        requireNonNull(resourceId, "resourceId cannot be null");
         return res(Pattern.compile(Pattern.quote(
                 String.format("%s:id/%s", resourcePackage, resourceId))));
     }
@@ -310,8 +299,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector res(@NonNull Pattern resourceName) {
-        checkNotNull(resourceName, "resourceName cannot be null");
-
+        requireNonNull(resourceName, "resourceName cannot be null");
         if (mRes != null) {
             throw new IllegalStateException("Resource name selector is already defined");
         }
@@ -328,8 +316,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector text(@NonNull String textValue) {
-        checkNotNull(textValue, "textValue cannot be null");
-
+        requireNonNull(textValue, "textValue cannot be null");
         return text(Pattern.compile(Pattern.quote(textValue)));
     }
 
@@ -342,8 +329,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textContains(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return text(RegexHelper.getPatternContains(substring));
     }
 
@@ -356,8 +342,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textStartsWith(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return text(RegexHelper.getPatternStartsWith(substring));
     }
 
@@ -370,8 +355,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textEndsWith(@NonNull String substring) {
-        checkNotNull(substring, "substring cannot be null");
-
+        requireNonNull(substring, "substring cannot be null");
         return text(RegexHelper.getPatternEndsWith(substring));
     }
 
@@ -383,8 +367,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector text(@NonNull Pattern textValue) {
-        checkNotNull(textValue, "textValue cannot be null");
-
+        requireNonNull(textValue, "textValue cannot be null");
         if (mText != null) {
             throw new IllegalStateException("Text selector is already defined");
         }
@@ -581,7 +564,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector hasParent(@NonNull BySelector parentSelector) {
-        checkNotNull(parentSelector, "parentSelector cannot be null");
+        requireNonNull(parentSelector, "parentSelector cannot be null");
         return hasAncestor(parentSelector, 1);
     }
 
@@ -594,7 +577,7 @@
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector hasAncestor(@NonNull BySelector ancestorSelector) {
-        checkNotNull(ancestorSelector, "ancestorSelector cannot be null");
+        requireNonNull(ancestorSelector, "ancestorSelector cannot be null");
         if (mAncestorSelector != null) {
             throw new IllegalStateException("Parent/ancestor selector is already defined");
         }
@@ -631,7 +614,7 @@
      * @throws IllegalArgumentException if the selector has a parent/ancestor selector
      */
     public @NonNull BySelector hasChild(@NonNull BySelector childSelector) {
-        checkNotNull(childSelector, "childSelector cannot be null");
+        requireNonNull(childSelector, "childSelector cannot be null");
         return hasDescendant(childSelector, 1);
     }
 
@@ -646,7 +629,7 @@
      * @throws IllegalArgumentException if the selector has a parent/ancestor selector
      */
     public @NonNull BySelector hasDescendant(@NonNull BySelector descendantSelector) {
-        checkNotNull(descendantSelector, "descendantSelector cannot be null");
+        requireNonNull(descendantSelector, "descendantSelector cannot be null");
         if (descendantSelector.mAncestorSelector != null) {
             // Search root is ambiguous with nested parent selectors.
             throw new IllegalArgumentException(
@@ -735,11 +718,4 @@
         builder.append("]");
         return builder.toString();
     }
-
-    private static <T> T checkNotNull(T value, @NonNull String message) {
-        if (value == null) {
-            throw new NullPointerException(message);
-        }
-        return value;
-    }
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
index 5cb978a..0eda6d6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
@@ -16,6 +16,8 @@
 
 package androidx.test.uiautomator;
 
+import static java.util.Objects.requireNonNull;
+
 import android.util.SparseArray;
 import android.view.accessibility.AccessibilityNodeInfo;
 
@@ -109,7 +111,7 @@
      */
     @NonNull
     public UiSelector text(@NonNull String text) {
-        checkNotNull(text, "text cannot be null");
+        requireNonNull(text, "text cannot be null");
         return buildSelector(SELECTOR_TEXT, text);
     }
 
@@ -125,7 +127,7 @@
      */
     @NonNull
     public UiSelector textMatches(@NonNull String regex) {
-        checkNotNull(regex, "regex cannot be null");
+        requireNonNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
@@ -140,7 +142,7 @@
      */
     @NonNull
     public UiSelector textStartsWith(@NonNull String text) {
-        checkNotNull(text, "text cannot be null");
+        requireNonNull(text, "text cannot be null");
         return buildSelector(SELECTOR_START_TEXT, text);
     }
 
@@ -155,7 +157,7 @@
      */
     @NonNull
     public UiSelector textContains(@NonNull String text) {
-        checkNotNull(text, "text cannot be null");
+        requireNonNull(text, "text cannot be null");
         return buildSelector(SELECTOR_CONTAINS_TEXT, text);
     }
 
@@ -168,7 +170,7 @@
      */
     @NonNull
     public UiSelector className(@NonNull String className) {
-        checkNotNull(className, "className cannot be null");
+        requireNonNull(className, "className cannot be null");
         return buildSelector(SELECTOR_CLASS, className);
     }
 
@@ -181,7 +183,7 @@
      */
     @NonNull
     public UiSelector classNameMatches(@NonNull String regex) {
-        checkNotNull(regex, "regex cannot be null");
+        requireNonNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
     }
 
@@ -194,7 +196,7 @@
      */
     @NonNull
     public <T> UiSelector className(@NonNull Class<T> type) {
-        checkNotNull(type, "type cannot be null");
+        requireNonNull(type, "type cannot be null");
         return buildSelector(SELECTOR_CLASS, type.getName());
     }
 
@@ -216,7 +218,7 @@
      */
     @NonNull
     public UiSelector description(@NonNull String desc) {
-        checkNotNull(desc, "desc cannot be null");
+        requireNonNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_DESCRIPTION, desc);
     }
 
@@ -236,7 +238,7 @@
      */
     @NonNull
     public UiSelector descriptionMatches(@NonNull String regex) {
-        checkNotNull(regex, "regex cannot be null");
+        requireNonNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
@@ -258,7 +260,7 @@
      */
     @NonNull
     public UiSelector descriptionStartsWith(@NonNull String desc) {
-        checkNotNull(desc, "desc cannot be null");
+        requireNonNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_START_DESCRIPTION, desc);
     }
 
@@ -280,7 +282,7 @@
      */
     @NonNull
     public UiSelector descriptionContains(@NonNull String desc) {
-        checkNotNull(desc, "desc cannot be null");
+        requireNonNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
     }
 
@@ -292,7 +294,7 @@
      */
     @NonNull
     public UiSelector resourceId(@NonNull String id) {
-        checkNotNull(id, "id cannot be null");
+        requireNonNull(id, "id cannot be null");
         return buildSelector(SELECTOR_RESOURCE_ID, id);
     }
 
@@ -305,7 +307,7 @@
      */
     @NonNull
     public UiSelector resourceIdMatches(@NonNull String regex) {
-        checkNotNull(regex, "regex cannot be null");
+        requireNonNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
     }
 
@@ -537,7 +539,7 @@
      */
     @NonNull
     public UiSelector childSelector(@NonNull UiSelector selector) {
-        checkNotNull(selector, "selector cannot be null");
+        requireNonNull(selector, "selector cannot be null");
         return buildSelector(SELECTOR_CHILD, selector);
     }
 
@@ -561,7 +563,7 @@
      */
     @NonNull
     public UiSelector fromParent(@NonNull UiSelector selector) {
-        checkNotNull(selector, "selector cannot be null");
+        requireNonNull(selector, "selector cannot be null");
         return buildSelector(SELECTOR_PARENT, selector);
     }
 
@@ -574,7 +576,7 @@
      */
     @NonNull
     public UiSelector packageName(@NonNull String name) {
-        checkNotNull(name, "name cannot be null");
+        requireNonNull(name, "name cannot be null");
         return buildSelector(SELECTOR_PACKAGE_NAME, name);
     }
 
@@ -587,7 +589,7 @@
      */
     @NonNull
     public UiSelector packageNameMatches(@NonNull String regex) {
-        checkNotNull(regex, "regex cannot be null");
+        requireNonNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
     }
 
@@ -1022,11 +1024,4 @@
         builder.append("]");
         return builder.toString();
     }
-
-    private static <T> T checkNotNull(T value, @NonNull String message) {
-        if (value == null) {
-            throw new NullPointerException(message);
-        }
-        return value;
-    }
 }
diff --git a/testutils/testutils-fonts/build.gradle b/testutils/testutils-fonts/build.gradle
index dffd039..911cf9b 100644
--- a/testutils/testutils-fonts/build.gradle
+++ b/testutils/testutils-fonts/build.gradle
@@ -14,12 +14,20 @@
  * limitations under the License.
  */
 
+
+import androidx.build.KmpPlatformsKt
 import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("kotlin-android")
+}
+
+def desktopEnabled = KmpPlatformsKt.enableDesktop(project)
+
+androidXMultiplatform {
+    android()
+    if (desktopEnabled) desktop()
 }
 
 dependencies {
diff --git a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
index f541247..1a5e97a 100644
--- a/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
+++ b/wear/compose/compose-navigation/src/main/java/androidx/wear/compose/navigation/SwipeDismissableNavHost.kt
@@ -16,6 +16,7 @@
 
 package androidx.wear.compose.navigation
 
+import android.util.Log
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
 import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.Composable
@@ -171,9 +172,23 @@
     // no WearNavigator.Destinations were added to the navigation backstack (be sure to build
     // the NavGraph using androidx.wear.compose.navigation.composable) or because the last entry
     // was popped prior to navigating (instead, use navigate with popUpTo).
-    val current = if (backStack.isNotEmpty()) backStack.last() else throw IllegalArgumentException(
-        "The WearNavigator backstack is empty, there is no navigation destination to display."
-    )
+    // If the activity is using FLAG_ACTIVITY_NEW_TASK then it also needs to set
+    // FLAG_ACTIVITY_CLEAR_TASK, otherwise the activity will be created twice,
+    // the first of these with an empty backstack.
+    val current = backStack.lastOrNull()
+
+    if (current == null) {
+        val warningText =
+            "Current backstack entry is empty. Please ensure: \n" +
+                "1. The current WearNavigator navigation backstack is not empty (e.g. by using " +
+                "androidx.wear.compose.navigation.composable to build your nav graph). \n" +
+                "2. The last entry is not popped prior to navigation " +
+                "(instead, use navigate with popUpTo). \n" +
+                "3. If the activity uses FLAG_ACTIVITY_NEW_TASK you should also set " +
+                "FLAG_ACTIVITY_CLEAR_TASK to maintain the backstack consistency."
+
+        Log.w(TAG, warningText)
+    }
 
     val swipeState = state.swipeToDismissBoxState
     LaunchedEffect(swipeState.currentValue) {
@@ -200,7 +215,7 @@
         modifier = Modifier,
         hasBackground = previous != null,
         backgroundKey = previous?.id ?: SwipeToDismissKeys.Background,
-        contentKey = current.id,
+        contentKey = current?.id ?: SwipeToDismissKeys.Content,
         content = { isBackground ->
             BoxedStackEntryContent(if (isBackground) previous else current, stateHolder, modifier)
         }
@@ -279,3 +294,5 @@
         }
     }
 }
+
+private const val TAG = "SwipeDismissableNavHost"
\ No newline at end of file
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -56,6 +56,7 @@
   public class StateStore {
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
index 814cdba..18e01b6 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/public_plus_experimental_current.txt
@@ -56,6 +56,7 @@
   public class StateStore {
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index 62aa8aa..6dd8857 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -58,6 +58,7 @@
     method public static androidx.wear.protolayout.expression.pipeline.StateStore create(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @UiThread public void setStateEntryValues(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue!>);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @UiThread public void setStateEntryValuesProto(java.util.Map<java.lang.String!,androidx.wear.protolayout.expression.proto.StateEntryProto.StateEntryValue!>);
+    field public static final int MAX_STATE_ENTRY_COUNT = 100; // 0x64
   }
 
   public interface TimeGateway {
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
index 3ed31ac..bbae3dc 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/BoundDynamicTypeImpl.java
@@ -16,6 +16,11 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import android.os.Handler;
+import android.os.Looper;
+
+import androidx.annotation.UiThread;
+
 import java.util.List;
 
 /**
@@ -76,6 +81,15 @@
 
     @Override
     public void close() {
+        if (Looper.getMainLooper().isCurrentThread()) {
+            closeInternal();
+        } else {
+            new Handler(Looper.getMainLooper()).post(this::closeInternal);
+        }
+    }
+
+    @UiThread
+    private void closeInternal() {
         mNodes.stream()
                 .filter(n -> n instanceof DynamicDataSourceNode)
                 .forEach(n -> ((DynamicDataSourceNode<?>) n).destroy());
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
index 068ce4b..3deccea 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluator.java
@@ -250,8 +250,8 @@
 
         /**
          * Gets the quota manager used for limiting the total number of dynamic types in the
-         * pipeline, or {@code null} if there are no restriction on the number of dynamic types.
-         * If present, the quota manager is used to prevent unreasonably expensive expressions.
+         * pipeline, or {@code null} if there are no restriction on the number of dynamic types. If
+         * present, the quota manager is used to prevent unreasonably expensive expressions.
          */
         @Nullable
         public QuotaManager getDynamicTypesQuotaManager() {
@@ -303,8 +303,8 @@
         MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
         TimeGateway timeGateway = config.getTimeGateway();
         if (timeGateway == null) {
-                timeGateway = new TimeGatewayImpl(uiHandler);
-                ((TimeGatewayImpl) timeGateway).enableUpdates();
+            timeGateway = new TimeGatewayImpl(uiHandler);
+            ((TimeGatewayImpl) timeGateway).enableUpdates();
         }
         this.mTimeDataSource = new EpochTimePlatformDataSource(uiExecutor, timeGateway);
         if (config.getSensorGateway() != null) {
@@ -331,7 +331,7 @@
         if (!mDynamicTypesQuotaManager.tryAcquireQuota(boundDynamicType.getDynamicNodeCount())) {
             throw new EvaluationException(
                     "Dynamic type expression limit reached. Try making the dynamic type expression"
-                        + " shorter or reduce the number of dynamic type expressions.");
+                            + " shorter or reduce the number of dynamic type expressions.");
         }
         return boundDynamicType;
     }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
index c2ce371..f722bbb 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/StateStore.java
@@ -18,6 +18,8 @@
 
 import static java.util.stream.Collectors.toMap;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -41,13 +43,27 @@
  * must only be used from the UI thread.
  */
 public class StateStore {
+    /**
+     * Maximum number for state entries allowed for this {@link StateStore}.
+     *
+     * <p>The ProtoLayout state model is not designed to handle large volumes of layout provided
+     * state. So we limit the number of state entries to keep the on-the-wire size and state
+     * store update times manageable.
+     */
+    @SuppressLint("MinMaxConstant")
+    public static final int MAX_STATE_ENTRY_COUNT = 100;
     @NonNull private final Map<String, StateEntryValue> mCurrentState = new ArrayMap<>();
 
     @NonNull
     private final Map<String, Set<DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>>>
             mRegisteredCallbacks = new ArrayMap<>();
 
-    /** Creates a {@link StateStore}. */
+    /**
+     * Creates a {@link StateStore}.
+     *
+     * @throws IllegalStateException if number of initialState entries is greater than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}.
+     */
     @NonNull
     public static StateStore create(
             @NonNull Map<String, StateEntryBuilders.StateEntryValue> initialState) {
@@ -56,6 +72,9 @@
 
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     public StateStore(@NonNull Map<String, StateEntryValue> initialState) {
+        if (initialState.size() > MAX_STATE_ENTRY_COUNT) {
+            throw stateTooLargeException(initialState.size());
+        }
         mCurrentState.putAll(initialState);
     }
 
@@ -63,6 +82,10 @@
      * Sets the given state, replacing the current state.
      *
      * <p>Informs registered listeners of changed values, invalidates removed values.
+     *
+     * @throws IllegalStateException if number of state entries is greater than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+     * will stay in place.
      */
     @UiThread
     public void setStateEntryValues(
@@ -74,10 +97,18 @@
      * Sets the given state, replacing the current state.
      *
      * <p>Informs registered listeners of changed values, invalidates removed values.
+     *
+     * @throws IllegalStateException if number of state entries is larger than
+     * {@link StateStore#MAX_STATE_ENTRY_COUNT}. The state will not update and old state entries
+     * will stay in place.
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     @UiThread
     public void setStateEntryValuesProto(@NonNull Map<String, StateEntryValue> newState) {
+        if (newState.size() > MAX_STATE_ENTRY_COUNT) {
+            throw stateTooLargeException(newState.size());
+        }
+
         // Figure out which nodes have actually changed.
         Set<String> removedKeys = getRemovedKeys(newState);
         Map<String, StateEntryValue> changedEntries = getChangedEntries(newState);
@@ -85,10 +116,9 @@
         Stream.concat(removedKeys.stream(), changedEntries.keySet().stream())
                 .forEach(
                         key -> {
-                            for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
-                                    callback :
-                                            mRegisteredCallbacks.getOrDefault(
-                                                    key, Collections.emptySet())) {
+                            for (DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> callback :
+                                    mRegisteredCallbacks.getOrDefault(
+                                            key, Collections.emptySet())) {
                                 callback.onPreUpdate();
                             }
                         });
@@ -168,4 +198,12 @@
         }
         return result;
     }
+
+    static IllegalStateException stateTooLargeException(int stateSize) {
+        return new IllegalStateException(
+                String.format(
+                        "Too many state entries: %d. The maximum number of allowed state entries "
+                                + "is %d.",
+                        stateSize, MAX_STATE_ENTRY_COUNT));
+    }
 }
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
index f9f699d..18bae95 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/TimeGatewayImpl.java
@@ -138,6 +138,7 @@
     }
 
     @Override
+    @UiThread
     public void close() {
         setUpdatesEnabled(false);
         registeredCallbacks.clear();
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
index d3a2bb4..ae1630d 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/StateStoreTest.java
@@ -16,6 +16,7 @@
 
 package androidx.wear.protolayout.expression.pipeline;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -38,6 +39,9 @@
 import org.mockito.InOrder;
 import org.mockito.Mockito;
 
+import java.util.HashMap;
+import java.util.Map;
+
 @RunWith(AndroidJUnit4.class)
 public class StateStoreTest {
     @Rule public Expect mExpect = Expect.create();
@@ -48,6 +52,8 @@
                             "foo", buildStateEntry("bar"),
                             "baz", buildStateEntry("foobar")));
 
+    public StateStoreTest() {}
+
     @Test
     public void setBuilderApi() {
         mStateStoreUnderTest.setStateEntryValues(
@@ -58,6 +64,25 @@
     }
 
     @Test
+    public void initState_largeNumberOfEntries_throws() {
+        Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+        for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+            state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+        }
+        assertThrows(IllegalStateException.class, () -> StateStore.create(state));
+    }
+
+    @Test
+    public void newState_largeNumberOfEntries_throws() {
+        Map<String, StateEntryBuilders.StateEntryValue> state = new HashMap<>();
+        for (int i = 0; i < StateStore.MAX_STATE_ENTRY_COUNT + 10; i++) {
+            state.put(Integer.toString(i), StateEntryBuilders.StateEntryValue.fromString("baz"));
+        }
+        assertThrows(
+                IllegalStateException.class, () -> mStateStoreUnderTest.setStateEntryValues(state));
+    }
+
+    @Test
     public void canReadInitialState() {
         mExpect.that(mStateStoreUnderTest.getStateEntryValuesProto("foo"))
                 .isEqualTo(buildStateEntry("bar"));
@@ -88,8 +113,7 @@
 
     @Test
     public void setStateFiresListeners() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
         mStateStoreUnderTest.registerCallback("foo", cb);
 
         mStateStoreUnderTest.setStateEntryValuesProto(
@@ -101,8 +125,7 @@
 
     @Test
     public void setStateFiresOnPreStateUpdateFirst() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
 
         InOrder inOrder = Mockito.inOrder(cb);
 
@@ -166,8 +189,7 @@
     @SuppressWarnings("unchecked")
     @Test
     public void canUnregisterListeners() {
-        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb =
-                buildStateUpdateCallbackMock();
+        DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> cb = buildStateUpdateCallbackMock();
         mStateStoreUnderTest.registerCallback("foo", cb);
 
         mStateStoreUnderTest.setStateEntryValuesProto(
@@ -183,8 +205,7 @@
     }
 
     @SuppressWarnings("unchecked")
-    private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue>
-            buildStateUpdateCallbackMock() {
+    private DynamicTypeValueReceiverWithPreUpdate<StateEntryValue> buildStateUpdateCallbackMock() {
         // This needs an unchecked cast because of the generic; this method just centralizes the
         // warning suppression.
         return mock(DynamicTypeValueReceiverWithPreUpdate.class);
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/action.proto b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
index 72e7e95..589a31c 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/action.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/action.proto
@@ -54,11 +54,11 @@
 
 // A launch action to send an intent to an Android activity.
 message AndroidActivity {
-  // The package name to send the intent to, for example, "com.google.weather".
+  // The package name to send the intent to, for example, "com.example.weather".
   string package_name = 1;
 
   // The fully qualified class name (including the package) to send the intent
-  // to, for example, "com.google.weather.WeatherOverviewActivity".
+  // to, for example, "com.example.weather.WeatherOverviewActivity".
   string class_name = 2;
 
   // The extras to be included in the intent.
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
index 9b7d7ac..d89a564 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipelineTest.java
@@ -168,7 +168,8 @@
     }
 
     @Test
-    public void buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+    public void
+            buildPipeline_dpProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
         List<Float> results = new ArrayList<>();
         float endValue = 10.0f;
         DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -183,7 +184,7 @@
 
     @Test
     public void
-            buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndVal() {
+            buildPipeline_degreesProp_animatable_animationsDisabled_hasStaticValue_assignsEndValue() {
         List<Float> results = new ArrayList<>();
         float endValue = 10.0f;
         DynamicFloat dynamicFloat = animatableFixedFloat(5.0f, endValue);
@@ -217,7 +218,7 @@
 
     @Test
     public void
-            buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndVal() {
+            buildPipeline_colorProp_animatable_animationsDisabled_noStaticValueSet_assignsEndValue() {
         List<Integer> results = new ArrayList<>();
         DynamicColor dynamicColor = animatableFixedColor(0, 1);
         ColorProp colorProp = ColorProp.newBuilder().setDynamicValue(dynamicColor).build();
@@ -1719,8 +1720,7 @@
                                                         Repeatable.newBuilder()
                                                                 .setRepeatMode(
                                                                         RepeatMode
-                                                                                .REPEAT_MODE_REVERSE
-                                                                )
+                                                                                .REPEAT_MODE_REVERSE)
                                                                 .setIterations(iterations)
                                                                 .setForwardRepeatOverride(
                                                                         alternateParameters)
@@ -1813,13 +1813,12 @@
         ProtoLayoutDynamicDataPipeline pipeline =
                 enableAnimations
                         ? new ProtoLayoutDynamicDataPipeline(
-                        /* sensorGateway= */ null,
+                                /* sensorGateway= */ null,
                                 mStateStore,
                                 new FixedQuotaManagerImpl(MAX_VALUE),
                                 new FixedQuotaManagerImpl(MAX_VALUE))
                         : new ProtoLayoutDynamicDataPipeline(
-                                /* sensorGateway= */ null,
-                                mStateStore);
+                                /* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1842,7 +1841,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1860,7 +1859,7 @@
         AddToListCallback<Integer> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1878,7 +1877,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1896,7 +1895,7 @@
         AddToListCallback<Float> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
@@ -1914,7 +1913,7 @@
         AddToListCallback<Integer> receiver =
                 new AddToListCallback<>(results, /* invalidList= */ null);
         ProtoLayoutDynamicDataPipeline pipeline =
-                new ProtoLayoutDynamicDataPipeline(  /* sensorGateway= */ null, mStateStore);
+                new ProtoLayoutDynamicDataPipeline(/* sensorGateway= */ null, mStateStore);
         shadowOf(getMainLooper()).idle();
 
         pipeline.setFullyVisible(true);
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 9e263bf..9a529c2 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
@@ -4205,8 +4205,9 @@
     private static FadeInTransition.Builder fadeIn(int delay) {
         return FadeInTransition.newBuilder()
                 .setAnimationSpec(
-                        AnimationSpec.newBuilder().setAnimationParameters(
-                                AnimationParameters.newBuilder().setDelayMillis(delay)));
+                        AnimationSpec.newBuilder()
+                                .setAnimationParameters(
+                                        AnimationParameters.newBuilder().setDelayMillis(delay)));
     }
 
     private LayoutElement textFadeInSlideIn(String text) {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
index 8889ec0..0fb8101 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/StateBuilders.java
@@ -149,9 +149,17 @@
         return this;
       }
 
+      private static final int MAX_STATE_SIZE = 30;
+
       /** Builds an instance from accumulated values. */
       @NonNull
       public State build() {
+        if (mImpl.getIdToValueMap().size() > MAX_STATE_SIZE) {
+          throw new IllegalStateException(
+                  String.format(
+                          "State size is too large: %d. Maximum " + "allowed state size is %d.",
+                          mImpl.getIdToValueMap().size(), MAX_STATE_SIZE));
+        }
         return new State(mImpl.build(), mFingerprint);
       }
     }
diff --git a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
index 184aa81..3fb2267 100644
--- a/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
+++ b/wear/tiles/tiles-renderer/src/main/java/androidx/wear/tiles/renderer/TileRenderer.java
@@ -202,15 +202,18 @@
     public View inflate(@NonNull ViewGroup parent) {
         String errorMessage =
                 "This method only works with the deprecated constructors that accept Layout and"
-                    + " Resources.";
+                        + " Resources.";
         try {
             // Waiting for the result from future for backwards compatibility.
             return inflateLayout(
-                    checkNotNull(mLayout, errorMessage),
-                    checkNotNull(mResources, errorMessage),
-                    parent).get(10, TimeUnit.SECONDS);
-        } catch (ExecutionException | InterruptedException | CancellationException |
-                 TimeoutException e) {
+                            checkNotNull(mLayout, errorMessage),
+                            checkNotNull(mResources, errorMessage),
+                            parent)
+                    .get(10, TimeUnit.SECONDS);
+        } catch (ExecutionException
+                | InterruptedException
+                | CancellationException
+                | TimeoutException e) {
             // Wrap checked exceptions to avoid changing the method signature.
             throw new RuntimeException("Rendering tile has not successfully finished.", e);
         }
@@ -219,13 +222,12 @@
     /**
      * Inflates a Tile into {@code parent}.
      *
-     * @param layout    The portion of the Tile to render.
+     * @param layout The portion of the Tile to render.
      * @param resources The resources for the Tile.
-     * @param parent    The view to attach the tile into.
+     * @param parent The view to attach the tile into.
      * @return The future with the first child that was inflated. This may be null if the Layout is
-     * empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
-     * contains an
-     * unsupported inner type.
+     *     empty or the top-level LayoutElement has no inner set, or the top-level LayoutElement
+     *     contains an unsupported inner type.
      */
     @NonNull
     public ListenableFuture<View> inflateAsync(
@@ -241,7 +243,6 @@
             @NonNull ResourceProto.Resources resources,
             @NonNull ViewGroup parent) {
         ListenableFuture<Void> result = mInstance.renderAndAttach(layout, resources, parent);
-            return FluentFuture.from(result)
-                    .transform(ignored -> parent.getChildAt(0), mUiExecutor);
+        return FluentFuture.from(result).transform(ignored -> parent.getChildAt(0), mUiExecutor);
     }
 }
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 8887171d..949c589 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -1836,13 +1836,13 @@
          *
          * Returns this Builder to allow chaining.
          */
-        fun setListEntryCollection(timelineEntries: Collection<ComplicationData>?) = apply {
-            if (timelineEntries == null) {
+        fun setListEntryCollection(listEntries: Collection<ComplicationData>?) = apply {
+            if (listEntries == null) {
                 fields.remove(EXP_FIELD_LIST_ENTRIES)
             } else {
                 fields.putParcelableArray(
                     EXP_FIELD_LIST_ENTRIES,
-                    timelineEntries
+                    listEntries
                         .map { data ->
                             data.fields.putInt(EXP_FIELD_LIST_ENTRY_TYPE, data.type)
                             data.fields
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 94db677..ddf43de 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -18,7 +18,7 @@
 
 import android.icu.util.ULocale
 import android.support.wearable.complications.ComplicationData as WireComplicationData
-import android.support.wearable.complications.ComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import androidx.annotation.MainThread
 import androidx.annotation.RestrictTo
@@ -41,17 +41,19 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.updateAndGet
 import kotlinx.coroutines.invoke
 import kotlinx.coroutines.launch
 
 /**
  * Evaluates a [WireComplicationData] with
  * [androidx.wear.protolayout.expression.DynamicBuilders.DynamicType] within its fields.
- *
- * Due to [WireComplicationData]'s shallow copy strategy the input is modified in-place.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ComplicationDataExpressionEvaluator(
@@ -65,27 +67,101 @@
      *
      * The expression is evaluated _separately_ on each flow collection.
      */
-    fun evaluate(unevaluatedData: WireComplicationData) =
-        flow<WireComplicationData> {
-            val state: MutableStateFlow<State> = unevaluatedData.buildState()
-            state.value.use {
-                val evaluatedData: Flow<WireComplicationData> =
-                    state
-                        .mapNotNull {
-                            when {
-                                // Emitting INVALID_DATA if there's an invalid receiver.
-                                it.invalidReceivers.isNotEmpty() -> INVALID_DATA
-                                // Emitting the data if all pending receivers are done and all
-                                // pre-updates are satisfied.
-                                it.pendingReceivers.isEmpty() -> it.data
-                                // Skipping states that are not ready for be emitted.
-                                else -> null
-                            }
-                        }
-                        .distinctUntilChanged()
-                emitAll(evaluatedData)
+    fun evaluate(unevaluatedData: WireComplicationData): Flow<WireComplicationData> =
+        evaluateTopLevelFields(unevaluatedData)
+            // Combining with fields that are made of WireComplicationData.
+            .combineWithDataList(unevaluatedData.timelineEntries) { entries ->
+                // Timeline entries are set on the built WireComplicationData.
+                WireComplicationData.Builder(
+                    this@combineWithDataList.build().apply { setTimelineEntryCollection(entries) }
+                )
             }
+            .combineWithDataList(unevaluatedData.listEntries) { setListEntryCollection(it) }
+            // Must be last, as it overwrites INVALID_DATA.
+            .combineWithEvaluatedPlaceholder(unevaluatedData.placeholder)
+            .distinctUntilChanged()
+
+    /** Evaluates "local" fields, excluding fields of type WireComplicationData. */
+    private fun evaluateTopLevelFields(
+        unevaluatedData: WireComplicationData
+    ): Flow<WireComplicationData> = flow {
+        val state: MutableStateFlow<State> = unevaluatedData.buildState()
+        state.value.use {
+            val evaluatedData: Flow<WireComplicationData> =
+                state.mapNotNull {
+                    when {
+                        // Emitting INVALID_DATA if there's an invalid receiver.
+                        it.invalidReceivers.isNotEmpty() -> INVALID_DATA
+                        // Emitting the data if all pending receivers are done and all
+                        // pre-updates are satisfied.
+                        it.pendingReceivers.isEmpty() -> it.data
+                        // Skipping states that are not ready for be emitted.
+                        else -> null
+                    }
+                }
+            emitAll(evaluatedData)
         }
+    }
+
+    /**
+     * Combines the receiver with the evaluated version of the provided list.
+     *
+     * If the receiver [Flow] emits [INVALID_DATA] or the input list is null or empty, this does not
+     * mutate the flow and does not wait for the entries to finish evaluating.
+     *
+     * If even one [WireComplicationData] within the provided list is evaluated to [INVALID_DATA],
+     * the output [Flow] becomes [INVALID_DATA] (the receiver [Flow] is ignored).
+     */
+    private fun Flow<WireComplicationData>.combineWithDataList(
+        unevaluatedEntries: List<WireComplicationData>?,
+        setter:
+            WireComplicationData.Builder.(
+                List<WireComplicationData>
+            ) -> WireComplicationData.Builder,
+    ): Flow<WireComplicationData> {
+        if (unevaluatedEntries.isNullOrEmpty()) return this
+        val evaluatedEntriesFlow: Flow<List<WireComplicationData>> =
+            combine(unevaluatedEntries.map { evaluate(it) })
+
+        return this.combine(evaluatedEntriesFlow).map {
+            (data: WireComplicationData, evaluatedEntries: List<WireComplicationData>?) ->
+            // Not mutating if invalid.
+            if (data === INVALID_DATA) return@map data
+            // An entry is invalid, emitting invalid.
+            if (evaluatedEntries.any { it === INVALID_DATA }) return@map INVALID_DATA
+            // All is well, mutating the input.
+            return@map WireComplicationData.Builder(data).setter(evaluatedEntries).build()
+        }
+    }
+
+    /**
+     * Same as [combineWithDataList], but sets the evaluated placeholder ONLY when the receiver
+     * [Flow] emits [TYPE_NO_DATA], or [keepExpression] is true, otherwise clears it and does not
+     * wait for the placeholder to finish evaluating.
+     *
+     * If the placeholder is not required (per the above paragraph), this doesn't wait for it.
+     */
+    private fun Flow<WireComplicationData>.combineWithEvaluatedPlaceholder(
+        unevaluatedPlaceholder: WireComplicationData?
+    ): Flow<WireComplicationData> {
+        if (unevaluatedPlaceholder == null) return this
+        val evaluatedPlaceholderFlow: Flow<WireComplicationData> = evaluate(unevaluatedPlaceholder)
+
+        return this.combine(evaluatedPlaceholderFlow).map {
+            (data: WireComplicationData, evaluatedPlaceholder: WireComplicationData?) ->
+            if (!keepExpression && data.type != TYPE_NO_DATA) {
+                // Clearing the placeholder when data is not TYPE_NO_DATA (it was meant as an
+                // expression fallback).
+                return@map WireComplicationData.Builder(data).setPlaceholder(null).build()
+            }
+            // Placeholder required but invalid, emitting invalid.
+            if (evaluatedPlaceholder === INVALID_DATA) return@map INVALID_DATA
+            // All is well, mutating the input.
+            return@map WireComplicationData.Builder(data)
+                .setPlaceholder(evaluatedPlaceholder)
+                .build()
+        }
+    }
 
     private suspend fun WireComplicationData.buildState() =
         MutableStateFlow(State(this)).apply {
@@ -177,7 +253,7 @@
      * [ComplicationEvaluationResultReceiver] that are evaluating it.
      */
     private inner class State(
-        val data: ComplicationData,
+        val data: WireComplicationData,
         val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
         val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
         val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
@@ -317,3 +393,35 @@
         runnable.run()
     }
 }
+
+/** Replacement of [kotlinx.coroutines.flow.combine], which doesn't seem to work. */
+internal fun <T> combine(flows: List<Flow<T>>): Flow<List<T>> = flow {
+    data class ValueExists(val value: T?, val exists: Boolean)
+    val latest = MutableStateFlow(List(flows.size) { ValueExists(null, false) })
+    @Suppress("UNCHECKED_CAST") // Flow<List<T?>> -> Flow<List<T>> safe after filtering exists.
+    emitAll(
+        flows
+            .mapIndexed { i, flow -> flow.map { i to it } } // List<Flow<Int, T>> (indexed flows)
+            .merge() // Flow<Int, T>
+            .map { (i, value) ->
+                // Updating latest and returning the current latest.
+                latest.updateAndGet {
+                    val newLatest = it.toMutableList()
+                    newLatest[i] = ValueExists(value, true)
+                    newLatest
+                }
+            } // Flow<List<ValueExists>>
+            // Filtering emissions until we have all values.
+            .filter { values -> values.all { it.exists } }
+            // Flow<List<T>> + defensive copy.
+            .map { values -> values.map { it.value } } as Flow<List<T>>
+    )
+}
+
+/**
+ * Another replacement of [kotlinx.coroutines.flow.combine] which is similar to
+ * `combine(List<Flow<T>>)` but allows different types for each flow.
+ */
+@Suppress("UNCHECKED_CAST")
+internal fun <T1, T2> Flow<T1>.combine(other: Flow<T2>): Flow<Pair<T1, T2>> =
+    combine(listOf(this as Flow<*>, other as Flow<*>)).map { (a, b) -> (a as T1) to (b as T2) }
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index c913c25..3a22917 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -17,6 +17,8 @@
 package androidx.wear.watchface.complications.data
 
 import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_NO_DATA
+import android.support.wearable.complications.ComplicationData.Companion.TYPE_SHORT_TEXT
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.util.Log
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
@@ -58,10 +60,7 @@
 
     @Test
     fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
-        val data =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
-                .setRangedValue(10f)
-                .build()
+        val data = WireComplicationData.Builder(TYPE_NO_DATA).setRangedValue(10f).build()
 
         val evaluator = ComplicationDataExpressionEvaluator()
 
@@ -81,7 +80,7 @@
     ) {
         SET_IMMEDIATELY_WHEN_ALL_DATA_AVAILABLE(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValueExpression(DynamicFloat.constant(1f))
                     .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
                     .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
@@ -90,23 +89,29 @@
                     .setContentDescription(
                         WireComplicationText(DynamicString.constant("Description"))
                     )
-                    .build(),
+                    .setPlaceholder(constantData("Placeholder"))
+                    .setListEntryCollection(listOf(constantData("List")))
+                    .build()
+                    .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) },
             states = listOf(),
             evaluated =
                 listOf(
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                    WireComplicationData.Builder(TYPE_NO_DATA)
                         .setRangedValue(1f)
                         .setLongText(WireComplicationText("Long Text"))
                         .setLongTitle(WireComplicationText("Long Title"))
                         .setShortText(WireComplicationText("Short Text"))
                         .setShortTitle(WireComplicationText("Short Title"))
                         .setContentDescription(WireComplicationText("Description"))
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .setListEntryCollection(listOf(evaluatedData("List")))
                         .build()
+                        .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
                 ),
         ),
         SET_ONLY_AFTER_ALL_FIELDS_EVALUATED(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
                     .setLongText(WireComplicationText(DynamicString.fromState("long_text")))
                     .setLongTitle(WireComplicationText(DynamicString.fromState("long_title")))
@@ -115,7 +120,10 @@
                     .setContentDescription(
                         WireComplicationText(DynamicString.fromState("description"))
                     )
-                    .build(),
+                    .setPlaceholder(stateData("placeholder"))
+                    .setListEntryCollection(listOf(stateData("list")))
+                    .build()
+                    .also { it.setTimelineEntryCollection(listOf(stateData("timeline"))) },
             states =
                 aggregate(
                     // Each map piles on top of the previous ones.
@@ -124,25 +132,38 @@
                     mapOf("long_title" to StateEntryValue.fromString("Long Title")),
                     mapOf("short_text" to StateEntryValue.fromString("Short Text")),
                     mapOf("short_title" to StateEntryValue.fromString("Short Title")),
-                    // Only the last one will trigger an evaluated data.
                     mapOf("description" to StateEntryValue.fromString("Description")),
+                    mapOf("placeholder" to StateEntryValue.fromString("Placeholder")),
+                    mapOf("list" to StateEntryValue.fromString("List")),
+                    mapOf("timeline" to StateEntryValue.fromString("Timeline")),
+                    // Only the last one will trigger an evaluated data.
                 ),
             evaluated =
                 listOf(
-                    INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                    // Before any state is available.
+                    INVALID_DATA,
+                    // INVALID_DATA with placeholder, after it's available (and others aren't).
+                    WireComplicationData.Builder(INVALID_DATA)
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .build(),
+                    // Evaluated data with after everything is available.
+                    WireComplicationData.Builder(TYPE_NO_DATA)
                         .setRangedValue(1f)
                         .setLongText(WireComplicationText("Long Text"))
                         .setLongTitle(WireComplicationText("Long Title"))
                         .setShortText(WireComplicationText("Short Text"))
                         .setShortTitle(WireComplicationText("Short Title"))
                         .setContentDescription(WireComplicationText("Description"))
+                        // Not trimmed for TYPE_NO_DATA.
+                        .setPlaceholder(evaluatedData("Placeholder"))
+                        .setListEntryCollection(listOf(evaluatedData("List")))
                         .build()
+                        .also { it.setTimelineEntryCollection(listOf(evaluatedData("Timeline"))) },
                 ),
         ),
         SET_TO_EVALUATED_IF_ALL_FIELDS_VALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("valid")))
                     .build(),
@@ -153,7 +174,7 @@
             evaluated =
                 listOf(
                     INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
                         .setShortTitle(WireComplicationText("Valid"))
                         .setShortText(WireComplicationText("Valid"))
                         .build(),
@@ -161,7 +182,7 @@
         ),
         SET_TO_NO_DATA_IF_FIRST_STATE_IS_INVALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
                     .build(),
@@ -177,7 +198,7 @@
         ),
         SET_TO_NO_DATA_IF_LAST_STATE_IS_INVALID(
             expressed =
-                WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
                     .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
                     .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
                     .build(),
@@ -192,13 +213,43 @@
             evaluated =
                 listOf(
                     INVALID_DATA, // Before state is available.
-                    WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
                         .setShortTitle(WireComplicationText("Valid"))
                         .setShortText(WireComplicationText("Valid"))
                         .build(),
                     INVALID_DATA, // After it was invalidated.
                 ),
         ),
+        SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_IF_NOT_NO_DATA(
+            expressed =
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .setPlaceholder(evaluatedData("Placeholder"))
+                    .build(),
+            states = listOf(),
+            evaluated =
+                listOf(
+                    // No placeholder.
+                    WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                        .setShortText(WireComplicationText("Text"))
+                        .build(),
+                )
+        ),
+        SET_TO_EVALUATED_WITHOUT_PLACEHOLDER_EVEN_IF_PLACEHOLDER_INVALID_IF_NOT_NO_DATA(
+            expressed =
+            WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText("Text"))
+                .setPlaceholder(stateData("placeholder"))
+                .build(),
+            states = listOf(), // placeholder state not set.
+            evaluated =
+            listOf(
+                // No placeholder.
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .build(),
+            )
+        ),
     }
 
     @Test
@@ -231,7 +282,7 @@
     @Test
     fun evaluate_cancelled_cleansUp() = runBlocking {
         val expressed =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(TYPE_NO_DATA)
                 .setRangedValueExpression(
                     // Uses TimeGateway, which needs cleaning up.
                     DynamicInstant.withSecondsPrecision(Instant.EPOCH)
@@ -262,19 +313,22 @@
     @Test
     fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
         val expressed =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+            WireComplicationData.Builder(TYPE_NO_DATA)
                 .setRangedValueExpression(DynamicFloat.constant(1f))
                 .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
                 .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
                 .setShortText(WireComplicationText(DynamicString.constant("Short Text")))
                 .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
                 .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+                .setPlaceholder(constantData("Placeholder"))
+                .setListEntryCollection(listOf(constantData("List")))
                 .build()
+                .also { it.setTimelineEntryCollection(listOf(constantData("Timeline"))) }
         val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
 
         assertThat(evaluator.evaluate(expressed).firstOrNull())
             .isEqualTo(
-                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                WireComplicationData.Builder(TYPE_NO_DATA)
                     .setRangedValue(1f)
                     .setRangedValueExpression(DynamicFloat.constant(1f))
                     .setLongText(
@@ -292,6 +346,29 @@
                     .setContentDescription(
                         WireComplicationText("Description", DynamicString.constant("Description"))
                     )
+                    .setPlaceholder(evaluatedWithConstantData("Placeholder"))
+                    .setListEntryCollection(listOf(evaluatedWithConstantData("List")))
+                    .build()
+                    .also {
+                        it.setTimelineEntryCollection(listOf(evaluatedWithConstantData("Timeline")))
+                    },
+            )
+    }
+
+    @Test
+    fun evaluate_keepExpressionNotNoData_doesNotTrimPlaceholder() = runBlocking {
+        val expressed =
+            WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                .setShortText(WireComplicationText("Text"))
+                .setPlaceholder(evaluatedData("Placeholder"))
+                .build()
+        val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
+
+        assertThat(evaluator.evaluate(expressed).firstOrNull())
+            .isEqualTo(
+                WireComplicationData.Builder(TYPE_SHORT_TEXT)
+                    .setShortText(WireComplicationText("Text"))
+                    .setPlaceholder(evaluatedData("Placeholder"))
                     .build()
             )
     }
@@ -300,5 +377,25 @@
         /** Converts `[{a: A}, {b: B}, {c: C}]` to `[{a: A}, {a: A, b: B}, {a: A, b: B, c: C}]`. */
         fun <K, V> aggregate(vararg maps: Map<K, V>): List<Map<K, V>> =
             maps.fold(listOf()) { acc, map -> acc + ((acc.lastOrNull() ?: mapOf()) + map) }
+
+        fun constantData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(DynamicString.constant(value)))
+                .build()
+
+        fun stateData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(DynamicString.fromState(value)))
+                .build()
+
+        fun evaluatedData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(value))
+                .build()
+
+        fun evaluatedWithConstantData(value: String) =
+            WireComplicationData.Builder(TYPE_NO_DATA)
+                .setLongText(WireComplicationText(value, DynamicString.constant(value)))
+                .build()
     }
 }
diff --git a/window/window/proguard-rules.pro b/window/window/proguard-rules.pro
index 609e1cc1..b8cf236 100644
--- a/window/window/proguard-rules.pro
+++ b/window/window/proguard-rules.pro
@@ -22,4 +22,6 @@
  androidx.window.layout.adapter.sidecar.DistinctElementSidecarCallback {
   public *** onDeviceStateChanged(androidx.window.sidecar.SidecarDeviceState);
   public *** onWindowLayoutChanged(android.os.IBinder, androidx.window.sidecar.SidecarWindowLayoutInfo);
-}
\ No newline at end of file
+}
+# Required for window area API reflection guard
+-keep interface androidx.window.area.reflectionguard.* {*;}
\ No newline at end of file
diff --git a/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
new file mode 100644
index 0000000..b385670
--- /dev/null
+++ b/window/window/src/androidTest/java/androidx/window/area/reflectionguard/WindowAreaComponentValidatorTest.kt
@@ -0,0 +1,257 @@
+/*
+ * 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.window.area.reflectionguard
+
+import android.app.Activity
+import android.content.Context
+import android.util.DisplayMetrics
+import android.view.View
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.ExtensionWindowAreaStatus
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.extensions.core.util.function.Consumer
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.junit.Test
+
+/**
+ * Unit test for [WindowAreaComponentValidator]
+ */
+class WindowAreaComponentValidatorTest {
+
+    /**
+     * Test that validator returns true if the component fully implements [WindowAreaComponent]
+     */
+    @Test
+    fun isWindowAreaComponentValid_fullImplementation() {
+        assertTrue(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentFullImplementation::class.java, 2))
+        assertTrue(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentFullImplementation::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns correct results for API Level 2 [WindowAreaComponent]
+     * implementation.
+     */
+    @Test
+    fun isWindowAreaComponentValid_apiLevel2() {
+        assertTrue(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentApiV2Implementation::class.java, 2))
+        assertFalse(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                IncompleteWindowAreaComponentApiV2Implementation::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns correct results for API Level 3 [WindowAreaComponent]
+     * implementation.
+     */
+    @Test
+    fun isWindowAreaComponentValid_apiLevel3() {
+        assertTrue(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentApiV3Implementation::class.java, 2))
+        assertTrue(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                WindowAreaComponentApiV3Implementation::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns false if the component implementation is incomplete
+     */
+    @Test
+    fun isWindowAreaComponentValid_falseIfIncompleteImplementation() {
+        assertFalse(
+            WindowAreaComponentValidator.isWindowAreaComponentValid(
+                IncompleteWindowAreaComponentApiV2Implementation::class.java, 2))
+    }
+
+    /**
+     * Test that validator returns true if the [ExtensionWindowAreaStatus] is valid
+     */
+    @Test
+    fun isExtensionWindowAreaStatusValid_trueIfValid() {
+        assertTrue(
+            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+                ValidExtensionWindowAreaStatus::class.java, 2))
+        assertTrue(
+            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+                ValidExtensionWindowAreaStatus::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns false if the [ExtensionWindowAreaStatus] is incomplete
+     */
+    @Test
+    fun isExtensionWindowAreaStatusValid_falseIfIncomplete() {
+        assertFalse(
+            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+                IncompleteExtensionWindowAreaStatus::class.java, 2))
+        assertFalse(
+            WindowAreaComponentValidator.isExtensionWindowAreaStatusValid(
+                IncompleteExtensionWindowAreaStatus::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns true if the [ExtensionWindowAreaPresentation] is valid
+     */
+    @Test
+    fun isExtensionWindowAreaPresentationValid_trueIfValid() {
+        assertTrue(
+            WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+                ValidExtensionWindowAreaPresentation::class.java, 3))
+    }
+
+    /**
+     * Test that validator returns false if the [ExtensionWindowAreaPresentation] is incomplete
+     */
+    @Test
+    fun isExtensionWindowAreaPresentationValid_falseIfIncomplete() {
+        assertFalse(
+            WindowAreaComponentValidator.isExtensionWindowAreaPresentationValid(
+                IncompleteExtensionWindowAreaPresentation::class.java, 3))
+    }
+
+    private class WindowAreaComponentFullImplementation : WindowAreaComponent {
+        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun endRearDisplaySession() {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class WindowAreaComponentApiV2Implementation : WindowAreaComponentApi2Requirements {
+        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun endRearDisplaySession() {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class WindowAreaComponentApiV3Implementation : WindowAreaComponentApi3Requirements {
+        override fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun startRearDisplaySession(activity: Activity, consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun endRearDisplaySession() {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun addRearDisplayPresentationStatusListener(
+            consumer: Consumer<ExtensionWindowAreaStatus>
+        ) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun removeRearDisplayPresentationStatusListener(
+            consumer: Consumer<ExtensionWindowAreaStatus>
+        ) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun startRearDisplayPresentationSession(
+            activity: Activity,
+            consumer: Consumer<Int>
+        ) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun endRearDisplayPresentationSession() {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun getRearDisplayPresentation(): ExtensionWindowAreaPresentation? {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class IncompleteWindowAreaComponentApiV2Implementation {
+        @Suppress("UNUSED_PARAMETER")
+        fun addRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+
+        @Suppress("UNUSED_PARAMETER")
+        fun removeRearDisplayStatusListener(consumer: Consumer<Int>) {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class ValidExtensionWindowAreaPresentation : ExtensionWindowAreaPresentation {
+        override fun getPresentationContext(): Context {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun setPresentationView(view: View) {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class IncompleteExtensionWindowAreaPresentation {
+        fun getPresentationContext(): Context {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class ValidExtensionWindowAreaStatus : ExtensionWindowAreaStatus {
+        override fun getWindowAreaStatus(): Int {
+            throw NotImplementedError("Not implemented")
+        }
+
+        override fun getWindowAreaDisplayMetrics(): DisplayMetrics {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+
+    private class IncompleteExtensionWindowAreaStatus {
+        fun getWindowAreaStatus(): Int {
+            throw NotImplementedError("Not implemented")
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
new file mode 100644
index 0000000..9153250
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaPresentationRequirements.java
@@ -0,0 +1,37 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+
+/**
+ * API requirements for [ExtensionWindowAreaPresentation]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaPresentationRequirements {
+    /** @see ExtensionWindowAreaPresentation#getPresentationContext */
+    @NonNull
+    Context getPresentationContext();
+
+    /** @see ExtensionWindowAreaPresentation#setPresentationView */
+    void setPresentationView(@NonNull View view);
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
new file mode 100644
index 0000000..14ba999
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/ExtensionWindowAreaStatusRequirements.java
@@ -0,0 +1,36 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.util.DisplayMetrics;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+
+/**
+ * API requirements for [ExtensionWindowAreaStatus]
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface ExtensionWindowAreaStatusRequirements {
+    /** @see ExtensionWindowAreaStatus#getWindowAreaStatus */
+    int getWindowAreaStatus();
+
+    /** @see ExtensionWindowAreaStatus#getWindowAreaDisplayMetrics */
+    @NonNull
+    DisplayMetrics getWindowAreaDisplayMetrics();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
new file mode 100644
index 0000000..0ab78c0
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi2Requirements.java
@@ -0,0 +1,48 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
+
+/**
+ * This file defines the Vendor API Level 2 Requirements for WindowAreaComponent. This is used
+ * in the client library to perform reflection guard to ensure that the OEM extension implementation
+ * is complete.
+ *
+ * @see WindowAreaComponent
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface WindowAreaComponentApi2Requirements {
+
+    /** @see WindowAreaComponent#addRearDisplayStatusListener */
+    void addRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#removeRearDisplayStatusListener */
+    void removeRearDisplayStatusListener(@NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#startRearDisplaySession */
+    void startRearDisplaySession(@NonNull Activity activity,
+            @NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#endRearDisplaySession */
+    void endRearDisplaySession();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
new file mode 100644
index 0000000..aad8216
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentApi3Requirements.java
@@ -0,0 +1,58 @@
+/*
+ * 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.window.area.reflectionguard;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation;
+import androidx.window.extensions.area.ExtensionWindowAreaStatus;
+import androidx.window.extensions.area.WindowAreaComponent;
+import androidx.window.extensions.core.util.function.Consumer;
+
+
+/**
+ * This file defines the Vendor API Level 3 Requirements for WindowAreaComponent. This is used
+ * in the client library to perform reflection guard to ensure that the OEM extension implementation
+ * is complete.
+ *
+ * @see WindowAreaComponent
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public interface WindowAreaComponentApi3Requirements extends WindowAreaComponentApi2Requirements {
+
+    /** @see WindowAreaComponent#addRearDisplayPresentationStatusListener */
+    void addRearDisplayPresentationStatusListener(
+            @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+    /** @see WindowAreaComponent#removeRearDisplayPresentationStatusListener */
+    void removeRearDisplayPresentationStatusListener(
+            @NonNull Consumer<ExtensionWindowAreaStatus> consumer);
+
+    /** @see WindowAreaComponent#startRearDisplayPresentationSession */
+    void startRearDisplayPresentationSession(@NonNull Activity activity,
+            @NonNull Consumer<Integer> consumer);
+
+    /** @see WindowAreaComponent#endRearDisplayPresentationSession */
+    void endRearDisplayPresentationSession();
+
+    /** @see WindowAreaComponent#getRearDisplayPresentation */
+    @Nullable
+    ExtensionWindowAreaPresentation getRearDisplayPresentation();
+}
diff --git a/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
new file mode 100644
index 0000000..d48d2ab
--- /dev/null
+++ b/window/window/src/main/java/androidx/window/area/reflectionguard/WindowAreaComponentValidator.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.window.area.reflectionguard
+
+import androidx.window.extensions.area.ExtensionWindowAreaPresentation
+import androidx.window.extensions.area.WindowAreaComponent
+import androidx.window.reflection.ReflectionUtils.validateImplementation
+
+/**
+ * Utility class to validate [WindowAreaComponent] implementation.
+ */
+internal object WindowAreaComponentValidator {
+
+    internal fun isWindowAreaComponentValid(windowAreaComponent: Class<*>, apiLevel: Int): Boolean {
+        return when {
+            apiLevel <= 1 -> false
+            apiLevel == 2 -> validateImplementation(
+                windowAreaComponent, WindowAreaComponentApi2Requirements::class.java
+            )
+            else -> validateImplementation(
+                windowAreaComponent, WindowAreaComponentApi3Requirements::class.java
+            )
+        }
+    }
+
+    internal fun isExtensionWindowAreaStatusValid(
+        extensionWindowAreaStatus: Class<*>,
+        apiLevel: Int
+    ): Boolean {
+        return when {
+            apiLevel <= 1 -> false
+            else -> validateImplementation(
+                extensionWindowAreaStatus, ExtensionWindowAreaStatusRequirements::class.java
+            )
+        }
+    }
+
+    internal fun isExtensionWindowAreaPresentationValid(
+        extensionWindowAreaPresentation: Class<*>,
+        apiLevel: Int
+    ): Boolean {
+        return when {
+            apiLevel <= 2 -> false
+            else -> validateImplementation(
+                extensionWindowAreaPresentation, ExtensionWindowAreaPresentation::class.java
+            )
+        }
+    }
+}
diff --git a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
index 326486e..ed8b7ee 100644
--- a/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
+++ b/window/window/src/main/java/androidx/window/reflection/ReflectionUtils.kt
@@ -80,4 +80,16 @@
     internal fun Method.doesReturn(clazz: Class<*>): Boolean {
         return returnType.equals(clazz)
     }
-}
\ No newline at end of file
+
+    internal fun validateImplementation(
+        implementation: Class<*>,
+        requirements: Class<*>,
+    ): Boolean {
+        return requirements.methods.all {
+            validateReflection("${implementation.name}#${it.name} is not valid") {
+                val implementedMethod = implementation.getMethod(it.name, *it.parameterTypes)
+                implementedMethod.isPublic && implementedMethod.doesReturn(it.returnType)
+            }
+        }
+    }
+}
diff --git a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
index 639d5b1..0660ca2 100644
--- a/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
+++ b/work/work-gcm/src/androidTest/java/androidx/work/impl/background/gcm/GcmTaskConverterTest.kt
@@ -23,6 +23,7 @@
 import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.SystemClock
 import androidx.work.impl.WorkManagerImpl
 import androidx.work.impl.background.gcm.GcmTaskConverter.EXECUTION_WINDOW_SIZE_IN_SECONDS
 import com.google.android.gms.gcm.Task
@@ -45,7 +46,7 @@
 
     @Before
     fun setUp() {
-        mTaskConverter = spy(GcmTaskConverter())
+        mTaskConverter = spy(GcmTaskConverter(SystemClock()))
     }
 
     @Test
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
index f8ffaba..71f595e 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmScheduler.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.work.Clock;
 import androidx.work.Logger;
 import androidx.work.impl.Scheduler;
 import androidx.work.impl.model.WorkSpec;
@@ -38,14 +39,14 @@
     private final GcmNetworkManager mNetworkManager;
     private final GcmTaskConverter mTaskConverter;
 
-    public GcmScheduler(@NonNull Context context) {
+    public GcmScheduler(@NonNull Context context, @NonNull Clock clock) {
         boolean isPlayServicesAvailable = GoogleApiAvailability.getInstance()
                 .isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS;
         if (!isPlayServicesAvailable) {
             throw new IllegalStateException("Google Play Services not available");
         }
         mNetworkManager = GcmNetworkManager.getInstance(context);
-        mTaskConverter = new GcmTaskConverter();
+        mTaskConverter = new GcmTaskConverter(clock);
     }
 
     @Override
diff --git a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
index 6dec44e..fb6e1ae 100644
--- a/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
+++ b/work/work-gcm/src/main/java/androidx/work/impl/background/gcm/GcmTaskConverter.java
@@ -27,6 +27,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
+import androidx.work.Clock;
 import androidx.work.Constraints;
 import androidx.work.NetworkType;
 import androidx.work.impl.model.WorkSpec;
@@ -40,7 +41,6 @@
  * Converts a {@link androidx.work.impl.model.WorkSpec} to a {@link Task}.
  */
 public class GcmTaskConverter {
-
     /**
      * This is referring to the size of the execution window in seconds. {@link GcmNetworkManager}
      * requires that we specify a window of time relative to {@code now} where a {@link Task}
@@ -53,6 +53,11 @@
     public static final long EXECUTION_WINDOW_SIZE_IN_SECONDS = 5L;
 
     static final String EXTRA_WORK_GENERATION = "androidx.work.impl.background.gcm.GENERATION";
+    private final Clock mClock;
+
+    public GcmTaskConverter(@NonNull Clock clock) {
+        mClock = clock;
+    }
 
     OneoffTask convert(@NonNull WorkSpec workSpec) {
         Bundle extras = new Bundle();
@@ -81,7 +86,7 @@
      */
     @VisibleForTesting
     public long now() {
-        return System.currentTimeMillis();
+        return mClock.currentTimeMillis();
     }
 
     private static Task.Builder applyConstraints(
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
index ffce534..5cba6e9 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/greedy/DelayedWorkTrackerTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.MediumTest
 import androidx.work.OneTimeWorkRequest
 import androidx.work.RunnableScheduler
+import androidx.work.SystemClock
 import androidx.work.worker.TestWorker
 import org.junit.Before
 import org.junit.Test
@@ -40,7 +41,7 @@
     fun setUp() {
         mScheduler = mock(GreedyScheduler::class.java)
         mRunnableScheduler = mock(RunnableScheduler::class.java)
-        mDelayedWorkTracker = DelayedWorkTracker(mScheduler, mRunnableScheduler)
+        mDelayedWorkTracker = DelayedWorkTracker(mScheduler, mRunnableScheduler, SystemClock())
     }
 
     @Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
index 2b97041..d000c32 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobInfoConverterTest.java
@@ -44,6 +44,7 @@
 import androidx.work.NetworkType;
 import androidx.work.OneTimeWorkRequest;
 import androidx.work.PeriodicWorkRequest;
+import androidx.work.SystemClock;
 import androidx.work.WorkManagerTest;
 import androidx.work.impl.WorkManagerImpl;
 import androidx.work.impl.model.WorkSpec;
@@ -70,7 +71,7 @@
     @Before
     public void setUp() {
         mConverter = new SystemJobInfoConverter(
-                ApplicationProvider.getApplicationContext());
+                ApplicationProvider.getApplicationContext(), new SystemClock());
     }
 
     @Test
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
index 305ccf2..164b411 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/background/systemjob/SystemJobSchedulerTest.java
@@ -126,7 +126,7 @@
                         workDatabase,
                         configuration,
                         mJobScheduler,
-                        new SystemJobInfoConverter(context)));
+                        new SystemJobInfoConverter(context, configuration.getClock())));
 
         doNothing().when(mSystemJobScheduler).scheduleInternal(any(WorkSpec.class), anyInt());
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
index 60dd8fe..27d7f35 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
+++ b/work/work-runtime/src/main/java/androidx/work/WorkRequest.kt
@@ -224,7 +224,7 @@
          * Sets an initial delay for the [WorkRequest].
          *
          * @param duration The length of the delay
-         * @return The current [Builder]         *
+         * @return The current [Builder]
          * @throws IllegalArgumentException if the given initial delay will push the execution time
          * past `Long.MAX_VALUE` and cause an overflow
          */
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
index 6a1ed29..b0786fe 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/Schedulers.java
@@ -26,6 +26,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
+import androidx.work.Clock;
 import androidx.work.Configuration;
 import androidx.work.Logger;
 import androidx.work.impl.background.systemalarm.SystemAlarmScheduler;
@@ -100,13 +101,13 @@
             List<WorkSpec> contentUriWorkSpecs = null;
             if (Build.VERSION.SDK_INT >= CONTENT_URI_TRIGGER_API_LEVEL) {
                 contentUriWorkSpecs = workSpecDao.getEligibleWorkForSchedulingWithContentUris();
-                markScheduled(workSpecDao, contentUriWorkSpecs);
+                markScheduled(workSpecDao, configuration.getClock(), contentUriWorkSpecs);
             }
 
             // Enqueued workSpecs when scheduling limits are applicable.
             eligibleWorkSpecsForLimitedSlots = workSpecDao.getEligibleWorkForScheduling(
                     configuration.getMaxSchedulerLimit());
-            markScheduled(workSpecDao, eligibleWorkSpecsForLimitedSlots);
+            markScheduled(workSpecDao, configuration.getClock(), eligibleWorkSpecsForLimitedSlots);
             if (contentUriWorkSpecs != null) {
                 eligibleWorkSpecsForLimitedSlots.addAll(contentUriWorkSpecs);
             }
@@ -157,7 +158,7 @@
             setComponentEnabled(context, SystemJobService.class, true);
             Logger.get().debug(TAG, "Created SystemJobScheduler and enabled SystemJobService");
         } else {
-            scheduler = tryCreateGcmBasedScheduler(context);
+            scheduler = tryCreateGcmBasedScheduler(context, configuration.getClock());
             if (scheduler == null) {
                 scheduler = new SystemAlarmScheduler(context);
                 setComponentEnabled(context, SystemAlarmService.class, true);
@@ -168,11 +169,12 @@
     }
 
     @Nullable
-    private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context) {
+    private static Scheduler tryCreateGcmBasedScheduler(@NonNull Context context, Clock clock) {
         try {
             Class<?> klass = Class.forName(GCM_SCHEDULER);
             Scheduler scheduler =
-                    (Scheduler) klass.getConstructor(Context.class).newInstance(context);
+                    (Scheduler) klass.getConstructor(Context.class, Clock.class)
+                            .newInstance(context, clock);
             Logger.get().debug(TAG, "Created " + GCM_SCHEDULER);
             return scheduler;
         } catch (Throwable throwable) {
@@ -184,9 +186,9 @@
     private Schedulers() {
     }
 
-    private static void markScheduled(WorkSpecDao dao, List<WorkSpec> workSpecs) {
+    private static void markScheduled(WorkSpecDao dao, Clock clock, List<WorkSpec> workSpecs) {
         if (workSpecs.size() > 0) {
-            long now = System.currentTimeMillis();
+            long now = clock.currentTimeMillis();
 
             // Mark all the WorkSpecs as scheduled.
             // Calls to Scheduler#schedule() could potentially result in more schedules
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
index 01e484a..3ae8711 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/DelayedWorkTracker.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
+import androidx.work.Clock;
 import androidx.work.Logger;
 import androidx.work.RunnableScheduler;
 import androidx.work.impl.model.WorkSpec;
@@ -43,14 +44,17 @@
     final GreedyScheduler mGreedyScheduler;
 
     private final RunnableScheduler mRunnableScheduler;
+    private final Clock mClock;
     private final Map<String, Runnable> mRunnables;
 
     public DelayedWorkTracker(
             @NonNull GreedyScheduler scheduler,
-            @NonNull RunnableScheduler runnableScheduler) {
+            @NonNull RunnableScheduler runnableScheduler,
+            @NonNull Clock clock) {
 
         mGreedyScheduler = scheduler;
         mRunnableScheduler = runnableScheduler;
+        mClock = clock;
         mRunnables = new HashMap<>();
     }
 
@@ -77,7 +81,7 @@
         };
 
         mRunnables.put(workSpec.id, runnable);
-        long now = System.currentTimeMillis();
+        long now = mClock.currentTimeMillis();
         long delay = nextRunTime - now;
         mRunnableScheduler.scheduleWithDelay(delay, runnable);
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
index b7e8767..bb48033 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/greedy/GreedyScheduler.java
@@ -93,7 +93,8 @@
     ) {
         mContext = context;
         mWorkConstraintsTracker = new WorkConstraintsTrackerImpl(trackers, this);
-        mDelayedWorkTracker = new DelayedWorkTracker(this, configuration.getRunnableScheduler());
+        mDelayedWorkTracker = new DelayedWorkTracker(
+                this, configuration.getRunnableScheduler(), configuration.getClock());
         mConfiguration = configuration;
         mProcessor = processor;
         mWorkLauncher = workLauncher;
@@ -152,7 +153,7 @@
             }
             long throttled = throttleIfNeeded(workSpec);
             long nextRunTime = max(workSpec.calculateNextRunTime(), throttled);
-            long now = System.currentTimeMillis();
+            long now = mConfiguration.getClock().currentTimeMillis();
             if (workSpec.state == WorkInfo.State.ENQUEUED) {
                 if (now < nextRunTime) {
                     // Future work
@@ -290,7 +291,7 @@
             AttemptData firstRunAttempt = mFirstRunAttempts.get(id);
             if (firstRunAttempt == null) {
                 firstRunAttempt = new AttemptData(workSpec.runAttemptCount,
-                        System.currentTimeMillis());
+                        mConfiguration.getClock().currentTimeMillis());
                 mFirstRunAttempts.put(id, firstRunAttempt);
             }
             return firstRunAttempt.mTimeStamp
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
index ade9938..1e73418 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/CommandHandler.java
@@ -24,6 +24,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.WorkerThread;
+import androidx.work.Clock;
 import androidx.work.Logger;
 import androidx.work.impl.ExecutionListener;
 import androidx.work.impl.StartStopToken;
@@ -128,10 +129,13 @@
     private final Context mContext;
     private final Map<WorkGenerationalId, DelayMetCommandHandler> mPendingDelayMet;
     private final Object mLock;
+    private final Clock mClock;
     private final StartStopTokens mStartStopTokens;
 
-    CommandHandler(@NonNull Context context, @NonNull StartStopTokens startStopTokens) {
+    CommandHandler(@NonNull Context context, Clock clock,
+            @NonNull StartStopTokens startStopTokens) {
         mContext = context;
+        mClock = clock;
         mStartStopTokens = startStopTokens;
         mPendingDelayMet = new HashMap<>();
         mLock = new Object();
@@ -332,7 +336,7 @@
         // Constraints changed command handler is synchronous. No cleanup
         // is necessary.
         ConstraintsCommandHandler changedCommandHandler =
-                new ConstraintsCommandHandler(mContext, startId, dispatcher);
+                new ConstraintsCommandHandler(mContext, mClock, startId, dispatcher);
         changedCommandHandler.handleConstraintsChanged();
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
index ad3361b..4bd3fbf 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/ConstraintsCommandHandler.java
@@ -24,6 +24,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.WorkerThread;
+import androidx.work.Clock;
 import androidx.work.Logger;
 import androidx.work.impl.constraints.WorkConstraintsTrackerImpl;
 import androidx.work.impl.constraints.trackers.Trackers;
@@ -35,7 +36,6 @@
 /**
  * This is a command handler which handles the constraints changed event.
  * Typically this happens for WorkSpec's for which we have pending alarms.
- *
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ConstraintsCommandHandler {
@@ -43,16 +43,18 @@
     private static final String TAG = Logger.tagWithPrefix("ConstraintsCmdHandler");
 
     private final Context mContext;
+    private final Clock mClock;
     private final int mStartId;
     private final SystemAlarmDispatcher mDispatcher;
     private final WorkConstraintsTrackerImpl mWorkConstraintsTracker;
 
     ConstraintsCommandHandler(
             @NonNull Context context,
+            Clock clock,
             int startId,
             @NonNull SystemAlarmDispatcher dispatcher) {
-
         mContext = context;
+        mClock = clock;
         mStartId = startId;
         mDispatcher = dispatcher;
         Trackers trackers = mDispatcher.getWorkManager().getTrackers();
@@ -74,7 +76,7 @@
 
         List<WorkSpec> eligibleWorkSpecs = new ArrayList<>(candidates.size());
         // Filter candidates should have already been scheduled.
-        long now = System.currentTimeMillis();
+        long now = mClock.currentTimeMillis();
         for (WorkSpec workSpec : candidates) {
             String workSpecId = workSpec.id;
             long triggerAt = workSpec.calculateNextRunTime();
@@ -87,7 +89,8 @@
         for (WorkSpec workSpec : eligibleWorkSpecs) {
             String workSpecId = workSpec.id;
             Intent intent = CommandHandler.createDelayMetIntent(mContext, generationalId(workSpec));
-            Logger.get().debug(TAG, "Creating a delay_met command for workSpec with id (" + workSpecId + ")");
+            Logger.get().debug(TAG,
+                    "Creating a delay_met command for workSpec with id (" + workSpecId + ")");
             mDispatcher.getTaskExecutor().getMainThreadExecutor().execute(
                     new SystemAlarmDispatcher.AddRunnable(mDispatcher, intent, mStartId));
         }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
index c92310b..d6a5ef4 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemalarm/SystemAlarmDispatcher.java
@@ -90,8 +90,9 @@
     ) {
         mContext = context.getApplicationContext();
         mStartStopTokens = new StartStopTokens();
-        mCommandHandler = new CommandHandler(mContext, mStartStopTokens);
         mWorkManager = workManager != null ? workManager : WorkManagerImpl.getInstance(context);
+        mCommandHandler = new CommandHandler(
+                mContext, mWorkManager.getConfiguration().getClock(), mStartStopTokens);
         mWorkTimer = new WorkTimer(mWorkManager.getConfiguration().getRunnableScheduler());
         mProcessor = processor != null ? processor : mWorkManager.getProcessor();
         mTaskExecutor = mWorkManager.getWorkTaskExecutor();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
index 61a24d5..f292f3f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobInfoConverter.java
@@ -29,6 +29,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
 import androidx.work.BackoffPolicy;
+import androidx.work.Clock;
 import androidx.work.Constraints;
 import androidx.work.Logger;
 import androidx.work.NetworkType;
@@ -50,8 +51,10 @@
     static final String EXTRA_WORK_SPEC_GENERATION = "EXTRA_WORK_SPEC_GENERATION";
 
     private final ComponentName mWorkServiceComponent;
+    private final Clock mClock;
 
-    SystemJobInfoConverter(@NonNull Context context) {
+    SystemJobInfoConverter(@NonNull Context context, Clock clock) {
+        mClock = clock;
         Context appContext = context.getApplicationContext();
         mWorkServiceComponent = new ComponentName(appContext, SystemJobService.class);
     }
@@ -86,7 +89,7 @@
         }
 
         long nextRunTime = workSpec.calculateNextRunTime();
-        long now = System.currentTimeMillis();
+        long now = mClock.currentTimeMillis();
         long offset = Math.max(nextRunTime - now, 0);
 
         if (Build.VERSION.SDK_INT <= 28) {
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
index af76cf1..fc679669 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/background/systemjob/SystemJobScheduler.java
@@ -78,7 +78,7 @@
                 workDatabase,
                 configuration,
                 (JobScheduler) context.getSystemService(JOB_SCHEDULER_SERVICE),
-                new SystemJobInfoConverter(context)
+                new SystemJobInfoConverter(context, configuration.getClock())
         );
     }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
index 03158ae..71e3ec8 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/CancelWorkRunnable.java
@@ -222,7 +222,9 @@
                     }
                     // Update the last cancelled time in Preference.
                     new PreferenceUtils(workManagerImpl.getWorkDatabase())
-                            .setLastCancelAllTimeMillis(System.currentTimeMillis());
+                            .setLastCancelAllTimeMillis(
+                                    workManagerImpl.getConfiguration().getClock()
+                                            .currentTimeMillis());
                     workDatabase.setTransactionSuccessful();
                 } finally {
                     workDatabase.endTransaction();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt b/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
index 2781cdf..24221a9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/workers/DiagnosticsWorker.kt
@@ -38,7 +38,8 @@
         val workNameDao = database.workNameDao()
         val workTagDao = database.workTagDao()
         val systemIdInfoDao = database.systemIdInfoDao()
-        val startAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)
+        val startAt =
+            workManager.configuration.clock.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)
         val completed = workSpecDao.getRecentlyCompletedWork(startAt)
         val running = workSpecDao.getRunningWork()
         val enqueued = workSpecDao.getAllEligibleWorkSpecsForScheduling(