Merge changes I842cb2e0,I476e3c43 into androidx-main

* changes:
  Cache field access on internal sets and maps in runtime
  Use IdentityArraySet to store snapshot invalidations
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 065f82d..081499e 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
@@ -24,7 +24,8 @@
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
 import androidx.appactions.interaction.capabilities.core.values.Message
@@ -70,7 +71,7 @@
     class CapabilityBuilder :
         CapabilityBuilderBase<
             CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
-            >(ACTION_SPEC) {
+        >(ACTION_SPEC) {
         override fun build(): ActionCapability {
             super.setProperty(Property.Builder().build())
             // TODO(b/268369632): No-op remove empty property builder after Property is removed.
@@ -80,9 +81,10 @@
     }
 
     // TODO(b/268369632): Remove Property from public capability APIs.
-    class Property internal constructor(
+    class Property
+    internal constructor(
         val recipient: SimpleProperty?,
-        val messageText: StringProperty?
+        val messageText: TypeProperty<StringValue>?
     ) {
         override fun toString(): String {
             return "Property(recipient=$recipient, messageText=$messageText)"
@@ -108,22 +110,22 @@
 
         class Builder {
             private var recipient: SimpleProperty? = null
-            private var messageText: StringProperty? = null
+            private var messageText: TypeProperty<StringValue>? = null
 
-            fun setRecipient(recipient: SimpleProperty): Builder =
-                apply { this.recipient = recipient }
+            fun setRecipient(recipient: SimpleProperty): Builder = apply {
+                this.recipient = recipient
+            }
 
-            fun setMessageText(messageText: StringProperty): Builder =
-                apply { this.messageText = messageText }
+            fun setMessageText(messageText: TypeProperty<StringValue>): Builder = apply {
+                this.messageText = messageText
+            }
 
             fun build(): Property = Property(recipient, messageText)
         }
     }
 
-    class Argument internal constructor(
-        val recipientList: List<Recipient>,
-        val messageText: String?
-    ) {
+    class Argument
+    internal constructor(val recipientList: List<Recipient>, val messageText: String?) {
         override fun toString(): String {
             return "Argument(recipient=$recipientList, messageTextList=$messageText)"
         }
@@ -150,20 +152,20 @@
             private var recipientList: List<Recipient> = mutableListOf()
             private var messageText: String? = null
 
-            fun setRecipientList(recipientList: List<Recipient>): Builder =
-                apply { this.recipientList = recipientList }
+            fun setRecipientList(recipientList: List<Recipient>): Builder = apply {
+                this.recipientList = recipientList
+            }
 
-            fun setMessageText(messageTextList: String): Builder =
-                apply { this.messageText = messageTextList }
+            fun setMessageText(messageTextList: String): Builder = apply {
+                this.messageText = messageTextList
+            }
 
             override fun build(): Argument = Argument(recipientList, messageText)
         }
     }
 
-    class Output internal constructor(
-        val message: Message?,
-        val executionStatus: ExecutionStatus?
-    ) {
+    class Output
+    internal constructor(val message: Message?, val executionStatus: ExecutionStatus?) {
         override fun toString(): String {
             return "Output(call=$message, executionStatus=$executionStatus)"
         }
@@ -190,9 +192,7 @@
             private var message: Message? = null
             private var executionStatus: ExecutionStatus? = null
 
-            fun setMessage(message: Message): Builder = apply {
-                this.message = message
-            }
+            fun setMessage(message: Message): Builder = apply { this.message = message }
 
             fun setExecutionStatus(executionStatus: ExecutionStatus): Builder = apply {
                 this.executionStatus = executionStatus
@@ -225,9 +225,7 @@
             val value: Value = Value.newBuilder().setStringValue(status).build()
             return ParamValue.newBuilder()
                 .setStructValue(
-                    Struct.newBuilder()
-                        .putFields(TypeConverters.FIELD_NAME_TYPE, value)
-                        .build()
+                    Struct.newBuilder().putFields(TypeConverters.FIELD_NAME_TYPE, value).build()
                 )
                 .build()
         }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
index 38b03d98..aed1277 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/PropertyConverter.java
@@ -21,8 +21,7 @@
 import androidx.annotation.NonNull;
 import androidx.appactions.interaction.capabilities.core.properties.ParamProperty;
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty.PossibleValue;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
 import androidx.appactions.interaction.proto.Entity;
@@ -35,16 +34,6 @@
 
     private PropertyConverter() {}
 
-    /** Create IntentParameter proto from a StringProperty. */
-    @NonNull
-    public static IntentParameter getIntentParameter(
-            @NonNull String paramName, @NonNull StringProperty property) {
-        IntentParameter.Builder builder = newIntentParameterBuilder(paramName, property);
-        extractPossibleValues(property, PropertyConverter::possibleValueToProto).stream()
-                .forEach(builder::addPossibleEntities);
-        return builder.build();
-    }
-
     /** Create IntentParameter proto from a SimpleProperty. */
     @NonNull
     public static IntentParameter getIntentParameter(
@@ -93,11 +82,11 @@
     }
 
     /**
-     * Converts a capabilities library StringProperty.PossibleValue to a appactions Entity proto .
+     * Converts a capabilities library [PossibleStringValue] to a appactions Entity proto .
      */
     @NonNull
-    public static Entity possibleValueToProto(@NonNull PossibleValue possibleValue) {
-        return androidx.appactions.interaction.proto.Entity.newBuilder()
+    public static Entity stringValueToProto(@NonNull StringValue possibleValue) {
+        return Entity.newBuilder()
                 .setIdentifier(possibleValue.getName())
                 .setName(possibleValue.getName())
                 .addAllAlternateNames(possibleValue.getAlternateNames())
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt
new file mode 100644
index 0000000..e0990c1
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/converters/UnionTypeSpec.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.appactions.interaction.capabilities.core.impl.converters
+
+import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException
+import androidx.appactions.interaction.protobuf.Struct
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+class UnionTypeSpec<T : Any> internal constructor(
+    private val bindings: List<MemberBinding<T, *>>,
+) : TypeSpec<T> {
+    internal class MemberBinding<T, M>(
+        val memberGetter: (T) -> M?,
+        val ctor: (M) -> T,
+        val typeSpec: TypeSpec<M>,
+    ) {
+        fun tryDeserialize(struct: Struct): T {
+            return ctor(typeSpec.fromStruct(struct))
+        }
+
+        fun trySerialize(obj: T): Struct? {
+            return memberGetter(obj)?.let { typeSpec.toStruct(it) }
+        }
+    }
+
+    override fun fromStruct(struct: Struct): T {
+        for (binding in bindings) {
+            try {
+                return binding.tryDeserialize(struct)
+            } catch (e: StructConversionException) {
+                continue
+            }
+        }
+        throw StructConversionException("failed to deserialize union type")
+    }
+
+    override fun toStruct(obj: T): Struct {
+        for (binding in bindings) {
+            binding.trySerialize(obj)?.let {
+                return it
+            }
+        }
+        throw StructConversionException("failed to serialize union type")
+    }
+
+    class Builder<T : Any> {
+        private val bindings = mutableListOf<MemberBinding<T, *>>()
+
+        fun <M> bindMemberType(
+            memberGetter: (T) -> M?,
+            ctor: (M) -> T,
+            typeSpec: TypeSpec<M>,
+        ) = apply {
+            bindings.add(
+                MemberBinding(
+                    memberGetter,
+                    ctor,
+                    typeSpec,
+                ),
+            )
+        }
+
+        fun build() = UnionTypeSpec<T>(bindings.toList())
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
index 2bc8502..64268bd 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecBuilder.java
@@ -26,7 +26,7 @@
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
 import androidx.appactions.interaction.capabilities.core.impl.spec.ParamBinding.ArgumentSetter;
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 import androidx.appactions.interaction.capabilities.core.values.EntityValue;
 import androidx.appactions.interaction.proto.AppActionsContext.IntentParameter;
@@ -437,15 +437,17 @@
      * Binds an optional string parameter.
      *
      * @param paramName the BII slot name of this parameter.
-     * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
-     *     {@code PropertyT} instance
+     * @param propertyGetter a function that returns a {@code
+     *     Optional<TypeProperty<PossibleStringValue>>} given a {@code PropertyT} instance
      * @param paramConsumer a function that accepts a String into the argument builder.
      */
     @NonNull
     public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
             bindOptionalStringParameter(
                     @NonNull String paramName,
-                    @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+                    @NonNull
+                            Function<? super PropertyT, Optional<TypeProperty<StringValue>>>
+                                    propertyGetter,
                     @NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
         return bindParameter(
                 paramName,
@@ -455,7 +457,9 @@
                                 .map(
                                         stringProperty ->
                                                 PropertyConverter.getIntentParameter(
-                                                        paramName, stringProperty)),
+                                                        paramName,
+                                                        stringProperty,
+                                                        PropertyConverter::stringValueToProto)),
                 (argBuilder, paramList) -> {
                     if (!paramList.isEmpty()) {
                         paramConsumer.accept(
@@ -470,15 +474,15 @@
      * Binds an required string parameter.
      *
      * @param paramName the BII slot name of this parameter.
-     * @param propertyGetter a function that returns a {@code StringProperty} given a {@code
-     *     PropertyT} instance
+     * @param propertyGetter a function that returns a {@code TypeProperty<PossibleStringValue>}
+     *     given a {@code PropertyT} instance
      * @param paramConsumer a function that accepts a String into the argument builder.
      */
     @NonNull
     public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
             bindRequiredStringParameter(
                     @NonNull String paramName,
-                    @NonNull Function<? super PropertyT, StringProperty> propertyGetter,
+                    @NonNull Function<? super PropertyT, TypeProperty<StringValue>> propertyGetter,
                     @NonNull BiConsumer<? super ArgumentBuilderT, String> paramConsumer) {
         return bindOptionalStringParameter(
                 paramName, property -> Optional.of(propertyGetter.apply(property)), paramConsumer);
@@ -488,8 +492,8 @@
      * Binds an repeated string parameter.
      *
      * @param paramName the BII slot name of this parameter.
-     * @param propertyGetter a function that returns a {@code Optional<StringProperty>} given a
-     *     {@code PropertyT} instance
+     * @param propertyGetter a function that returns a {@code
+     *     Optional<TypeProperty<PossibleStringValue>>} given a {@code PropertyT} instance
      * @param paramConsumer a function that accepts a {@code List<String>} into the argument
      *     builder.
      */
@@ -497,7 +501,9 @@
     public ActionSpecBuilder<PropertyT, ArgumentT, ArgumentBuilderT, OutputT>
             bindRepeatedStringParameter(
                     @NonNull String paramName,
-                    @NonNull Function<? super PropertyT, Optional<StringProperty>> propertyGetter,
+                    @NonNull
+                            Function<? super PropertyT, Optional<TypeProperty<StringValue>>>
+                                    propertyGetter,
                     @NonNull BiConsumer<? super ArgumentBuilderT, List<String>> paramConsumer) {
         return bindParameter(
                 paramName,
@@ -507,7 +513,9 @@
                                 .map(
                                         stringProperty ->
                                                 PropertyConverter.getIntentParameter(
-                                                        paramName, stringProperty)),
+                                                        paramName,
+                                                        stringProperty,
+                                                        PropertyConverter::stringValueToProto)),
                 (argBuilder, paramList) -> {
                     if (!paramList.isEmpty()) {
                         paramConsumer.accept(
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
index 4b04e11..4db410c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/Entity.kt
@@ -17,7 +17,7 @@
 package androidx.appactions.interaction.capabilities.core.properties
 
 /**
- * Entities are used when defining ActionCapability for defining possible values for ParamProperty.
+ * Entities are used defining possible values for [ParamProperty].
  */
 class Entity internal constructor(
     val id: String?,
@@ -55,9 +55,9 @@
         /** Builds and returns an Entity. */
         fun build() = Entity(
             id,
-            requireNotNull(name, {
+            requireNotNull(name) {
                 "setName must be called before build"
-            }),
+            },
             alternateNames,
         )
     }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt
deleted file mode 100644
index 5d211e9..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringProperty.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.properties
-
-/** The property which describes a string parameter for {@code ActionCapability}. */
-class StringProperty internal constructor(
-    override val possibleValues: List<StringProperty.PossibleValue>,
-    override val isRequired: Boolean,
-    override val isValueMatchRequired: Boolean,
-    override val isProhibited: Boolean,
-) : ParamProperty<StringProperty.PossibleValue> {
-    /** Represents a single possible value for StringProperty. */
-    class PossibleValue internal constructor(
-        val name: String,
-        val alternateNames: List<String>,
-    ) {
-        companion object {
-            @JvmStatic
-            fun of(name: String, vararg alternateNames: String) = PossibleValue(
-                name,
-                alternateNames.toList(),
-            )
-        }
-    }
-
-    /** Builder for {@link StringProperty}. */
-    class Builder {
-        private val possibleValues = mutableListOf<PossibleValue>()
-        private var isRequired = false
-        private var isValueMatchRequired = false
-        private var isProhibited = false
-
-        /**
-         * Adds a possible string value for this property.
-         *
-         * @param name           the possible string value.
-         * @param alternateNames the alternate names for this value.
-         */
-        fun addPossibleValue(name: String, vararg alternateNames: String) = apply {
-            possibleValues.add(PossibleValue.of(name, *alternateNames))
-        }
-
-        /** Sets whether or not this property requires a value for fulfillment. */
-        fun setRequired(isRequired: Boolean) = apply {
-            this.isRequired = isRequired
-        }
-
-        /**
-         * Sets whether or not this property requires that the value for this property must match
-         * one of
-         * the string values in the defined possible values.
-         */
-        fun setValueMatchRequired(isValueMatchRequired: Boolean) = apply {
-            this.isValueMatchRequired = isValueMatchRequired
-        }
-
-        /**
-         * Sets whether the String property is prohibited in the response.
-         *
-         * @param isProhibited Whether this property is prohibited in the response.
-         */
-        fun setProhibited(isProhibited: Boolean) = apply {
-            this.isProhibited = isProhibited
-        }
-
-        /** Builds the property for this string parameter. */
-        fun build() =
-            StringProperty(
-                possibleValues.toList(),
-                isRequired,
-                isValueMatchRequired,
-                isProhibited,
-            )
-    }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt
new file mode 100644
index 0000000..fbe5bef
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/properties/StringValue.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.appactions.interaction.capabilities.core.properties
+
+/**
+ * One of the possible possible values for [ParamProperty].
+ */
+class StringValue internal constructor(
+    val name: String,
+    val alternateNames: List<String>,
+) {
+    companion object {
+        @JvmStatic
+        fun of(name: String, vararg alternateNames: String) = StringValue(
+            name,
+            alternateNames.toList(),
+        )
+    }
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
deleted file mode 100644
index 7954fa2..0000000
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.appactions.interaction.capabilities.core.task.impl;
-
-import androidx.annotation.NonNull;
-import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
-import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter;
-import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException;
-import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver;
-import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
-import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult;
-import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver;
-import androidx.appactions.interaction.capabilities.core.task.InventoryResolver;
-import androidx.appactions.interaction.capabilities.core.task.ValidationResult;
-import androidx.appactions.interaction.capabilities.core.task.ValueListener;
-import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException;
-import androidx.appactions.interaction.capabilities.core.values.SearchAction;
-import androidx.appactions.interaction.proto.ParamValue;
-
-import com.google.auto.value.AutoOneOf;
-import com.google.common.util.concurrent.ListenableFuture;
-
-import java.util.List;
-
-/**
- * A wrapper around all types of slot resolvers (value listeners + disambig resolvers).
- *
- * <p>This allows one type of resolver to be bound for each slot, and abstracts the details of the
- * individual resolvers. It is also the place where repeated fields are handled.
- *
- * @param <ValueTypeT>
- */
-@AutoOneOf(GenericResolverInternal.Kind.class)
-public abstract class GenericResolverInternal<ValueTypeT> {
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListener(
-            @NonNull ValueListener<ValueTypeT> valueListener) {
-        return AutoOneOf_GenericResolverInternal.value(valueListener);
-    }
-
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromValueListListener(
-            @NonNull ValueListener<List<ValueTypeT>> valueListListener) {
-        return AutoOneOf_GenericResolverInternal.valueList(valueListListener);
-    }
-
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityResolver(
-            @NonNull AppEntityResolver<ValueTypeT> appEntityResolver) {
-        return AutoOneOf_GenericResolverInternal.appEntityResolver(appEntityResolver);
-    }
-
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromAppEntityListResolver(
-            @NonNull AppEntityListResolver<ValueTypeT> appEntityListResolver) {
-        return AutoOneOf_GenericResolverInternal.appEntityListResolver(appEntityListResolver);
-    }
-
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryResolver(
-            @NonNull InventoryResolver<ValueTypeT> inventoryResolver) {
-        return AutoOneOf_GenericResolverInternal.inventoryResolver(inventoryResolver);
-    }
-
-    @NonNull
-    public static <ValueTypeT> GenericResolverInternal<ValueTypeT> fromInventoryListResolver(
-            @NonNull InventoryListResolver<ValueTypeT> inventoryListResolverListener) {
-        return AutoOneOf_GenericResolverInternal.inventoryListResolver(
-                inventoryListResolverListener);
-    }
-
-    /** Returns the Kind of this resolver */
-    @NonNull
-    public abstract Kind getKind();
-
-    abstract ValueListener<ValueTypeT> value();
-
-    abstract ValueListener<List<ValueTypeT>> valueList();
-
-    abstract AppEntityResolver<ValueTypeT> appEntityResolver();
-
-    abstract AppEntityListResolver<ValueTypeT> appEntityListResolver();
-
-    abstract InventoryResolver<ValueTypeT> inventoryResolver();
-
-    abstract InventoryListResolver<ValueTypeT> inventoryListResolver();
-
-    /** Wrapper which should invoke the `lookupAndRender` provided by the developer. */
-    @NonNull
-    public ListenableFuture<EntitySearchResult<ValueTypeT>> invokeLookup(
-            @NonNull SearchAction<ValueTypeT> searchAction) throws InvalidResolverException {
-        switch (getKind()) {
-            case APP_ENTITY_RESOLVER:
-                return appEntityResolver().lookupAndRender(searchAction);
-            case APP_ENTITY_LIST_RESOLVER:
-                return appEntityListResolver().lookupAndRender(searchAction);
-            default:
-                throw new InvalidResolverException(
-                        String.format(
-                                "invokeLookup is not supported on this resolver of type %s",
-                                getKind().name()));
-        }
-    }
-
-    /**
-     * Wrapper which should invoke the EntityRender#renderEntities method when the Assistant is
-     * prompting for disambiguation.
-     */
-    @NonNull
-    public ListenableFuture<Void> invokeEntityRender(@NonNull List<String> entityIds)
-            throws InvalidResolverException {
-        switch (getKind()) {
-            case INVENTORY_RESOLVER:
-                return inventoryResolver().renderChoices(entityIds);
-            case INVENTORY_LIST_RESOLVER:
-                return inventoryListResolver().renderChoices(entityIds);
-            default:
-                throw new InvalidResolverException(
-                        String.format(
-                                "invokeEntityRender is not supported on this resolver of type %s",
-                                getKind().name()));
-        }
-    }
-
-    /**
-     * Notifies the app that a new value for this argument has been set by Assistant. This method
-     * should only be called with completely grounded values.
-     */
-    @NonNull
-    public ListenableFuture<ValidationResult> notifyValueChange(
-            @NonNull List<ParamValue> paramValues,
-            @NonNull ParamValueConverter<ValueTypeT> converter)
-            throws StructConversionException {
-        SlotTypeConverter<ValueTypeT> singularConverter = SlotTypeConverter.ofSingular(converter);
-        SlotTypeConverter<List<ValueTypeT>> repeatedConverter =
-                SlotTypeConverter.ofRepeated(converter);
-
-        switch (getKind()) {
-            case VALUE:
-                return value().onReceivedAsync(singularConverter.convert(paramValues));
-            case VALUE_LIST:
-                return valueList().onReceivedAsync(repeatedConverter.convert(paramValues));
-            case APP_ENTITY_RESOLVER:
-                return appEntityResolver().onReceivedAsync(singularConverter.convert(paramValues));
-            case APP_ENTITY_LIST_RESOLVER:
-                return appEntityListResolver()
-                        .onReceivedAsync(repeatedConverter.convert(paramValues));
-            case INVENTORY_RESOLVER:
-                return inventoryResolver().onReceivedAsync(singularConverter.convert(paramValues));
-            case INVENTORY_LIST_RESOLVER:
-                return inventoryListResolver()
-                        .onReceivedAsync(repeatedConverter.convert(paramValues));
-        }
-        throw new IllegalStateException("unreachable");
-    }
-
-    /** The kind of resolver. */
-    public enum Kind {
-        VALUE,
-        VALUE_LIST,
-        APP_ENTITY_RESOLVER,
-        APP_ENTITY_LIST_RESOLVER,
-        INVENTORY_RESOLVER,
-        INVENTORY_LIST_RESOLVER
-    }
-}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt
new file mode 100644
index 0000000..02909a6
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/GenericResolverInternal.kt
@@ -0,0 +1,127 @@
+/*
+ * 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.appactions.interaction.capabilities.core.task.impl
+
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
+import androidx.appactions.interaction.capabilities.core.impl.converters.SlotTypeConverter
+import androidx.appactions.interaction.capabilities.core.impl.exceptions.StructConversionException
+import androidx.appactions.interaction.capabilities.core.task.AppEntityListResolver
+import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver
+import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult
+import androidx.appactions.interaction.capabilities.core.task.InventoryListResolver
+import androidx.appactions.interaction.capabilities.core.task.InventoryResolver
+import androidx.appactions.interaction.capabilities.core.task.ValidationResult
+import androidx.appactions.interaction.capabilities.core.task.ValueListener
+import androidx.appactions.interaction.capabilities.core.task.impl.exceptions.InvalidResolverException
+import androidx.appactions.interaction.capabilities.core.values.SearchAction
+import androidx.appactions.interaction.proto.ParamValue
+import androidx.concurrent.futures.await
+
+/**
+ * A wrapper around all types of slot resolvers (value listeners + disambig resolvers).
+ *
+ * This allows one type of resolver to be bound for each slot, and abstracts the details of the
+ * individual resolvers. It is also the place where repeated fields are handled.
+ *
+ * </ValueTypeT>
+ */
+internal class GenericResolverInternal<ValueTypeT>
+private constructor(
+    val value: ValueListener<ValueTypeT>? = null,
+    val valueList: ValueListener<List<ValueTypeT>>? = null,
+    val appEntityResolver: AppEntityResolver<ValueTypeT>? = null,
+    val appEntityListResolver: AppEntityListResolver<ValueTypeT>? = null,
+    val inventoryResolver: InventoryResolver<ValueTypeT>? = null,
+    val inventoryListResolver: InventoryListResolver<ValueTypeT>? = null,
+) {
+
+    /** Wrapper which should invoke the `lookupAndRender` provided by the developer. */
+    @Throws(InvalidResolverException::class)
+    suspend fun invokeLookup(
+        searchAction: SearchAction<ValueTypeT>
+    ): EntitySearchResult<ValueTypeT> {
+        return if (appEntityResolver != null) {
+            appEntityResolver.lookupAndRender(searchAction).await()
+        } else if (appEntityListResolver != null) {
+            appEntityListResolver.lookupAndRender(searchAction).await()
+        } else {
+            throw InvalidResolverException("invokeLookup is not supported on this resolver")
+        }
+    }
+
+    /**
+     * Wrapper which should invoke the EntityRender#renderEntities method when the Assistant is
+     * prompting for disambiguation.
+     */
+    @Throws(InvalidResolverException::class)
+    suspend fun invokeEntityRender(entityIds: List<String>) {
+        if (inventoryResolver != null) {
+            inventoryResolver.renderChoices(entityIds).await()
+        } else if (inventoryListResolver != null)
+            (inventoryListResolver.renderChoices(entityIds).await())
+        else {
+            throw InvalidResolverException("invokeEntityRender is not supported on this resolver")
+        }
+    }
+
+    /**
+     * Notifies the app that a new value for this argument has been set by Assistant. This method
+     * should only be called with completely grounded values.
+     */
+    @Throws(StructConversionException::class)
+    suspend fun notifyValueChange(
+        paramValues: List<ParamValue>,
+        converter: ParamValueConverter<ValueTypeT>
+    ): ValidationResult {
+        val singularConverter = SlotTypeConverter.ofSingular(converter)
+        val repeatedConverter = SlotTypeConverter.ofRepeated(converter)
+        return when {
+            value != null -> value.onReceivedAsync(singularConverter.convert(paramValues))
+            valueList != null -> valueList.onReceivedAsync(repeatedConverter.convert(paramValues))
+            appEntityResolver != null ->
+                appEntityResolver.onReceivedAsync(singularConverter.convert(paramValues))
+            appEntityListResolver != null ->
+                appEntityListResolver.onReceivedAsync(repeatedConverter.convert(paramValues))
+            inventoryResolver != null ->
+                inventoryResolver.onReceivedAsync(singularConverter.convert(paramValues))
+            inventoryListResolver != null ->
+                inventoryListResolver.onReceivedAsync(repeatedConverter.convert(paramValues))
+            else -> throw IllegalStateException("unreachable")
+        }.await()
+    }
+
+    companion object {
+        fun <ValueTypeT> fromValueListener(valueListener: ValueListener<ValueTypeT>) =
+            GenericResolverInternal(value = valueListener)
+
+        fun <ValueTypeT> fromValueListListener(valueListListener: ValueListener<List<ValueTypeT>>) =
+            GenericResolverInternal(valueList = valueListListener)
+
+        fun <ValueTypeT> fromAppEntityResolver(appEntityResolver: AppEntityResolver<ValueTypeT>) =
+            GenericResolverInternal(appEntityResolver = appEntityResolver)
+
+        fun <ValueTypeT> fromAppEntityListResolver(
+            appEntityListResolver: AppEntityListResolver<ValueTypeT>
+        ) = GenericResolverInternal(appEntityListResolver = appEntityListResolver)
+
+        fun <ValueTypeT> fromInventoryResolver(inventoryResolver: InventoryResolver<ValueTypeT>) =
+            GenericResolverInternal(inventoryResolver = inventoryResolver)
+
+        fun <ValueTypeT> fromInventoryListResolver(
+            inventoryListResolver: InventoryListResolver<ValueTypeT>
+        ) = GenericResolverInternal(inventoryListResolver = inventoryListResolver)
+    }
+}
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
index 7cb544d..df72ef9 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskHandler.kt
@@ -16,6 +16,7 @@
 
 package androidx.appactions.interaction.capabilities.core.task.impl
 
+import androidx.annotation.RestrictTo
 import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter
 import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
 import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter
@@ -25,59 +26,55 @@
 import androidx.appactions.interaction.capabilities.core.task.InventoryResolver
 import androidx.appactions.interaction.capabilities.core.task.ValueListener
 import androidx.appactions.interaction.proto.ParamValue
-import java.util.function.Function
 
-import androidx.annotation.RestrictTo
-
-/**
- * Container of multi-turn Task related function references.
- *
- */
+/** Container of multi-turn Task related function references. */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-data class TaskHandler<ConfirmationT> (
-    val taskParamMap: Map<String, TaskParamBinding<*>>,
-    val confirmationType: ConfirmationType,
-    val confirmationDataBindings: Map<String, Function<ConfirmationT, List<ParamValue>>>,
-    val onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>?,
+data class TaskHandler<ConfirmationT>
+internal constructor(
+    internal val taskParamMap: Map<String, TaskParamBinding<*>>,
+    internal val confirmationType: ConfirmationType,
+    internal val confirmationDataBindings: Map<String, (ConfirmationT) -> List<ParamValue>>,
+    internal val onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>?,
 ) {
     class Builder<ConfirmationT>(
-        val confirmationType: ConfirmationType = ConfirmationType.NOT_SUPPORTED,
+        private val confirmationType: ConfirmationType = ConfirmationType.NOT_SUPPORTED,
     ) {
-        val mutableTaskParamMap = mutableMapOf<String, TaskParamBinding<*>>()
-        val confirmationDataBindings: MutableMap<
-            String, Function<ConfirmationT, List<ParamValue>>,> = mutableMapOf()
-        var onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>? = null
+        private val mutableTaskParamMap = mutableMapOf<String, TaskParamBinding<*>>()
+        private val confirmationDataBindings =
+            mutableMapOf<String, (ConfirmationT) -> List<ParamValue>>()
+        private var onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>? =
+            null
 
         fun <ValueTypeT> registerInventoryTaskParam(
             paramName: String,
             listener: InventoryResolver<ValueTypeT>,
             converter: ParamValueConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_IF_NO_IDENTIFIER,
-                GenericResolverInternal.fromInventoryResolver(listener),
-                converter,
-                null,
-                null,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_IF_NO_IDENTIFIER,
+                    GenericResolverInternal.fromInventoryResolver(listener),
+                    converter,
+                    null,
+                    null,
+                )
         }
 
         fun <ValueTypeT> registerInventoryListTaskParam(
             paramName: String,
             listener: InventoryListResolver<ValueTypeT>,
             converter: ParamValueConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_IF_NO_IDENTIFIER,
-                GenericResolverInternal.fromInventoryListResolver(listener),
-                converter,
-                null,
-                null,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_IF_NO_IDENTIFIER,
+                    GenericResolverInternal.fromInventoryListResolver(listener),
+                    converter,
+                    null,
+                    null,
+                )
         }
 
         fun <ValueTypeT> registerAppEntityTaskParam(
@@ -86,16 +83,16 @@
             converter: ParamValueConverter<ValueTypeT>,
             entityConverter: DisambigEntityConverter<ValueTypeT>,
             searchActionConverter: SearchActionConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_IF_NO_IDENTIFIER,
-                GenericResolverInternal.fromAppEntityResolver(listener),
-                converter,
-                entityConverter,
-                searchActionConverter,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_IF_NO_IDENTIFIER,
+                    GenericResolverInternal.fromAppEntityResolver(listener),
+                    converter,
+                    entityConverter,
+                    searchActionConverter,
+                )
         }
 
         fun <ValueTypeT> registerAppEntityListTaskParam(
@@ -104,77 +101,73 @@
             converter: ParamValueConverter<ValueTypeT>,
             entityConverter: DisambigEntityConverter<ValueTypeT>,
             searchActionConverter: SearchActionConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_IF_NO_IDENTIFIER,
-                GenericResolverInternal.fromAppEntityListResolver(listener),
-                converter,
-                entityConverter,
-                searchActionConverter,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_IF_NO_IDENTIFIER,
+                    GenericResolverInternal.fromAppEntityListResolver(listener),
+                    converter,
+                    entityConverter,
+                    searchActionConverter,
+                )
         }
 
         fun <ValueTypeT> registerValueTaskParam(
             paramName: String,
             listener: ValueListener<ValueTypeT>,
             converter: ParamValueConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_NEVER,
-                GenericResolverInternal.fromValueListener(listener),
-                converter,
-                null,
-                null,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_NEVER,
+                    GenericResolverInternal.fromValueListener(listener),
+                    converter,
+                    null,
+                    null,
+                )
         }
 
         fun <ValueTypeT> registerValueListTaskParam(
             paramName: String,
             listener: ValueListener<List<ValueTypeT>>,
             converter: ParamValueConverter<ValueTypeT>,
-        ): Builder<ConfirmationT> {
-            mutableTaskParamMap[paramName] = TaskParamBinding(
-                paramName,
-                GROUND_NEVER,
-                GenericResolverInternal.fromValueListListener(listener),
-                converter,
-                null,
-                null,
-            )
-            return this
+        ): Builder<ConfirmationT> = apply {
+            mutableTaskParamMap[paramName] =
+                TaskParamBinding(
+                    paramName,
+                    GROUND_NEVER,
+                    GenericResolverInternal.fromValueListListener(listener),
+                    converter,
+                    null,
+                    null,
+                )
         }
 
         /**
          * Registers an optional, non-repeated confirmation data.
          *
-         * @param paramName          the BIC confirmation data slot name of this parameter.
-         * @param confirmationGetter a getter of the confirmation data from the {@code ConfirmationT}
-         *                           instance.
-         * @param converter          a converter from confirmation data to a ParamValue.
+         * @param paramName the BIC confirmation data slot name of this parameter.
+         * @param confirmationGetter a getter of the confirmation data from the {@code
+         *   ConfirmationT} instance.
+         * @param converter a converter from confirmation data to a ParamValue.
          */
         fun <T> registerConfirmationOutput(
             paramName: String,
             confirmationGetter: (ConfirmationT) -> T?,
             converter: (T) -> ParamValue,
-        ): Builder<ConfirmationT> {
-            confirmationDataBindings.put(
-                paramName,
-            ) { output: ConfirmationT ->
+        ): Builder<ConfirmationT> = apply {
+            confirmationDataBindings[paramName] = { output: ConfirmationT ->
                 listOfNotNull(confirmationGetter(output)).map(converter)
             }
-            return this
         }
 
         /** Sets the onReadyToConfirmListener for this capability. */
         fun setOnReadyToConfirmListenerInternal(
             onReadyToConfirmListener: OnReadyToConfirmListenerInternal<ConfirmationT>,
-        ): Builder<ConfirmationT> {
+        ): Builder<ConfirmationT> = apply {
             this.onReadyToConfirmListener = onReadyToConfirmListener
-            return this
         }
 
         fun build(): TaskHandler<ConfirmationT> {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
index 3c5ad89..07aced8 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskOrchestrator.kt
@@ -467,7 +467,7 @@
                 taskHandler.confirmationDataBindings.entries.map {
                     FulfillmentResponse.StructuredOutput.OutputValue.newBuilder()
                         .setName(it.key)
-                        .addAllValues(it.value.apply(confirmation))
+                        .addAllValues(it.value.invoke(confirmation))
                         .build()
                 },
             )
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
index e24f9fa..ff78b18 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskParamBinding.kt
@@ -15,7 +15,6 @@
  */
 package androidx.appactions.interaction.capabilities.core.task.impl
 
-import androidx.annotation.RestrictTo
 import androidx.appactions.interaction.capabilities.core.impl.converters.DisambigEntityConverter
 import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
 import androidx.appactions.interaction.capabilities.core.impl.converters.SearchActionConverter
@@ -24,14 +23,14 @@
 /**
  * A binding between a parameter and its Property converter / Argument setter.
  *
- *
-</ValueTypeT> */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-data class TaskParamBinding<ValueTypeT> internal constructor(
+ * </ValueTypeT>
+ */
+internal data class TaskParamBinding<ValueTypeT>
+constructor(
     val name: String,
     val groundingPredicate: (ParamValue) -> Boolean,
     val resolver: GenericResolverInternal<ValueTypeT>,
     val converter: ParamValueConverter<ValueTypeT>,
     val entityConverter: DisambigEntityConverter<ValueTypeT>?,
     val searchActionConverter: SearchActionConverter<ValueTypeT>?,
-)
\ No newline at end of file
+)
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
index fc41c53..94f3c8b 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskSlotProcessor.kt
@@ -25,7 +25,6 @@
 import androidx.appactions.interaction.proto.CurrentValue
 import androidx.appactions.interaction.proto.DisambiguationData
 import androidx.appactions.interaction.proto.ParamValue
-import androidx.concurrent.futures.await
 import kotlin.IllegalStateException
 import kotlin.String
 import kotlin.Throws
@@ -166,7 +165,7 @@
         updatedValue: List<ParamValue>,
         binding: TaskParamBinding<T>
     ): ValidationResult {
-        return binding.resolver.notifyValueChange(updatedValue, binding.converter).await()
+        return binding.resolver.notifyValueChange(updatedValue, binding.converter)
     }
 
     @Throws(InvalidResolverException::class)
@@ -175,7 +174,7 @@
         binding: TaskParamBinding<*>
     ) {
         val entityIds = disambiguationData.entitiesList.map { it.identifier }
-        binding.resolver.invokeEntityRender(entityIds).await()
+        binding.resolver.invokeEntityRender(entityIds)
     }
 
     /**
@@ -237,12 +236,8 @@
         val entityConverter = binding.entityConverter
         val searchActionConverter = binding.searchActionConverter
         val searchAction = searchActionConverter.toSearchAction(ungroundedParamValue)
-        val entitySearchResult = binding.resolver.invokeLookup(searchAction).await()
-        return processEntitySearchResult<T>(
-            entitySearchResult,
-            entityConverter,
-            ungroundedParamValue
-        )
+        val entitySearchResult = binding.resolver.invokeLookup(searchAction)
+        return processEntitySearchResult(entitySearchResult, entityConverter, ungroundedParamValue)
     }
 
     /**
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
index 69d9f74..05f27e4 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityTest.kt
@@ -27,7 +27,7 @@
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
 import androidx.appactions.interaction.capabilities.core.properties.Entity
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.testing.ArgumentUtils
 import androidx.appactions.interaction.capabilities.core.testing.FakeCallbackInternal
@@ -69,7 +69,7 @@
                             TypeProperty.Builder<Entity>().build(),
                         )
                         .setOptionalStringField(
-                            StringProperty.Builder().setProhibited(true).build(),
+                            TypeProperty.Builder<StringValue>().setProhibited(true).build(),
                         )
                         .build(),
                 actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
@@ -122,7 +122,7 @@
                             TypeProperty.Builder<Entity>().build(),
                         )
                         .setOptionalStringField(
-                            StringProperty.Builder().setProhibited(true).build(),
+                            TypeProperty.Builder<StringValue>().setProhibited(true).build(),
                         )
                         .build(),
                 actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
index 7c6afd3..5d8587c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/impl/spec/ActionSpecTest.java
@@ -23,7 +23,7 @@
 import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter;
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters;
 import androidx.appactions.interaction.capabilities.core.properties.Entity;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 import androidx.appactions.interaction.capabilities.core.testing.spec.Output;
 import androidx.appactions.interaction.capabilities.core.values.EntityValue;
@@ -185,7 +185,7 @@
                                                 .build())
                                 .setValueMatchRequired(true)
                                 .build(),
-                        new StringProperty.Builder().build());
+                        new TypeProperty.Builder<StringValue>().build());
 
         assertThat(ACTION_SPEC.convertPropertyToProto(property))
                 .isEqualTo(
@@ -246,14 +246,17 @@
                                                         .build())
                                         .setRequired(true)
                                         .build()),
-                        new StringProperty.Builder().build(),
+                        new TypeProperty.Builder<StringValue>().build(),
                         Optional.of(
-                                new StringProperty.Builder()
-                                        .addPossibleValue("value1")
+                                new TypeProperty.Builder<StringValue>()
+                                        .addPossibleEntities(StringValue.of("value1"))
                                         .setValueMatchRequired(true)
                                         .setRequired(true)
                                         .build()),
-                        Optional.of(new StringProperty.Builder().setProhibited(true).build()));
+                        Optional.of(
+                                new TypeProperty.Builder<StringValue>()
+                                        .setProhibited(true)
+                                        .build()));
 
         assertThat(ACTION_SPEC.convertPropertyToProto(property))
                 .isEqualTo(
@@ -435,9 +438,9 @@
                 Optional<TypeProperty<Entity>> optionalEntityField,
                 Optional<TypeProperty<TestEnum>> optionalEnumField,
                 Optional<TypeProperty<Entity>> repeatedEntityField,
-                StringProperty requiredStringField,
-                Optional<StringProperty> optionalStringField,
-                Optional<StringProperty> repeatedStringField) {
+                TypeProperty<StringValue> requiredStringField,
+                Optional<TypeProperty<StringValue>> optionalStringField,
+                Optional<TypeProperty<StringValue>> repeatedStringField) {
             return new AutoValue_ActionSpecTest_Property(
                     requiredEntityField,
                     optionalEntityField,
@@ -449,7 +452,8 @@
         }
 
         static Property create(
-                TypeProperty<Entity> requiredEntityField, StringProperty requiredStringField) {
+                TypeProperty<Entity> requiredEntityField,
+                TypeProperty<StringValue> requiredStringField) {
             return create(
                     requiredEntityField,
                     Optional.empty(),
@@ -468,11 +472,11 @@
 
         abstract Optional<TypeProperty<Entity>> repeatedEntityField();
 
-        abstract StringProperty requiredStringField();
+        abstract TypeProperty<StringValue> requiredStringField();
 
-        abstract Optional<StringProperty> optionalStringField();
+        abstract Optional<TypeProperty<StringValue>> optionalStringField();
 
-        abstract Optional<StringProperty> repeatedStringField();
+        abstract Optional<TypeProperty<StringValue>> repeatedStringField();
     }
 
     @AutoValue
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 da094b2..c39ef8d 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
@@ -31,7 +31,7 @@
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver
 import androidx.appactions.interaction.capabilities.core.task.EntitySearchResult
@@ -659,7 +659,7 @@
         val property: CapabilityStructFill.Property =
             CapabilityStructFill.Property.newBuilder()
                 .setListItem(SimpleProperty.Builder().setRequired(true).build())
-                .setAnyString(StringProperty.Builder().setRequired(true).build())
+                .setAnyString(TypeProperty.Builder<StringValue>().setRequired(true).build())
                 .build()
         val item1: ListItem = ListItem.newBuilder().setName("red apple").setId("item1").build()
         val item2: ListItem = ListItem.newBuilder().setName("green apple").setId("item2").build()
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
index 54f25db..eca0b7a 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityUtilsTest.kt
@@ -16,7 +16,8 @@
 package androidx.appactions.interaction.capabilities.core.task.impl
 
 import androidx.appactions.interaction.capabilities.core.impl.converters.PropertyConverter
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.proto.AppActionsContext
 import androidx.appactions.interaction.proto.CurrentValue
 import androidx.appactions.interaction.proto.FulfillmentRequest
@@ -36,7 +37,8 @@
         intentParameters.add(
             PropertyConverter.getIntentParameter(
                 "required",
-                StringProperty.Builder().setRequired(true).build()
+                TypeProperty.Builder<StringValue>().setRequired(true).build(),
+                PropertyConverter::stringValueToProto
             )
         )
         assertThat(TaskCapabilityUtils.isSlotFillingComplete(args, intentParameters)).isTrue()
@@ -48,7 +50,8 @@
         intentParameters.add(
             PropertyConverter.getIntentParameter(
                 "required",
-                StringProperty.Builder().setRequired(true).build()
+                TypeProperty.Builder<StringValue>().setRequired(true).build(),
+                PropertyConverter::stringValueToProto
             )
         )
         assertThat(TaskCapabilityUtils.isSlotFillingComplete(emptyMap(), intentParameters))
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
index 26dab57..56c256c 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityStructFill.java
@@ -23,7 +23,8 @@
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 import androidx.appactions.interaction.capabilities.core.task.AppEntityResolver;
 import androidx.appactions.interaction.capabilities.core.values.ListItem;
 
@@ -85,7 +86,7 @@
 
         public abstract Optional<SimpleProperty> listItem();
 
-        public abstract Optional<StringProperty> anyString();
+        public abstract Optional<TypeProperty<StringValue>> anyString();
 
         /** Builder for {@link Property} */
         @AutoValue.Builder
@@ -95,7 +96,7 @@
             public abstract Builder setListItem(@NonNull SimpleProperty value);
 
             @NonNull
-            public abstract Builder setAnyString(@NonNull StringProperty value);
+            public abstract Builder setAnyString(@NonNull TypeProperty<StringValue> value);
 
             @NonNull
             public abstract Property build();
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
index f504c72..f383555 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/CapabilityTwoStrings.java
@@ -20,7 +20,8 @@
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec;
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 
 import com.google.auto.value.AutoValue;
 
@@ -75,19 +76,19 @@
             return new AutoValue_CapabilityTwoStrings_Property.Builder();
         }
 
-        public abstract Optional<StringProperty> stringSlotA();
+        public abstract Optional<TypeProperty<StringValue>> stringSlotA();
 
-        public abstract Optional<StringProperty> stringSlotB();
+        public abstract Optional<TypeProperty<StringValue>> stringSlotB();
 
         /** Builder for {@link Property} */
         @AutoValue.Builder
         public abstract static class Builder {
 
             @NonNull
-            public abstract Builder setStringSlotA(@NonNull StringProperty value);
+            public abstract Builder setStringSlotA(@NonNull TypeProperty<StringValue> value);
 
             @NonNull
-            public abstract Builder setStringSlotB(@NonNull StringProperty value);
+            public abstract Builder setStringSlotB(@NonNull TypeProperty<StringValue> value);
 
             @NonNull
             public abstract Property build();
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
index f5e3839..030b5c3 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/testing/spec/Property.java
@@ -19,7 +19,7 @@
 import androidx.annotation.NonNull;
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf;
 import androidx.appactions.interaction.capabilities.core.properties.Entity;
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty;
+import androidx.appactions.interaction.capabilities.core.properties.StringValue;
 import androidx.appactions.interaction.capabilities.core.properties.TypeProperty;
 
 import com.google.auto.value.AutoValue;
@@ -36,11 +36,11 @@
 
     public abstract TypeProperty<Entity> requiredEntityField();
 
-    public abstract Optional<StringProperty> optionalStringField();
+    public abstract Optional<TypeProperty<StringValue>> optionalStringField();
 
     public abstract Optional<TypeProperty<TestEnum>> enumField();
 
-    public abstract Optional<StringProperty> repeatedStringField();
+    public abstract Optional<TypeProperty<StringValue>> repeatedStringField();
 
     /** Builder for the testing Property. */
     @AutoValue.Builder
@@ -48,11 +48,11 @@
 
         public abstract Builder setRequiredEntityField(TypeProperty<Entity> property);
 
-        public abstract Builder setOptionalStringField(StringProperty property);
+        public abstract Builder setOptionalStringField(TypeProperty<StringValue> property);
 
         public abstract Builder setEnumField(TypeProperty<TestEnum> property);
 
-        public abstract Builder setRepeatedStringField(StringProperty property);
+        public abstract Builder setRepeatedStringField(TypeProperty<StringValue> property);
 
         @NonNull
         @Override
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 fb2829d..574d0ee 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
@@ -22,7 +22,8 @@
 import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import java.util.Optional
 
@@ -49,7 +50,7 @@
             CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
             >(ACTION_SPEC) {
         private var propertyBuilder: Property.Builder = Property.Builder()
-        fun setNameProperty(name: StringProperty): CapabilityBuilder =
+        fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
             apply {
                 propertyBuilder.setName(name)
             }
@@ -63,7 +64,7 @@
 
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
-        val name: StringProperty?,
+        val name: TypeProperty<StringValue>?,
     ) {
         override fun toString(): String {
             return "Property(name=$name)"
@@ -85,9 +86,9 @@
         }
 
         class Builder {
-            private var name: StringProperty? = null
+            private var name: TypeProperty<StringValue>? = null
 
-            fun setName(name: StringProperty): Builder =
+            fun setName(name: TypeProperty<StringValue>): Builder =
                 apply { this.name = name }
 
             fun build(): Property = Property(name)
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 4df6fab..d231eff 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
@@ -22,7 +22,8 @@
 import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import java.util.Optional
 
@@ -49,7 +50,7 @@
             CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
             >(ACTION_SPEC) {
         private var propertyBuilder: Property.Builder = Property.Builder()
-        fun setNameProperty(name: StringProperty): CapabilityBuilder =
+        fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
             apply {
                 propertyBuilder.setName(name)
             }
@@ -63,7 +64,7 @@
 
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
-        val name: StringProperty?,
+        val name: TypeProperty<StringValue>?,
     ) {
         override fun toString(): String {
             return "Property(name=$name)"
@@ -85,9 +86,9 @@
         }
 
         class Builder {
-            private var name: StringProperty? = null
+            private var name: TypeProperty<StringValue>? = null
 
-            fun setName(name: StringProperty): Builder =
+            fun setName(name: TypeProperty<StringValue>): Builder =
                 apply { this.name = name }
 
             fun build(): Property = Property(name)
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 1545abc..734a0ec 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
@@ -24,7 +24,8 @@
 import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import java.time.Duration
 import java.util.Optional
@@ -62,7 +63,7 @@
                 Property.Builder().setDuration(duration).build()
             }
 
-        fun setNameProperty(name: StringProperty): CapabilityBuilder =
+        fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
             apply {
                 Property.Builder().setName(name).build()
             }
@@ -77,7 +78,7 @@
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
         val duration: SimpleProperty?,
-        val name: StringProperty?
+        val name: TypeProperty<StringValue>?
     ) {
         override fun toString(): String {
             return "Property(duration=$duration, name=$name)"
@@ -103,12 +104,12 @@
 
         class Builder {
             private var duration: SimpleProperty? = null
-            private var name: StringProperty? = null
+            private var name: TypeProperty<StringValue>? = null
 
             fun setDuration(duration: SimpleProperty): Builder =
                 apply { this.duration = duration }
 
-            fun setName(name: StringProperty): Builder =
+            fun setName(name: TypeProperty<StringValue>): Builder =
                 apply { this.name = name }
 
             fun build(): Property = Property(duration, name)
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 d8ca5c3..6942f96 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
@@ -22,7 +22,8 @@
 import androidx.appactions.interaction.capabilities.core.CapabilityFactory
 import androidx.appactions.interaction.capabilities.core.impl.BuilderOf
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpecBuilder
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import java.util.Optional
 
@@ -49,7 +50,7 @@
             CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
             >(ACTION_SPEC) {
         private var propertyBuilder: Property.Builder = Property.Builder()
-        fun setNameProperty(name: StringProperty): CapabilityBuilder =
+        fun setNameProperty(name: TypeProperty<StringValue>): CapabilityBuilder =
             apply {
                 propertyBuilder.setName(name)
             }
@@ -63,7 +64,7 @@
 
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
-        val name: StringProperty?,
+        val name: TypeProperty<StringValue>?,
     ) {
         override fun toString(): String {
             return "Property(name=$name)"
@@ -85,9 +86,9 @@
         }
 
         class Builder {
-            private var name: StringProperty? = null
+            private var name: TypeProperty<StringValue>? = null
 
-            fun setName(name: StringProperty): Builder =
+            fun setName(name: TypeProperty<StringValue>): Builder =
                 apply { this.name = name }
 
             fun build(): Property = Property(name)
diff --git a/appactions/interaction/interaction-capabilities-productivity/build.gradle b/appactions/interaction/interaction-capabilities-productivity/build.gradle
index 21b8526..aa110aa 100644
--- a/appactions/interaction/interaction-capabilities-productivity/build.gradle
+++ b/appactions/interaction/interaction-capabilities-productivity/build.gradle
@@ -24,7 +24,6 @@
 
 dependencies {
     api(libs.kotlinStdlib)
-    implementation("org.jetbrains.kotlin:kotlin-reflect:1.8.10")
     implementation("androidx.annotation:annotation:1.1.0")
     implementation(project(":appactions:interaction:interaction-capabilities-core"))
 }
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 b196e5f..22faf7a 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
@@ -26,7 +26,6 @@
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
 import androidx.appactions.interaction.capabilities.core.values.SuccessStatus
-import androidx.appactions.interaction.capabilities.core.values.Timer
 import androidx.appactions.interaction.proto.ParamValue
 import androidx.appactions.interaction.protobuf.Struct
 import androidx.appactions.interaction.protobuf.Value
@@ -42,11 +41,11 @@
         "timer",
         { property -> Optional.ofNullable(property.timerList) },
         PauseTimer.Argument.Builder::setTimerList,
-        TypeConverters::toTimer
+        TimerValue.FROM_PARAM_VALUE,
     ).bindOptionalOutput(
         "executionStatus",
         { output -> Optional.ofNullable(output.executionStatus) },
-        PauseTimer.ExecutionStatus::toParamValue
+        PauseTimer.ExecutionStatus::toParamValue,
     ).build()
 
 // TODO(b/267806701): Add capability factory annotation once the testing library is fully migrated.
@@ -54,7 +53,7 @@
 
     class CapabilityBuilder :
         CapabilityBuilderBase<
-            CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session
+            CapabilityBuilder, Property, Argument, Output, Confirmation, TaskUpdater, Session,
             >(ACTION_SPEC) {
         override fun build(): ActionCapability {
             super.setProperty(Property.Builder().build())
@@ -64,7 +63,7 @@
 
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
-        val timerList: SimpleProperty?
+        val timerList: SimpleProperty?,
     ) {
         override fun toString(): String {
             return "Property(timerList=$timerList}"
@@ -96,7 +95,7 @@
     }
 
     class Argument internal constructor(
-        val timerList: List<Timer>?
+        val timerList: List<TimerValue>?,
     ) {
         override fun toString(): String {
             return "Argument(timerList=$timerList)"
@@ -118,9 +117,11 @@
         }
 
         class Builder : BuilderOf<Argument> {
-            private var timerList: List<Timer>? = null
+            private var timerList: List<TimerValue>? = null
 
-            fun setTimerList(timerList: List<Timer>): Builder = apply { this.timerList = timerList }
+            fun setTimerList(timerList: List<TimerValue>): Builder = apply {
+                this.timerList = timerList
+            }
 
             override fun build(): Argument = Argument(timerList)
         }
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 74bec9e..069bf78 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
@@ -23,8 +23,9 @@
 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
+import androidx.appactions.interaction.capabilities.core.properties.StringValue
 import androidx.appactions.interaction.capabilities.core.properties.SimpleProperty
-import androidx.appactions.interaction.capabilities.core.properties.StringProperty
+import androidx.appactions.interaction.capabilities.core.properties.TypeProperty
 import androidx.appactions.interaction.capabilities.core.task.ValueListener
 import androidx.appactions.interaction.capabilities.core.task.impl.AbstractTaskUpdater
 import androidx.appactions.interaction.capabilities.core.values.GenericErrorStatus
@@ -96,8 +97,8 @@
 
     // TODO(b/268369632): Remove Property from public capability APIs.
     class Property internal constructor(
-        val identifier: StringProperty?,
-        val name: StringProperty?,
+        val identifier: TypeProperty<StringValue>?,
+        val name: TypeProperty<StringValue>?,
         val duration: SimpleProperty?
     ) {
         override fun toString(): String {
@@ -125,14 +126,14 @@
         }
 
         class Builder {
-            private var identifier: StringProperty? = null
-            private var name: StringProperty? = null
+            private var identifier: TypeProperty<StringValue>? = null
+            private var name: TypeProperty<StringValue>? = null
             private var duration: SimpleProperty? = null
 
-            fun setIdentifier(identifier: StringProperty): Builder =
+            fun setIdentifier(identifier: TypeProperty<StringValue>): Builder =
                 apply { this.identifier = identifier }
 
-            fun setName(name: StringProperty): Builder =
+            fun setName(name: TypeProperty<StringValue>): Builder =
                 apply { this.name = name }
 
             fun setDuration(duration: SimpleProperty): Builder =
diff --git a/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt
new file mode 100644
index 0000000..76515c8
--- /dev/null
+++ b/appactions/interaction/interaction-capabilities-productivity/src/main/java/androidx/appactions/interaction/capabilities/productivity/TimerValue.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.appactions.interaction.capabilities.productivity
+
+import androidx.appactions.interaction.capabilities.core.impl.converters.ParamValueConverter
+import androidx.appactions.interaction.capabilities.core.impl.converters.TypeConverters
+import androidx.appactions.interaction.capabilities.core.impl.converters.UnionTypeSpec
+import androidx.appactions.interaction.capabilities.core.values.SearchAction
+import androidx.appactions.interaction.capabilities.core.values.Timer
+
+class TimerValue private constructor(
+    val asTimer: Timer?,
+    val asTimerFilter: SearchAction<Timer>?,
+) {
+    constructor(timer: Timer) : this(timer, null)
+
+    // TODO(b/268071906) add TimerFilter type to SearchAction
+    constructor(timerFilter: SearchAction<Timer>) : this(null, timerFilter)
+
+    companion object {
+        private val TYPE_SPEC = UnionTypeSpec.Builder<TimerValue>()
+            .bindMemberType(
+                memberGetter = TimerValue::asTimer,
+                ctor = { TimerValue(it) },
+                typeSpec = TypeConverters.TIMER_TYPE_SPEC,
+            )
+            .bindMemberType(
+                memberGetter = TimerValue::asTimerFilter,
+                ctor = { TimerValue(it) },
+                typeSpec = TypeConverters.createSearchActionTypeSpec(
+                    TypeConverters.TIMER_TYPE_SPEC,
+                ),
+            )
+            .build()
+
+        internal val FROM_PARAM_VALUE = ParamValueConverter {
+            TYPE_SPEC.fromStruct(it.getStructValue())
+        }
+    }
+}
diff --git a/appcompat/appcompat-resources/build.gradle b/appcompat/appcompat-resources/build.gradle
index bab4b22..cd96606 100644
--- a/appcompat/appcompat-resources/build.gradle
+++ b/appcompat/appcompat-resources/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":appcompat:appcompat"))
-    }
-
     api("androidx.annotation:annotation:1.2.0")
     api("androidx.core:core:1.6.0")
     implementation("androidx.collection:collection:1.0.0")
diff --git a/appcompat/appcompat/build.gradle b/appcompat/appcompat/build.gradle
index efb2192..aea54f9 100644
--- a/appcompat/appcompat/build.gradle
+++ b/appcompat/appcompat/build.gradle
@@ -8,11 +8,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":appcompat:appcompat-resources"))
-    }
-
     api("androidx.annotation:annotation:1.3.0")
     api("androidx.core:core:1.9.0")
 
diff --git a/autofill/autofill/api/current.txt b/autofill/autofill/api/current.txt
index 668e933..0b0a188 100644
--- a/autofill/autofill/api/current.txt
+++ b/autofill/autofill/api/current.txt
@@ -16,7 +16,12 @@
     field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
     field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
     field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
     field public static final String AUTOFILL_HINT_GENDER = "gender";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+    field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
     field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
     field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
     field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/api/public_plus_experimental_current.txt b/autofill/autofill/api/public_plus_experimental_current.txt
index 668e933..0b0a188 100644
--- a/autofill/autofill/api/public_plus_experimental_current.txt
+++ b/autofill/autofill/api/public_plus_experimental_current.txt
@@ -16,7 +16,12 @@
     field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
     field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
     field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
     field public static final String AUTOFILL_HINT_GENDER = "gender";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+    field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
     field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
     field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
     field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/api/restricted_current.txt b/autofill/autofill/api/restricted_current.txt
index d057b98..2ce5394 100644
--- a/autofill/autofill/api/restricted_current.txt
+++ b/autofill/autofill/api/restricted_current.txt
@@ -16,7 +16,12 @@
     field public static final String AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE = "creditCardSecurityCode";
     field public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
     field public static final String AUTOFILL_HINT_EMAIL_OTP = "emailOTPCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
+    field public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
     field public static final String AUTOFILL_HINT_GENDER = "gender";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+    field public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+    field public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
     field @Deprecated public static final String AUTOFILL_HINT_NAME = "name";
     field public static final String AUTOFILL_HINT_NEW_PASSWORD = "newPassword";
     field public static final String AUTOFILL_HINT_NEW_USERNAME = "newUsername";
diff --git a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
index ebc4328..e265e47 100644
--- a/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
+++ b/autofill/autofill/src/main/java/androidx/autofill/HintConstants.java
@@ -40,7 +40,7 @@
      * should be <code>{@value #AUTOFILL_HINT_EMAIL_ADDRESS}</code>).
      *
      * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
-     * hints.
+     * hints.3
      */
     public static final String AUTOFILL_HINT_EMAIL_ADDRESS = "emailAddress";
 
@@ -680,4 +680,65 @@
      * hints.
      */
     public static final String AUTOFILL_HINT_UPI_VPA = "upiVirtualPaymentAddress";
+
+    /**
+     * Hint indicating that this view can be autofilled with a loyalty number.
+     *
+     * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+     * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+     * should be <code>{@value #AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER}</code>).
+     *
+     * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+     * hints.
+     */
+    public static final String AUTOFILL_HINT_LOYALTY_ACCOUNT_NUMBER = "loyaltyAccountNumber";
+
+    /**
+     * Hint indicating that this view can be autofilled with a gift card code.
+     *
+     * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+     * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+     * should be <code>{@value #AUTOFILL_HINT_GIFT_CARD_CODE}</code>).
+     *
+     * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+     * hints.
+     */
+    public static final String AUTOFILL_HINT_GIFT_CARD_NUMBER = "giftCardNumber";
+
+    /**
+     * Hint indicating that this view can be autofilled with a gift card pin.
+     *
+     * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+     * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+     * should be <code>{@value #AUTOFILL_HINT_GIFT_CARD_PIN}</code>).
+     *
+     * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+     * hints.
+     */
+    public static final String AUTOFILL_HINT_GIFT_CARD_PIN = "giftCardPIN";
+
+    /**
+     * Hint indicating that this view can be autofilled with a flight number.
+     * Examples: UA 355 (United Airlines), WN 355 (Southwest), AA 9158 (American Airlines)
+     *
+     * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+     * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+     * should be <code>{@value #AUTOFILL_HINT_FLIGHT_NUMBER}</code>).
+     *
+     * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+     * hints.
+     */
+    public static final String AUTOFILL_HINT_FLIGHT_NUMBER = "flightNumber";
+
+    /**
+     * Hint indicating that this view can be autofilled with a flight confirmation code.
+     *
+     * <p>Can be used with either {@link android.view.View#setAutofillHints(String[])} or <a
+     * href="#attr_android:autofillHint">{@code android:autofillHint}</a> (in which case the value
+     * should be <code>{@value #AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE}</code>).
+     *
+     * <p>See {@link android.view.View#setAutofillHints(String...)} for more info about autofill
+     * hints.
+     */
+    public static final String AUTOFILL_HINT_FLIGHT_CONFIRMATION_CODE = "flightConfirmationCode";
 }
diff --git a/biometric/biometric-ktx/build.gradle b/biometric/biometric-ktx/build.gradle
old mode 100755
new mode 100644
index 89d81fc..dec0bdd
--- a/biometric/biometric-ktx/build.gradle
+++ b/biometric/biometric-ktx/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":biometric:biometric-ktx"))
-    }
-
     api(libs.kotlinStdlib)
     api(libs.kotlinCoroutinesCore)
     api(project(":biometric:biometric"))
diff --git a/biometric/biometric/build.gradle b/biometric/biometric/build.gradle
index 4243ce5..c295ae0 100644
--- a/biometric/biometric/build.gradle
+++ b/biometric/biometric/build.gradle
@@ -28,11 +28,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":biometric:biometric-ktx"))
-    }
-
     // Public API dependencies
     api("androidx.annotation:annotation:1.1.0")
     api("androidx.core:core:1.3.2")
diff --git a/busytown/androidx_multiplatform_mac.sh b/busytown/androidx_multiplatform_mac.sh
index b159ca6..9f2dfbd 100755
--- a/busytown/androidx_multiplatform_mac.sh
+++ b/busytown/androidx_multiplatform_mac.sh
@@ -15,7 +15,7 @@
 # Setup simulators
 impl/androidx-native-mac-simulator-setup.sh
 
-impl/build.sh buildOnServer :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
+impl/build.sh buildOnServer :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false createAllArchives -Pandroidx.constraints=true
 
 # run a separate createAllArchives task to prepare a repository
 # folder in DIST.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
index 7492aa0..048c265 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -21,6 +21,8 @@
 import android.annotation.SuppressLint
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.params.DynamicRangeProfiles
+import android.os.Build
 import android.util.Range
 import android.util.Size
 import android.view.Surface
@@ -39,6 +41,13 @@
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraState
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.BIT_DEPTH_8_BIT
+import androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION
+import androidx.camera.core.DynamicRange.FORMAT_HDR10
+import androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS
+import androidx.camera.core.DynamicRange.FORMAT_HLG
 import androidx.camera.core.ExposureState
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.ZoomState
@@ -177,4 +186,47 @@
         Log.warn { "TODO: isPrivateReprocessingSupported are not yet supported." }
         return false
     }
+
+    @SuppressLint("ClassVerificationFailure")
+    override fun getSupportedDynamicRanges(): Set<DynamicRange> {
+        // TODO: use DynamicRangesCompat instead after it is migrates from camera-camera2.
+        if (Build.VERSION.SDK_INT >= 33) {
+            val availableProfiles = cameraProperties.metadata[
+                CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES]!!
+            return profileSetToDynamicRangeSet(availableProfiles.supportedProfiles)
+        }
+        return setOf(DynamicRange.SDR)
+    }
+
+    private fun profileSetToDynamicRangeSet(profileSet: Set<Long>): Set<DynamicRange> {
+        return profileSet.map { profileToDynamicRange(it) }.toSet()
+    }
+
+    private fun profileToDynamicRange(profile: Long): DynamicRange {
+        return checkNotNull(PROFILE_TO_DR_MAP[profile]) {
+            "Dynamic range profile cannot be converted to a DynamicRange object: $profile"
+        }
+    }
+
+    companion object {
+        private val DR_HLG10 = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+        private val DR_HDR10 = DynamicRange(FORMAT_HDR10, BIT_DEPTH_10_BIT)
+        private val DR_HDR10_PLUS = DynamicRange(FORMAT_HDR10_PLUS, BIT_DEPTH_10_BIT)
+        private val DR_DOLBY_VISION_10_BIT = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_10_BIT)
+        private val DR_DOLBY_VISION_8_BIT = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_8_BIT)
+        private val PROFILE_TO_DR_MAP: Map<Long, DynamicRange> = mapOf(
+            DynamicRangeProfiles.STANDARD to DynamicRange.SDR,
+            DynamicRangeProfiles.HLG10 to DR_HLG10,
+            DynamicRangeProfiles.HDR10 to DR_HDR10,
+            DynamicRangeProfiles.HDR10_PLUS to DR_HDR10_PLUS,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM to DR_DOLBY_VISION_10_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_OEM_PO to DR_DOLBY_VISION_10_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF to DR_DOLBY_VISION_10_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_10B_HDR_REF_PO to DR_DOLBY_VISION_10_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM to DR_DOLBY_VISION_8_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_OEM_PO to DR_DOLBY_VISION_8_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF to DR_DOLBY_VISION_8_BIT,
+            DynamicRangeProfiles.DOLBY_VISION_8B_HDR_REF_PO to DR_DOLBY_VISION_8_BIT,
+        )
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
index 5541a40..ed6acf5 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/FocusMeteringControl.kt
@@ -27,6 +27,7 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
+import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.adapter.propagateTo
@@ -43,11 +44,11 @@
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoSet
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
 
 /**
  * Implementation of focus and metering controls exposed by [CameraControlInternal].
@@ -110,7 +111,7 @@
         val signal = CompletableDeferred<FocusMeteringResult>()
 
         useCaseCamera?.let { useCaseCamera ->
-            val job = threads.sequentialScope.launch {
+            threads.sequentialScope.launch {
                 cancelSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
                 updateSignal?.setCancelException("Cancelled by another startFocusAndMetering()")
                 updateSignal = signal
@@ -158,50 +159,54 @@
                 } else {
                     (false to autoFocusTimeoutMs)
                 }
-                withTimeoutOrNull(timeout) {
-                    /**
-                     * If device does not support a 3A region, we should not update it at all.
-                     * If device does support but a region list is empty, it means any previously
-                     * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
-                     */
-                    useCaseCamera.requestControl.startFocusAndMeteringAsync(
-                        aeRegions = if (maxAeRegionCount > 0)
-                            aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
-                        else null,
-                        afRegions = if (maxAfRegionCount > 0)
-                            afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
-                        else null,
-                        awbRegions = if (maxAwbRegionCount > 0)
-                            awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
-                        else null,
-                        afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON)
-                    ).await()
-                }.let { result3A ->
-                    if (result3A != null) {
-                        if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
-                            signal.completeExceptionally(
-                                OperationCanceledException("Camera is not active.")
-                            )
-                        } else {
-                            signal.complete(result3A.toFocusMeteringResult(
-                                shouldTriggerAf = afRectangles.isNotEmpty()
-                            ))
+
+                /**
+                 * If device does not support a 3A region, we should not update it at all.
+                 * If device does support but a region list is empty, it means any previously
+                 * set region should be removed, so the no-op METERING_REGIONS_DEFAULT is used.
+                 */
+                val result3A = useCaseCamera.requestControl.startFocusAndMeteringAsync(
+                    aeRegions = if (maxAeRegionCount > 0)
+                        aeRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+                    else null,
+                    afRegions = if (maxAfRegionCount > 0)
+                        afRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+                    else null,
+                    awbRegions = if (maxAwbRegionCount > 0)
+                        awbRectangles.ifEmpty { METERING_REGIONS_DEFAULT.toList() }
+                    else null,
+                    aeLockBehavior = if (maxAeRegionCount > 0)
+                        Lock3ABehavior.AFTER_NEW_SCAN
+                    else null,
+                    afLockBehavior = if (maxAfRegionCount > 0)
+                        Lock3ABehavior.AFTER_NEW_SCAN
+                    else null,
+                    awbLockBehavior = if (maxAwbRegionCount > 0)
+                        Lock3ABehavior.AFTER_NEW_SCAN
+                    else null,
+                    afTriggerStartAeMode = cameraProperties.getSupportedAeMode(AeMode.ON),
+                    timeLimitNs = TimeUnit.NANOSECONDS.convert(
+                        timeout,
+                        TimeUnit.MILLISECONDS
+                    )
+                ).await()
+
+                if (result3A.status == Result3A.Status.SUBMIT_FAILED) {
+                    signal.completeExceptionally(
+                        OperationCanceledException("Camera is not active.")
+                    )
+                } else if (result3A.status == Result3A.Status.TIME_LIMIT_REACHED) {
+                    if (isCancelEnabled) {
+                        if (signal.isActive) {
+                            cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
                         }
                     } else {
-                        if (isCancelEnabled) {
-                            if (signal.isActive) {
-                                cancelFocusAndMeteringNowAsync(useCaseCamera, signal)
-                            }
-                        } else {
-                            signal.complete(FocusMeteringResult.create(false))
-                        }
+                        signal.complete(FocusMeteringResult.create(false))
                     }
-                }
-            }
-
-            signal.invokeOnCompletion { throwable ->
-                if (throwable is OperationCanceledException) {
-                    job.cancel()
+                } else {
+                    signal.complete(result3A.toFocusMeteringResult(
+                        shouldTriggerAf = afRectangles.isNotEmpty()
+                    ))
                 }
             }
         } ?: run {
@@ -311,7 +316,8 @@
          * in priority. On the other hand, resultAfState == null matters only if the result comes
          * from a submitted request, so it should be checked after frameMetadata == null.
          *
-         * Ref: [FocusMeteringAction] and [Controller3A.lock3A] documentations.
+         * @see FocusMeteringAction
+         * @see androidx.camera.camera2.pipe.graph.Controller3A.lock3A
          */
         val isFocusSuccessful = when {
             !shouldTriggerAf -> false
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
index 3e96a04..bfa1fc7 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraRequestControl.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
+import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.METERING_REGIONS_DEFAULT
 import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Request
@@ -132,7 +133,11 @@
         aeRegions: List<MeteringRectangle>? = null,
         afRegions: List<MeteringRectangle>? = null,
         awbRegions: List<MeteringRectangle>? = null,
-        afTriggerStartAeMode: AeMode? = null
+        aeLockBehavior: Lock3ABehavior? = null,
+        afLockBehavior: Lock3ABehavior? = null,
+        awbLockBehavior: Lock3ABehavior? = null,
+        afTriggerStartAeMode: AeMode? = null,
+        timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
     ): Deferred<Result3A>
 
     suspend fun cancelFocusAndMeteringAsync(): Deferred<Result3A>
@@ -219,14 +224,21 @@
         aeRegions: List<MeteringRectangle>?,
         afRegions: List<MeteringRectangle>?,
         awbRegions: List<MeteringRectangle>?,
-        afTriggerStartAeMode: AeMode?
+        aeLockBehavior: Lock3ABehavior?,
+        afLockBehavior: Lock3ABehavior?,
+        awbLockBehavior: Lock3ABehavior?,
+        afTriggerStartAeMode: AeMode?,
+        timeLimitNs: Long,
     ): Deferred<Result3A> = graph.acquireSession().use {
         it.lock3A(
             aeRegions = aeRegions,
             afRegions = afRegions,
             awbRegions = awbRegions,
-            afLockBehavior = Lock3ABehavior.AFTER_NEW_SCAN,
-            afTriggerStartAeMode = afTriggerStartAeMode
+            aeLockBehavior = aeLockBehavior,
+            afLockBehavior = afLockBehavior,
+            awbLockBehavior = awbLockBehavior,
+            afTriggerStartAeMode = afTriggerStartAeMode,
+            timeLimitNs = timeLimitNs,
         )
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
index 9898814..3b0b4c2 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/FocusMeteringControlTest.kt
@@ -29,6 +29,7 @@
 import android.util.Size
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
 import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
@@ -60,7 +61,6 @@
 import androidx.camera.testing.SurfaceTextureProvider
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeUseCase
-import androidx.concurrent.futures.await
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
@@ -361,6 +361,20 @@
     }
 
     @Test
+    fun startFocusAndMetering_defaultPoint_3ALocksAreCorrect() = runBlocking {
+        startFocusMeteringAndAwait(FocusMeteringAction.Builder(point1).build())
+
+        with(fakeRequestControl.focusMeteringCalls.last()) {
+            assertWithMessage("Wrong lock behavior for AE")
+                .that(aeLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+            assertWithMessage("Wrong lock behavior for AF")
+                .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+            assertWithMessage("Wrong lock behavior for AWB")
+                .that(awbLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+        }
+    }
+
+    @Test
     fun startFocusAndMetering_multiplePoints_3ARectsAreCorrect() = runBlocking {
         // Camera 0 i.e. Max AF count = 3, Max AE count = 3, Max AWB count = 1
         startFocusMeteringAndAwait(
@@ -1268,7 +1282,7 @@
     }
 
     @Test
-    fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotSet() {
+    fun startFocusMetering_onlyAfSupported_unsupportedRegionsNotConfigured() {
         // camera 5 supports 1 AF and 0 AE/AWB regions
         focusMeteringControl = initFocusMeteringControl(cameraId = CAMERA_ID_5)
 
@@ -1282,8 +1296,14 @@
 
         with(fakeRequestControl.focusMeteringCalls.last()) {
             assertWithMessage("Wrong number of AE regions").that(aeRegions).isNull()
+            assertWithMessage("Wrong lock behavior for AE").that(aeLockBehavior).isNull()
+
             assertWithMessage("Wrong number of AF regions").that(afRegions?.size).isEqualTo(1)
+            assertWithMessage("Wrong lock behavior for AE")
+                .that(afLockBehavior).isEqualTo(Lock3ABehavior.AFTER_NEW_SCAN)
+
             assertWithMessage("Wrong number of AWB regions").that(awbRegions).isNull()
+            assertWithMessage("Wrong lock behavior for AWB").that(awbLockBehavior).isNull()
         }
     }
 
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
index 8da683c..1ffe0a0 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/interop/Camera2CameraInfoTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.camera.core.CameraState
+import androidx.camera.core.DynamicRange
 import androidx.camera.core.ExposureState
 import androidx.camera.core.ZoomState
 import androidx.camera.core.impl.CameraCaptureCallback
@@ -173,6 +174,10 @@
             override fun getSupportedHighResolutions(format: Int): MutableList<Size> {
                 throw NotImplementedError("Not used in testing")
             }
+
+            override fun getSupportedDynamicRanges(): MutableSet<DynamicRange> {
+                throw NotImplementedError("Not used in testing")
+            }
         }
         Camera2CameraInfo.from(wrongCameraInfo)
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
index 215e70a..64d3363 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeUseCaseCamera.kt
@@ -19,6 +19,8 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.params.MeteringRectangle
 import androidx.camera.camera2.pipe.AeMode
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.Lock3ABehavior
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.Result3A
@@ -31,9 +33,11 @@
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.Config
+import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.withTimeoutOrNull
 
 class FakeUseCaseCameraComponentBuilder : UseCaseCameraComponent.Builder {
     private var config: UseCaseCameraConfig = UseCaseCameraConfig(emptyList(), CameraStateAdapter())
@@ -101,11 +105,27 @@
         aeRegions: List<MeteringRectangle>?,
         afRegions: List<MeteringRectangle>?,
         awbRegions: List<MeteringRectangle>?,
-        afTriggerStartAeMode: AeMode?
+        aeLockBehavior: Lock3ABehavior?,
+        afLockBehavior: Lock3ABehavior?,
+        awbLockBehavior: Lock3ABehavior?,
+        afTriggerStartAeMode: AeMode?,
+        timeLimitNs: Long,
     ): Deferred<Result3A> {
         focusMeteringCalls.add(
-            FocusMeteringParams(aeRegions, afRegions, awbRegions, afTriggerStartAeMode)
+            FocusMeteringParams(
+                aeRegions, afRegions, awbRegions,
+                aeLockBehavior, afLockBehavior, awbLockBehavior,
+                afTriggerStartAeMode,
+                timeLimitNs
+            )
         )
+        withTimeoutOrNull(TimeUnit.MILLISECONDS.convert(timeLimitNs, TimeUnit.NANOSECONDS)) {
+            focusMeteringResult.await()
+        }.let { result3A ->
+            if (result3A == null) {
+                focusMeteringResult.complete(Result3A(status = Result3A.Status.TIME_LIMIT_REACHED))
+            }
+        }
         return focusMeteringResult
     }
 
@@ -127,7 +147,11 @@
         val aeRegions: List<MeteringRectangle>? = null,
         val afRegions: List<MeteringRectangle>? = null,
         val awbRegions: List<MeteringRectangle>? = null,
-        val afTriggerStartAeMode: AeMode? = null
+        val aeLockBehavior: Lock3ABehavior? = null,
+        val afLockBehavior: Lock3ABehavior? = null,
+        val awbLockBehavior: Lock3ABehavior? = null,
+        val afTriggerStartAeMode: AeMode? = null,
+        val timeLimitNs: Long = CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS,
     )
 
     data class RequestParameters(
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 973ad8b..0f3dd35 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -16,9 +16,11 @@
 
 package androidx.camera.camera2.internal;
 
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
 import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_CONCURRENT;
 import static androidx.camera.core.concurrent.CameraCoordinator.CAMERA_OPERATING_MODE_SINGLE;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.TestCase.assertTrue;
@@ -31,6 +33,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.internal.verification.VerificationModeFactory.times;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.graphics.ImageFormat;
@@ -55,7 +60,6 @@
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
-import androidx.camera.core.InitializationException;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -100,11 +104,10 @@
 import org.mockito.Mockito;
 
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -127,7 +130,16 @@
     private static final int DEFAULT_PAIRED_CAMERA_LENS_FACING = CameraSelector.LENS_FACING_FRONT;
     // For the purpose of this test, always say we have 1 camera available.
     private static final int DEFAULT_AVAILABLE_CAMERA_COUNT = 1;
-    private static final Set<CameraInternal.State> STABLE_STATES = new HashSet<>(Arrays.asList(
+    private static final int DEFAULT_TEMPLATE_TYPE = CameraDevice.TEMPLATE_PREVIEW;
+    private static final Map<Integer, Boolean> DEFAULT_TEMPLATE_TO_ZSL_DISABLED = new HashMap<>();
+
+    static {
+        DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_PREVIEW, false);
+        DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_RECORD, true);
+        DEFAULT_TEMPLATE_TO_ZSL_DISABLED.put(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG, false);
+    }
+
+    private static final Set<CameraInternal.State> STABLE_STATES = new HashSet<>(asList(
             CameraInternal.State.CLOSED,
             CameraInternal.State.OPEN,
             CameraInternal.State.RELEASED));
@@ -139,9 +151,8 @@
             new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
     );
 
-    private ArrayList<FakeUseCase> mFakeUseCases = new ArrayList<>();
+    private final ArrayList<FakeUseCase> mFakeUseCases = new ArrayList<>();
     private Camera2CameraImpl mCamera2CameraImpl;
-    private Camera2CameraImpl mPairedCamera2CameraImpl;
     private static HandlerThread sCameraHandlerThread;
     private static Handler sCameraHandler;
     private FakeCameraCoordinator mCameraCoordinator;
@@ -154,7 +165,7 @@
     SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
 
     @BeforeClass
-    public static void classSetup() throws InitializationException {
+    public static void classSetup() {
         sCameraHandlerThread = new HandlerThread("cameraThread");
         sCameraHandlerThread.start();
         sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
@@ -214,11 +225,11 @@
         mCamera2CameraImpl.open();
 
         UseCase useCase = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase));
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase));
         mCamera2CameraImpl.release();
     }
 
@@ -235,21 +246,21 @@
     @Test
     public void attachAndActiveUseCase() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         mCamera2CameraImpl.onUseCaseActive(useCase1);
 
         verify(mMockOnImageAvailableListener, timeout(4000).atLeastOnce())
                 .onImageAvailable(any(ImageReader.class));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
     public void detachUseCase() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
         mCamera2CameraImpl.onUseCaseActive(useCase1);
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
@@ -261,8 +272,8 @@
     @Test
     public void unopenedCamera() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
     }
@@ -270,8 +281,8 @@
     @Test
     public void closedCamera() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
     }
@@ -283,12 +294,12 @@
         mCamera2CameraImpl.release();
         mCamera2CameraImpl.open();
 
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         mCamera2CameraImpl.onUseCaseActive(useCase1);
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
@@ -297,54 +308,54 @@
         mCamera2CameraImpl.open();
         mCamera2CameraImpl.release();
 
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         mCamera2CameraImpl.onUseCaseActive(useCase1);
 
         verify(mMockOnImageAvailableListener, never()).onImageAvailable(any(ImageReader.class));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
     public void attach_oneUseCase_isAttached() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
 
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
     public void attach_sameUseCases_staysAttached() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         boolean attachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase1);
 
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
 
         assertThat(attachedAfterFirstAdd).isTrue();
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
     public void attach_twoUseCases_bothBecomeAttached() {
         UseCase useCase1 = createUseCase();
         UseCase useCase2 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isTrue();
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
     }
 
     @Test
     public void detach_detachedUseCase_staysDetached() {
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
     }
@@ -353,34 +364,34 @@
     public void detachOneAttachedUseCase_fromAttachedUseCases_onlyDetachedSingleUseCase() {
         UseCase useCase1 = createUseCase();
         UseCase useCase2 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
         boolean useCase1isAttachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase1);
         boolean useCase2isAttachedAfterFirstAdd = mCamera2CameraImpl.isUseCaseAttached(useCase2);
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         assertThat(useCase1isAttachedAfterFirstAdd).isTrue();
         assertThat(useCase2isAttachedAfterFirstAdd).isTrue();
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase2));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
     }
 
     @Test
     public void detachSameAttachedUseCaseTwice_onlyDetachesSameUseCase() {
         UseCase useCase1 = createUseCase();
         UseCase useCase2 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase1)).isFalse();
         assertThat(mCamera2CameraImpl.isUseCaseAttached(useCase2)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase2));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
     }
 
     @Test
@@ -389,7 +400,7 @@
         blockHandler();
 
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         DeferrableSurface surface1 = useCase1.getSessionConfig().getSurfaces().get(0);
 
         unblockHandler();
@@ -405,13 +416,13 @@
 
         assertThat(surface1).isNotEqualTo(surface2);
 
-        // Old surface is decremented when CameraCaptueSession is closed by new
+        // Old surface is decremented when CameraCaptureSession is closed by new
         // CameraCaptureSession.
         assertThat(surface1.getUseCount()).isEqualTo(0);
-        // New surface is decremented when CameraCaptueSession is closed by
+        // New surface is decremented when CameraCaptureSession is closed by
         // mCamera2CameraImpl.release()
         assertThat(surface2.getUseCount()).isEqualTo(0);
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
@@ -422,7 +433,7 @@
         blockHandler();
 
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
 
         CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
         CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
@@ -431,12 +442,12 @@
         captureConfigBuilder.addCameraCaptureCallback(captureCallback);
 
         mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
-                Arrays.asList(captureConfigBuilder.build()),
+                singletonList(captureConfigBuilder.build()),
                 ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
                 ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
 
         UseCase useCase2 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase2));
 
         // Unblock camera handler to make camera operation run quickly .
         // To make the single request not able to run in 1st capture session.  and verify if it can
@@ -448,7 +459,7 @@
         verify(captureCallback, timeout(3000).times(1))
                 .onCaptureCompleted(any(CameraCaptureResult.class));
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
     }
 
     @Test
@@ -461,19 +472,19 @@
         UseCase useCase1 = createUseCase();
         UseCase useCase2 = createUseCase();
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
         CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
         CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
-        captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW);
+        captureConfigBuilder.setTemplateType(DEFAULT_TEMPLATE_TYPE);
         captureConfigBuilder.addSurface(useCase1.getSessionConfig().getSurfaces().get(0));
         captureConfigBuilder.addCameraCaptureCallback(captureCallback);
 
         mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
-                Arrays.asList(captureConfigBuilder.build()),
+                singletonList(captureConfigBuilder.build()),
                 ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
                 ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         // Unblock camera handle to make camera operation run quickly .
         // To make the single request not able to run in 1st capture session.  and verify if it can
@@ -488,7 +499,7 @@
         verify(captureCallback, times(0))
                 .onCaptureCompleted(any(CameraCaptureResult.class));
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase2));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
     }
 
     @Test
@@ -559,8 +570,10 @@
         mCameraStateRegistry.registerCamera(
                 mockCamera,
                 CameraXExecutors.directExecutor(),
-                () -> {},
-                () -> {});
+                () -> {
+                },
+                () -> {
+                });
         mCameraStateRegistry.tryOpenCamera(mockCamera);
 
         mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
@@ -597,18 +610,18 @@
             throws InterruptedException {
         mCamera2CameraImpl.open();
         UseCase useCase1 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         mCamera2CameraImpl.onUseCaseActive(useCase1);
 
         // Wait a little bit for the camera to open.
         assertTrue(mSessionStateCallback.waitForOnConfigured(1));
 
         // Remove the useCase1 and trigger the CaptureSession#close().
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
 
         // Create the secondary use case immediately and open it before the first use case closed.
         UseCase useCase2 = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase2));
         mCamera2CameraImpl.onUseCaseActive(useCase2);
         // Wait for the secondary capture session is configured.
         assertTrue(mSessionStateCallback.waitForOnConfigured(1));
@@ -617,7 +630,7 @@
 
         mCamera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(),
                 mockObserver);
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase2));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase2));
         mCamera2CameraImpl.close();
 
         // Wait for the CLOSED state. If the test fail, the CameraX might in wrong internal state,
@@ -633,27 +646,24 @@
         // Create another use case to keep the camera open.
         UseCase useCaseDummy = createUseCase();
         UseCase useCase = createUseCase();
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase, useCaseDummy));
+        mCamera2CameraImpl.attachUseCases(asList(useCase, useCaseDummy));
         mCamera2CameraImpl.onUseCaseActive(useCase);
 
         // Wait a little bit for the camera to open.
         assertTrue(mSessionStateCallback.waitForOnConfigured(2));
 
         // Remove the useCase and trigger the CaptureSession#close().
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase));
         assertTrue(mSessionStateCallback.waitForOnClosed(2));
     }
 
     // Blocks the camera thread handler.
     private void blockHandler() {
-        sCameraHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                try {
-                    mSemaphore.acquire();
-                } catch (InterruptedException e) {
-
-                }
+        sCameraHandler.post(() -> {
+            try {
+                mSemaphore.acquire();
+            } catch (InterruptedException e) {
+                // Do nothing.
             }
         });
     }
@@ -663,11 +673,14 @@
         mSemaphore.release();
     }
 
+    @NonNull
     private UseCase createUseCase() {
-        return createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
+        return createUseCase(DEFAULT_TEMPLATE_TYPE);
     }
 
-    private UseCase createUseCase(int template, boolean isZslDisabled) {
+    @NonNull
+    private UseCase createUseCase(int template) {
+        boolean isZslDisabled = getDefaultZslDisabled(template);
         FakeUseCaseConfig.Builder configBuilder =
                 new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
                                 new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
@@ -676,12 +689,10 @@
         return createUseCase(configBuilder.getUseCaseConfig(), template);
     }
 
+    @NonNull
     private UseCase createUseCase(@NonNull FakeUseCaseConfig config, int template) {
-        CameraSelector selector =
-                new CameraSelector.Builder().requireLensFacing(
-                        CameraSelector.LENS_FACING_BACK).build();
-        TestUseCase testUseCase = new TestUseCase(template, config,
-                selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
+        TestUseCase testUseCase = new TestUseCase(template, config, DEFAULT_BACK_CAMERA,
+                mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
 
         testUseCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
         mFakeUseCases.add(testUseCase);
@@ -693,8 +704,8 @@
         TestUseCase useCase1 = spy((TestUseCase) createUseCase());
         TestUseCase useCase2 = spy((TestUseCase) createUseCase());
 
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
@@ -704,7 +715,7 @@
         verify(useCase1, times(1)).onStateAttached();
         verify(useCase2, times(1)).onStateAttached();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2));
     }
 
     @Test
@@ -713,9 +724,9 @@
         TestUseCase useCase2 = spy((TestUseCase) createUseCase());
         TestUseCase useCase3 = spy((TestUseCase) createUseCase());
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1, useCase2));
+        mCamera2CameraImpl.attachUseCases(asList(useCase1, useCase2));
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1, useCase2, useCase3));
+        mCamera2CameraImpl.detachUseCases(asList(useCase1, useCase2, useCase3));
 
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
@@ -730,13 +741,14 @@
     private boolean isCameraControlActive(Camera2CameraControlImpl camera2CameraControlImpl) {
         ListenableFuture<Void> listenableFuture = camera2CameraControlImpl.setZoomRatio(2.0f);
         try {
-            // setZoom() will fail immediately when Cameracontrol is not active.
+            // setZoom() will fail immediately when CameraControl is not active.
             listenableFuture.get(50, TimeUnit.MILLISECONDS);
         } catch (ExecutionException e) {
             if (e.getCause() instanceof CameraControl.OperationCanceledException) {
                 return false;
             }
         } catch (InterruptedException | TimeoutException e) {
+            // Do nothing.
         }
         return true;
     }
@@ -750,12 +762,12 @@
 
         UseCase useCase1 = createUseCase();
 
-        mCamera2CameraImpl.attachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
         assertThat(isCameraControlActive(camera2CameraControlImpl)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Collections.singletonList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
     }
 
     @Test
@@ -764,11 +776,11 @@
                 (Camera2CameraControlImpl) mCamera2CameraImpl.getCameraControlInternal();
         UseCase useCase1 = createUseCase();
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.attachUseCases(singletonList(useCase1));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(isCameraControlActive(camera2CameraControlImpl)).isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
+        mCamera2CameraImpl.detachUseCases(singletonList(useCase1));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
         assertThat(isCameraControlActive(camera2CameraControlImpl)).isFalse();
@@ -776,9 +788,9 @@
 
     @Test
     public void attachUseCaseWithTemplatePreview() throws InterruptedException {
-        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
+        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(preview));
+        mCamera2CameraImpl.attachUseCases(singletonList(preview));
         mCamera2CameraImpl.onUseCaseActive(preview);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
@@ -793,15 +805,15 @@
         assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
                 .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(preview));
+        mCamera2CameraImpl.detachUseCases(singletonList(preview));
     }
 
     @Test
     public void attachUseCaseWithTemplateRecord() throws InterruptedException {
-        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */false);
-        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
+        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, record));
+        mCamera2CameraImpl.attachUseCases(asList(preview, record));
         mCamera2CameraImpl.onUseCaseActive(preview);
         mCamera2CameraImpl.onUseCaseActive(record);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -817,7 +829,7 @@
         assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
                 .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_RECORD);
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, record));
+        mCamera2CameraImpl.detachUseCases(asList(preview, record));
     }
 
     @SdkSuppress(minSdkVersion = 23)
@@ -826,14 +838,10 @@
         if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
             return;
         }
-        UseCase preview = createUseCase(
-                CameraDevice.TEMPLATE_PREVIEW,
-                /* isZslDisabled = */false);
-        UseCase zsl = createUseCase(
-                CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
-                /* isZslDisabled = */false);
+        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, zsl));
+        mCamera2CameraImpl.attachUseCases(asList(preview, zsl));
         mCamera2CameraImpl.onUseCaseActive(preview);
         mCamera2CameraImpl.onUseCaseActive(zsl);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -852,7 +860,7 @@
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isFalse();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, zsl));
+        mCamera2CameraImpl.detachUseCases(asList(preview, zsl));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(mCamera2CameraImpl.getCameraControlInternal()
                 .isZslDisabledByByUserCaseConfig()).isFalse();
@@ -864,13 +872,11 @@
         if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
             return;
         }
-        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
-                false);
-        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
-        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
-                /* isZslDisabled = */false);
+        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, record, zsl));
+        mCamera2CameraImpl.attachUseCases(asList(preview, record, zsl));
         mCamera2CameraImpl.onUseCaseActive(preview);
         mCamera2CameraImpl.onUseCaseActive(record);
         mCamera2CameraImpl.onUseCaseActive(zsl);
@@ -890,7 +896,7 @@
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(preview, record, zsl));
+        mCamera2CameraImpl.detachUseCases(asList(preview, record, zsl));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
@@ -903,13 +909,11 @@
         if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
             return;
         }
-        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
-                false);
-        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD, /* isZslDisabled = */true);
-        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
-                /* isZslDisabled = */false);
+        UseCase preview = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+        UseCase record = createUseCase(CameraDevice.TEMPLATE_RECORD);
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(preview, zsl));
+        mCamera2CameraImpl.attachUseCases(asList(preview, zsl));
         mCamera2CameraImpl.onUseCaseActive(preview);
         mCamera2CameraImpl.onUseCaseActive(zsl);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -918,7 +922,7 @@
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isFalse();
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(record));
+        mCamera2CameraImpl.attachUseCases(singletonList(record));
         mCamera2CameraImpl.onUseCaseActive(record);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
@@ -926,32 +930,32 @@
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(record));
+        mCamera2CameraImpl.detachUseCases(singletonList(record));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isFalse();
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(record));
+        mCamera2CameraImpl.attachUseCases(singletonList(record));
         mCamera2CameraImpl.onUseCaseActive(record);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(zsl));
+        mCamera2CameraImpl.detachUseCases(singletonList(zsl));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(preview));
+        mCamera2CameraImpl.detachUseCases(singletonList(preview));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
                 .isTrue();
 
-        mCamera2CameraImpl.detachUseCases(Arrays.asList(record));
+        mCamera2CameraImpl.detachUseCases(singletonList(record));
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
         assertThat(
                 mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
@@ -961,8 +965,7 @@
     @SdkSuppress(minSdkVersion = 23)
     @Test
     public void zslDisabled_whenHighResolutionIsEnabled() throws InterruptedException {
-        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
-                /* isZslDisabled = */false);
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG);
 
         // Creates a test use case with high resolution enabled.
         ResolutionSelector highResolutionSelector =
@@ -983,7 +986,7 @@
             return;
         }
 
-        mCamera2CameraImpl.attachUseCases(Arrays.asList(zsl, highResolutionUseCase));
+        mCamera2CameraImpl.attachUseCases(asList(zsl, highResolutionUseCase));
         mCamera2CameraImpl.onUseCaseActive(zsl);
         mCamera2CameraImpl.onUseCaseActive(highResolutionUseCase);
         HandlerUtil.waitForLooperToIdle(sCameraHandler);
@@ -1002,14 +1005,14 @@
                 && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_CONCURRENT)) {
             Camera2CameraInfoImpl pairedCamera2CameraInfo = new Camera2CameraInfoImpl(
                     mPairedCameraId, cameraManagerCompat);
-            mPairedCamera2CameraImpl = new Camera2CameraImpl(
+            Camera2CameraImpl pairedCamera2CameraImpl = new Camera2CameraImpl(
                     cameraManagerCompat, mPairedCameraId, pairedCamera2CameraInfo,
                     mCameraCoordinator,
                     mCameraStateRegistry, sCameraExecutor, sCameraHandler,
                     DisplayInfoManager.getInstance(ApplicationProvider.getApplicationContext()));
             mCameraCoordinator.addConcurrentCameraIdsAndCameraSelectors(
                     new HashMap<String, CameraSelector>() {{
-                        put(mCameraId, CameraSelector.DEFAULT_BACK_CAMERA);
+                        put(mCameraId, DEFAULT_BACK_CAMERA);
                         put(mPairedCameraId, CameraSelector.DEFAULT_FRONT_CAMERA);
                     }});
             mCameraCoordinator.setCameraOperatingMode(CAMERA_OPERATING_MODE_CONCURRENT);
@@ -1017,9 +1020,8 @@
                     CAMERA_OPERATING_MODE_SINGLE, CAMERA_OPERATING_MODE_CONCURRENT);
 
             // Act.
-            UseCase preview1 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
-                    false);
-            mCamera2CameraImpl.attachUseCases(Arrays.asList(preview1));
+            UseCase preview1 = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+            mCamera2CameraImpl.attachUseCases(singletonList(preview1));
             mCamera2CameraImpl.onUseCaseActive(preview1);
             HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
@@ -1029,10 +1031,9 @@
             verify(mMockRepeatingCaptureCallback, never()).onCaptureCompleted(captor.capture());
 
             // Act.
-            UseCase preview2 = createUseCase(CameraDevice.TEMPLATE_PREVIEW, /* isZslDisabled = */
-                    false);
-            mPairedCamera2CameraImpl.attachUseCases(Arrays.asList(preview2));
-            mPairedCamera2CameraImpl.onUseCaseActive(preview2);
+            UseCase preview2 = createUseCase(CameraDevice.TEMPLATE_PREVIEW);
+            pairedCamera2CameraImpl.attachUseCases(singletonList(preview2));
+            pairedCamera2CameraImpl.onUseCaseActive(preview2);
             HandlerUtil.waitForLooperToIdle(sCameraHandler);
 
             // Assert.
@@ -1043,40 +1044,18 @@
             assertThat(captureResult.get(CaptureResult.CONTROL_CAPTURE_INTENT))
                     .isEqualTo(CaptureRequest.CONTROL_CAPTURE_INTENT_PREVIEW);
 
-            mCamera2CameraImpl.detachUseCases(Arrays.asList(preview1));
+            mCamera2CameraImpl.detachUseCases(singletonList(preview1));
         }
     }
 
-    private DeferrableSurface getUseCaseSurface(UseCase useCase) {
-        return useCase.getSessionConfig().getSurfaces().get(0);
-    }
-
     private void changeUseCaseSurface(UseCase useCase) {
         useCase.updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
     }
 
-    private void waitForCameraClose(Camera2CameraImpl camera2CameraImpl)
-            throws InterruptedException {
-        Semaphore semaphore = new Semaphore(0);
-
-        Observable.Observer<CameraInternal.State> observer =
-                new Observable.Observer<CameraInternal.State>() {
-                    @Override
-                    public void onNewData(@Nullable CameraInternal.State value) {
-                        // Ignore any transient states.
-                        if (value == CameraInternal.State.CLOSED) {
-                            semaphore.release();
-                        }
-                    }
-
-                    @Override
-                    public void onError(@NonNull Throwable t) { /* Ignore any transient errors. */ }
-                };
-
-        camera2CameraImpl.getCameraState().addObserver(CameraXExecutors.directExecutor(), observer);
-
-        // Wait until camera reaches closed state
-        semaphore.acquire();
+    private static boolean getDefaultZslDisabled(int templateType) {
+        Boolean isZslDisabled = DEFAULT_TEMPLATE_TO_ZSL_DISABLED.get(templateType);
+        checkState(isZslDisabled != null, "No default mapping from template to zsl disabled");
+        return isZslDisabled;
     }
 
     public static class TestUseCase extends FakeUseCase {
@@ -1084,7 +1063,6 @@
         HandlerThread mHandlerThread = new HandlerThread("HandlerThread");
         Handler mHandler;
         FakeUseCaseConfig mConfig;
-        private String mCameraId;
         private DeferrableSurface mDeferrableSurface;
         private final CameraCaptureCallback mRepeatingCaptureCallback;
         private final int mTemplate;
@@ -1104,13 +1082,15 @@
             mRepeatingCaptureCallback = repeatingCaptureCallback;
             mHandlerThread.start();
             mHandler = new Handler(mHandlerThread.getLooper());
-            Integer lensFacing =
+            int lensFacing =
                     cameraSelector.getLensFacing() == null ? CameraSelector.LENS_FACING_BACK :
                             cameraSelector.getLensFacing();
-            mCameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing);
-            bindToCamera(new FakeCamera(mCameraId, null,
-                            new FakeCameraInfoInternal(mCameraId, 0, lensFacing)),
-                    null, null);
+            String cameraId = CameraUtil.getCameraIdWithLensFacing(lensFacing);
+            if (cameraId == null) {
+                cameraId = "FakeId";
+            }
+            bindToCamera(new FakeCamera(cameraId, null,
+                    new FakeCameraInfoInternal(cameraId, 0, lensFacing)), null, null);
             updateSuggestedStreamSpec(StreamSpec.builder(new Size(640, 480)).build());
         }
 
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
index 04c3858..7977243 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2CameraControlDeviceTest.java
@@ -70,7 +70,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 @OptIn(markerClass = ExperimentalCamera2Interop.class)
-@SdkSuppress(minSdkVersion = 22) // b/272066193
+@SdkSuppress(minSdkVersion = 21)
 public final class Camera2CameraControlDeviceTest {
     private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
     private CameraSelector mCameraSelector;
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
index 29a28b7..471ab2c 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/interop/Camera2InteropDeviceTest.java
@@ -71,7 +71,7 @@
 @LargeTest
 @RunWith(AndroidJUnit4.class)
 @OptIn(markerClass = ExperimentalCamera2Interop.class)
-@SdkSuppress(minSdkVersion = 22) // b/272066193
+@SdkSuppress(minSdkVersion = 21)
 public final class Camera2InteropDeviceTest {
     private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
     private CameraSelector mCameraSelector;
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index e893e30..d53901a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -40,6 +40,7 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.camera2.internal.compat.StreamConfigurationMapCompat;
+import androidx.camera.camera2.internal.compat.params.DynamicRangesCompat;
 import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
 import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
 import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
@@ -48,6 +49,7 @@
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.Logger;
@@ -72,6 +74,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
@@ -435,6 +438,15 @@
         return size != null ? Arrays.asList(size) : Collections.emptyList();
     }
 
+    @NonNull
+    @Override
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        DynamicRangesCompat dynamicRangesCompat = DynamicRangesCompat.fromCameraCharacteristics(
+                mCameraCharacteristicsCompat);
+
+        return dynamicRangesCompat.getSupportedDynamicRanges();
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 0c0a626..235acef 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -18,6 +18,8 @@
 
 import static android.hardware.camera2.CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES;
 
+import static androidx.camera.core.DynamicRange.SDR;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -31,6 +33,7 @@
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraDevice;
 import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.params.DynamicRangeProfiles;
 import android.os.Build;
 import android.util.Pair;
 import android.util.Range;
@@ -45,6 +48,7 @@
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.DynamicRange;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.SurfaceOrientedMeteringPointFactory;
@@ -94,8 +98,10 @@
     private static final boolean CAMERA0_FLASH_INFO_BOOLEAN = true;
     private static final int CAMERA0_SUPPORTED_PRIVATE_REPROCESSING =
             CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_PRIVATE_REPROCESSING;
+    private static final int CAMERA0_SUPPORTED_DYNAMIC_RANGE_TEN_BIT =
+            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DYNAMIC_RANGE_TEN_BIT;
     private static final int[] CAMERA0_SUPPORTED_CAPABILITIES = new int[] {
-            CAMERA0_SUPPORTED_PRIVATE_REPROCESSING,
+            CAMERA0_SUPPORTED_PRIVATE_REPROCESSING, CAMERA0_SUPPORTED_DYNAMIC_RANGE_TEN_BIT
     };
     private static final float[] CAMERA0_LENS_FOCAL_LENGTH = new float[]{
             3.0F,
@@ -105,13 +111,15 @@
     private static final SizeF CAMERA0_SENSOR_PHYSICAL_SIZE = new SizeF(1.5F, 1F);
     private static final Rect CAMERA0_SENSOR_ACTIVE_ARRAY_SIZE = new Rect(0, 0, 1920, 1080);
     private static final Size CAMERA0_SENSOR_PIXEL_ARRAY_SIZE = new Size(1920, 1080);
-
     private static final Range<?>[] CAMERA0_AE_FPS_RANGES = {
             new Range<>(12, 30),
             new Range<>(24, 24),
             new Range<>(30, 30),
             new Range<>(60, 60)
     };
+    private static final DynamicRangeProfiles CAMERA0_DYNAMIC_RANGE_PROFILES =
+            new DynamicRangeProfiles(new long[]{DynamicRangeProfiles.HLG10, 0, 0});
+
     private static final String CAMERA1_ID = "1";
     private static final int CAMERA1_SUPPORTED_HARDWARE_LEVEL =
             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3;
@@ -135,6 +143,8 @@
             new Range<>(12, 30),
             new Range<>(30, 30),
     };
+    private static final DynamicRange HLG10 = new DynamicRange(DynamicRange.FORMAT_HLG,
+            DynamicRange.BIT_DEPTH_10_BIT);
 
     private CameraCharacteristicsCompat mCameraCharacteristics0;
     private CameraManagerCompat mCameraManagerCompat;
@@ -146,7 +156,7 @@
 
     @Test
     public void canCreateCameraInfo() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -156,7 +166,7 @@
 
     @Test
     public void cameraInfo_canReturnSensorOrientation() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -167,7 +177,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -186,7 +196,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
@@ -204,7 +214,7 @@
 
     @Test
     public void cameraInfo_canReturnLensFacing() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -214,7 +224,7 @@
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forBackCamera()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -224,7 +234,7 @@
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forFrontCamera()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraInfoInternal cameraInfoInternal =
                 new Camera2CameraInfoImpl(CAMERA1_ID, mCameraManagerCompat);
@@ -234,7 +244,7 @@
     @Test
     public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl camera2CameraInfoImpl =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -245,7 +255,7 @@
     @Test
     public void cameraInfoWithCameraControl_canReturnTorchState()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
         Camera2CameraInfoImpl camera2CameraInfoImpl =
@@ -257,7 +267,7 @@
     @Test
     public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl camera2CameraInfoImpl =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -278,7 +288,7 @@
     @Test
     public void cameraInfoWithCameraControl_getZoom_valueIsCorrect()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
         when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
@@ -293,7 +303,7 @@
     @Test
     public void cameraInfoWithoutCameraControl_getDetaultZoomState()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl camera2CameraInfoImpl =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -304,7 +314,7 @@
     @Test
     public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl camera2CameraInfoImpl =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -324,7 +334,7 @@
     @Test
     public void cameraInfoWithCameraControl_canReturnExposureState()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
         when(mExposureControl.getExposureState()).thenReturn(exposureState);
@@ -339,7 +349,7 @@
     @Test
     public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl camera2CameraInfoImpl =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -359,7 +369,7 @@
 
     @Test
     public void cameraInfo_getImplementationType_legacy() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final CameraInfoInternal cameraInfo =
                 new Camera2CameraInfoImpl(CAMERA0_ID, mCameraManagerCompat);
@@ -369,7 +379,7 @@
 
     @Test
     public void cameraInfo_getImplementationType_noneLegacy() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA1_ID, mCameraManagerCompat);
@@ -380,7 +390,7 @@
     @Test
     public void addSessionCameraCaptureCallback_isCalledToCameraControl()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA1_ID, mCameraManagerCompat);
@@ -396,7 +406,7 @@
     @Test
     public void removeSessionCameraCaptureCallback_isCalledToCameraControl()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA1_ID, mCameraManagerCompat);
@@ -411,7 +421,7 @@
     @Test
     public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA1_ID, mCameraManagerCompat);
@@ -427,7 +437,7 @@
     @Test
     public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA1_ID, mCameraManagerCompat);
@@ -451,7 +461,7 @@
     @Test
     public void cameraInfoWithCameraControl_canReturnIsFocusMeteringSupported()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -473,7 +483,7 @@
     @Test
     public void canReturnCameraCharacteristicsMapWithPhysicalCameras()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         CameraCharacteristics characteristics0 = mock(CameraCharacteristics.class);
         CameraCharacteristics characteristicsPhysical2 = mock(CameraCharacteristics.class);
@@ -498,7 +508,7 @@
     @Test
     public void canReturnCameraCharacteristicsMapWithMainCamera()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl impl = new Camera2CameraInfoImpl("0", mCameraManagerCompat);
         Map<String, CameraCharacteristics> map = impl.getCameraCharacteristicsMap();
@@ -510,7 +520,7 @@
     @Test
     public void cameraInfoWithCameraControl_canReturnIsPrivateReprocessingSupported()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -521,7 +531,7 @@
     @Config(minSdk = 23)
     @Test
     public void isZslSupported_apiVersionMet_returnTrue() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -532,7 +542,7 @@
     @Config(maxSdk = 22)
     @Test
     public void isZslSupported_apiVersionNotMet_returnFalse() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -543,7 +553,7 @@
     @Test
     public void isZslSupported_noReprocessingCapability_returnFalse()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ false);
+        init(/* hasAvailableCapabilities = */ false);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -558,7 +568,7 @@
         ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-F936B");
 
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -573,7 +583,7 @@
         ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G973");
 
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -588,7 +598,7 @@
         ReflectionHelpers.setStaticField(Build.class, "BRAND", "xiaomi");
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "Mi 8");
 
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -603,7 +613,7 @@
         ReflectionHelpers.setStaticField(Build.class, "BRAND", "xiaomi");
         ReflectionHelpers.setStaticField(Build.class, "MODEL", "Mi A1");
 
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
                 CAMERA0_ID, mCameraManagerCompat);
@@ -613,7 +623,7 @@
 
     @Test
     public void canReturnSupportedResolutions() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ true);
+        init(/* hasAvailableCapabilities = */ true);
 
         Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
                 mCameraManagerCompat);
@@ -637,7 +647,7 @@
 
     @Test
     public void cameraInfo_canReturnIntrinsicZoomRatio() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ false);
+        init(/* hasAvailableCapabilities = */ false);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA2_ID,
                 mCameraManagerCompat);
@@ -649,7 +659,7 @@
 
     @Test
     public void cameraInfo_canReturnSupportedFpsRanges() throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ false);
+        init(/* hasAvailableCapabilities = */ false);
 
         final Camera2CameraInfoImpl cameraInfo0 = new Camera2CameraInfoImpl(CAMERA0_ID,
                 mCameraManagerCompat);
@@ -666,7 +676,7 @@
     @Test
     public void cameraInfo_returnsEmptyFpsRanges_whenNotSupported()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ false);
+        init(/* hasAvailableCapabilities = */ false);
 
         final Camera2CameraInfoImpl cameraInfo1 = new Camera2CameraInfoImpl(CAMERA1_ID,
                 mCameraManagerCompat);
@@ -679,7 +689,7 @@
     @Test
     public void cameraInfo_checkDefaultCameraIntrinsicZoomRatio()
             throws CameraAccessExceptionCompat {
-        init(/* hasReprocessingCapabilities = */ false);
+        init(/* hasAvailableCapabilities = */ false);
 
         final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
                 mCameraManagerCompat);
@@ -690,6 +700,31 @@
         assertThat(resultZoomRatio).isEqualTo(1.0F);
     }
 
+    @Config(minSdk = 33)
+    @Test
+    public void apiVersionMet_canReturnSupportedDynamicRanges() throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+                CAMERA0_ID, mCameraManagerCompat);
+
+        Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+        assertThat(supportedDynamicRanges).containsExactly(SDR, HLG10);
+    }
+
+    @Config(maxSdk = 32)
+    @Test
+    public void apiVersionNotMet_canReturnSupportedDynamicRanges()
+            throws CameraAccessExceptionCompat {
+        init(/* hasAvailableCapabilities = */ true);
+
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+                CAMERA0_ID, mCameraManagerCompat);
+
+        Set<DynamicRange> supportedDynamicRanges = cameraInfo.getSupportedDynamicRanges();
+        assertThat(supportedDynamicRanges).containsExactly(SDR);
+    }
+
     private CameraManagerCompat initCameraManagerWithPhysicalIds(
             List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
         FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
@@ -701,8 +736,8 @@
         return CameraManagerCompat.from(cameraManagerImpl);
     }
 
-    private void init(boolean hasReprocessingCapabilities) throws CameraAccessExceptionCompat {
-        initCameras(hasReprocessingCapabilities);
+    private void init(boolean hasAvailableCapabilities) throws CameraAccessExceptionCompat {
+        initCameras(hasAvailableCapabilities);
 
         mCameraManagerCompat =
                 CameraManagerCompat.from((Context) ApplicationProvider.getApplicationContext());
@@ -720,7 +755,7 @@
         when(mMockCameraControl.getFocusMeteringControl()).thenReturn(mFocusMeteringControl);
     }
 
-    private void initCameras(boolean hasReprocessingCapabilities) {
+    private void initCameras(boolean hasAvailableCapabilities) {
         // **** Camera 0 characteristics ****//
         CameraCharacteristics characteristics0 =
                 ShadowCameraCharacteristics.newCameraCharacteristics();
@@ -771,8 +806,14 @@
                 CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES,
                 CAMERA0_AE_FPS_RANGES);
 
+        if (Build.VERSION.SDK_INT >= 33) {
+            shadowCharacteristics0.set(
+                    CameraCharacteristics.REQUEST_AVAILABLE_DYNAMIC_RANGE_PROFILES,
+                    CAMERA0_DYNAMIC_RANGE_PROFILES);
+        }
+
         // Mock the request capability
-        if (hasReprocessingCapabilities) {
+        if (hasAvailableCapabilities) {
             shadowCharacteristics0.set(REQUEST_AVAILABLE_CAPABILITIES,
                     CAMERA0_SUPPORTED_CAPABILITIES);
         }
diff --git a/camera/camera-core/api/current.txt b/camera/camera-core/api/current.txt
index 4614cf8..2f9f00d 100644
--- a/camera/camera-core/api/current.txt
+++ b/camera/camera-core/api/current.txt
@@ -359,6 +359,12 @@
     method public static float getDefaultPointSize();
   }
 
+  @RequiresApi(21) public class MirrorMode {
+    field public static final int MIRROR_MODE_OFF = 0; // 0x0
+    field public static final int MIRROR_MODE_ON = 1; // 0x1
+    field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+  }
+
   @RequiresApi(21) public final class Preview extends androidx.camera.core.UseCase {
     method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
     method public int getTargetRotation();
diff --git a/camera/camera-core/api/public_plus_experimental_current.txt b/camera/camera-core/api/public_plus_experimental_current.txt
index fc38de4..7f210f6 100644
--- a/camera/camera-core/api/public_plus_experimental_current.txt
+++ b/camera/camera-core/api/public_plus_experimental_current.txt
@@ -376,6 +376,12 @@
     method public static float getDefaultPointSize();
   }
 
+  @RequiresApi(21) public class MirrorMode {
+    field public static final int MIRROR_MODE_OFF = 0; // 0x0
+    field public static final int MIRROR_MODE_ON = 1; // 0x1
+    field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+  }
+
   @RequiresApi(21) public final class Preview extends androidx.camera.core.UseCase {
     method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
     method public int getTargetRotation();
diff --git a/camera/camera-core/api/restricted_current.txt b/camera/camera-core/api/restricted_current.txt
index 4614cf8..2f9f00d 100644
--- a/camera/camera-core/api/restricted_current.txt
+++ b/camera/camera-core/api/restricted_current.txt
@@ -359,6 +359,12 @@
     method public static float getDefaultPointSize();
   }
 
+  @RequiresApi(21) public class MirrorMode {
+    field public static final int MIRROR_MODE_OFF = 0; // 0x0
+    field public static final int MIRROR_MODE_ON = 1; // 0x1
+    field public static final int MIRROR_MODE_ON_FRONT_ONLY = 2; // 0x2
+  }
+
   @RequiresApi(21) public final class Preview extends androidx.camera.core.UseCase {
     method public androidx.camera.core.ResolutionInfo? getResolutionInfo();
     method public int getTargetRotation();
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
index 287d3d4a..71f4651 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/UseCaseTest.kt
@@ -22,9 +22,9 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
-import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
 import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
 import androidx.camera.core.concurrent.CameraCoordinator
 import androidx.camera.core.impl.Config
 import androidx.camera.core.impl.ImageOutputConfig
@@ -286,8 +286,8 @@
     }
 
     @Test
-    fun setMirrorModeFrontOn_isMirroringRequiredDependsOnCamera() {
-        val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_FRONT_ON)
+    fun setMirrorModeOnFrontOnly_isMirroringRequiredDependsOnCamera() {
+        val fakeUseCase = createFakeUseCase(mirrorMode = MIRROR_MODE_ON_FRONT_ONLY)
         assertThat(fakeUseCase.isMirroringRequired(fakeCamera)).isFalse()
         assertThat(fakeUseCase.isMirroringRequired(fakeFrontCamera)).isTrue()
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
index 72e2efa..49af138 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/MirrorMode.java
@@ -23,13 +23,12 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
-// TODO: to public API
 /**
  * The mirror mode.
  *
+ * <p>Constants describing image mirroring transforms.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class MirrorMode {
     /** No mirror effect will be applied. */
     public static final int MIRROR_MODE_OFF = 0;
@@ -38,17 +37,17 @@
     public static final int MIRROR_MODE_ON = 1;
 
     /**
-     * The mirroring effect is applied only when the lens facing of the associated camera is
+     * The mirror effect is applied only when the lens facing of the associated camera is
      * {@link CameraSelector#LENS_FACING_FRONT}.
      */
-    public static final int MIRROR_MODE_FRONT_ON = 2;
+    public static final int MIRROR_MODE_ON_FRONT_ONLY = 2;
 
     private MirrorMode() {
     }
 
     /**
      */
-    @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_FRONT_ON})
+    @IntDef({MIRROR_MODE_OFF, MIRROR_MODE_ON, MIRROR_MODE_ON_FRONT_ONLY})
     @Retention(RetentionPolicy.SOURCE)
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
     public @interface Mirror {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index f994a2d..1164a47 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
@@ -17,7 +17,7 @@
 package androidx.camera.core;
 
 import static androidx.camera.core.CameraEffect.PREVIEW;
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
 import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
@@ -737,7 +737,7 @@
     public static final class Defaults implements ConfigProvider<PreviewConfig> {
         private static final int DEFAULT_SURFACE_OCCUPANCY_PRIORITY = 2;
         private static final int DEFAULT_ASPECT_RATIO = AspectRatio.RATIO_4_3;
-        private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_FRONT_ON;
+        private static final int DEFAULT_MIRROR_MODE = MIRROR_MODE_ON_FRONT_ONLY;
 
         private static final ResolutionSelector DEFAULT_RESOLUTION_SELECTOR =
                 new ResolutionSelector.Builder().setAspectRatioStrategy(
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index 3257ece..0aa20f0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -16,12 +16,13 @@
 
 package androidx.camera.core;
 
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
+import static androidx.camera.core.impl.StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED;
 import static androidx.camera.core.impl.utils.TransformUtils.within360;
 import static androidx.camera.core.processing.TargetUtils.isSuperset;
 import static androidx.core.util.Preconditions.checkArgument;
@@ -30,6 +31,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.media.ImageReader;
+import android.util.Range;
 import android.util.Size;
 import android.view.Surface;
 
@@ -353,6 +355,17 @@
     }
 
     /**
+     * Returns the target frame rate range for the associated VideoCapture use case.
+     *
+     * @return The target frame rate.
+     */
+    @NonNull
+    @RestrictTo(Scope.LIBRARY_GROUP)
+    protected Range<Integer> getTargetFramerateInternal() {
+        return mCurrentConfig.getTargetFramerate(FRAME_RATE_RANGE_UNSPECIFIED);
+    }
+
+    /**
      * Returns the mirror mode.
      *
      * <p>If mirror mode is not set, defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
@@ -376,7 +389,7 @@
                 return false;
             case MIRROR_MODE_ON:
                 return true;
-            case MIRROR_MODE_FRONT_ON:
+            case MIRROR_MODE_ON_FRONT_ONLY:
                 return camera.isFrontFacing();
             default:
                 throw new AssertionError("Unknown mirrorMode: " + mirrorMode);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
index f7ec5a0..84beaf0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraInfoInternal.java
@@ -25,10 +25,12 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.DynamicRange;
 import androidx.core.util.Preconditions;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
@@ -103,6 +105,14 @@
     @NonNull
     List<Size> getSupportedHighResolutions(int format);
 
+    /**
+     * Returns the supported dynamic ranges of this camera.
+     *
+     * @return a set of supported dynamic range, or an empty set if no dynamic range is supported.
+     */
+    @NonNull
+    Set<DynamicRange> getSupportedDynamicRanges();
+
     /** {@inheritDoc} */
     @NonNull
     @Override
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
index f37cb38..d8152fa 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/EncoderProfilesProxy.java
@@ -85,6 +85,9 @@
         /** Constant representing bit depth 8. */
         public static final int BIT_DEPTH_8 = 8;
 
+        /** Constant representing bit depth 10. */
+        public static final int BIT_DEPTH_10 = 10;
+
         @Retention(RetentionPolicy.SOURCE)
         @IntDef({H263, H264, HEVC, VP8, MPEG_4_SP, VP9, DOLBY_VISION, AV1,
                 MediaRecorder.VideoEncoder.DEFAULT})
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index 390e103..9cef56c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -414,7 +414,7 @@
          * Sets the mirror mode of the intended target for images from this configuration.
          *
          * <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
-         * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
+         * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
          *
          * @param mirrorMode The mirror mode of the intended target.
          * @return The current Builder.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
index 3556349..345fe82 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/CameraUseCaseAdapter.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.CameraEffect.PREVIEW;
 import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
+import static androidx.camera.core.impl.utils.TransformUtils.rectToSize;
 import static androidx.camera.core.processing.TargetUtils.getNumberOfTargets;
 import static androidx.core.util.Preconditions.checkArgument;
 import static androidx.core.util.Preconditions.checkState;
@@ -554,8 +555,17 @@
         if (!newUseCases.isEmpty()) {
             Map<UseCaseConfig<?>, UseCase> configToUseCaseMap = new HashMap<>();
             Map<UseCaseConfig<?>, List<Size>> configToSupportedSizesMap = new HashMap<>();
+            Rect sensorRect;
+            try {
+                sensorRect = ((CameraControlInternal) getCameraControl()).getSensorRect();
+            } catch (NullPointerException e) {
+                // TODO(b/274531208): Remove the unnecessary SENSOR_INFO_ACTIVE_ARRAY_SIZE NPE
+                //  check related code only which is used for robolectric tests
+                sensorRect = null;
+            }
             SupportedOutputSizesSorter supportedOutputSizesSorter = new SupportedOutputSizesSorter(
-                    (CameraInfoInternal) getCameraInfo());
+                    (CameraInfoInternal) getCameraInfo(),
+                    sensorRect != null ? rectToSize(sensorRect) : null);
             for (UseCase useCase : newUseCases) {
                 ConfigPair configPair = configPairMap.get(useCase);
                 // Combine with default configuration.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
index 9f7524c..2a84e43 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/SupportedOutputSizesSorter.java
@@ -74,11 +74,14 @@
     private final boolean mIsSensorLandscapeResolution;
     private final SupportedOutputSizesSorterLegacy mSupportedOutputSizesSorterLegacy;
 
-    SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal) {
+    SupportedOutputSizesSorter(@NonNull CameraInfoInternal cameraInfoInternal,
+            @Nullable Size activeArraySize) {
         mCameraInfoInternal = cameraInfoInternal;
         mSensorOrientation = mCameraInfoInternal.getSensorRotationDegrees();
         mLensFacing = mCameraInfoInternal.getLensFacing();
-        mFullFovRatio = calculateFullFovRatio(mCameraInfoInternal);
+        mFullFovRatio = activeArraySize != null ? calculateFullFovRatioFromActiveArraySize(
+                activeArraySize) : calculateFullFovRatioFromSupportedOutputSizes(
+                mCameraInfoInternal);
         // Determines the sensor resolution orientation info by the full FOV ratio.
         mIsSensorLandscapeResolution = mFullFovRatio != null ? mFullFovRatio.getNumerator()
                 >= mFullFovRatio.getDenominator() : true;
@@ -87,6 +90,14 @@
     }
 
     /**
+     * Calculates the full FOV ratio by the active array size.
+     */
+    @NonNull
+    private Rational calculateFullFovRatioFromActiveArraySize(@NonNull Size activeArraySize) {
+        return new Rational(activeArraySize.getWidth(), activeArraySize.getHeight());
+    }
+
+    /**
      * Calculates the full FOV ratio by the output sizes retrieved from CameraInfoInternal.
      *
      * <p>For most devices, the full FOV ratio should match the aspect ratio of the max supported
@@ -94,7 +105,8 @@
      * test to fail if it is not set in the test environment.
      */
     @Nullable
-    private Rational calculateFullFovRatio(@NonNull CameraInfoInternal cameraInfoInternal) {
+    private Rational calculateFullFovRatioFromSupportedOutputSizes(
+            @NonNull CameraInfoInternal cameraInfoInternal) {
         List<Size> jpegOutputSizes = cameraInfoInternal.getSupportedResolutions(ImageFormat.JPEG);
         if (jpegOutputSizes.isEmpty()) {
             return null;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
index d144363..40b9b28 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/DefaultSurfaceProcessor.java
@@ -29,6 +29,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.annotation.VisibleForTesting;
 import androidx.annotation.WorkerThread;
+import androidx.camera.core.Logger;
 import androidx.camera.core.SurfaceOutput;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
@@ -42,6 +43,7 @@
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -53,13 +55,14 @@
 @RequiresApi(21)
 public class DefaultSurfaceProcessor implements SurfaceProcessorInternal,
         SurfaceTexture.OnFrameAvailableListener {
+    private static final String TAG = "DefaultSurfaceProcessor";
     private final OpenGlRenderer mGlRenderer;
     @VisibleForTesting
     final HandlerThread mGlThread;
     private final Executor mGlExecutor;
     @VisibleForTesting
     final Handler mGlHandler;
-    private final AtomicBoolean mIsReleased = new AtomicBoolean(false);
+    private final AtomicBoolean mIsReleaseRequested = new AtomicBoolean(false);
     private final float[] mTextureMatrix = new float[16];
     private final float[] mSurfaceOutputMatrix = new float[16];
     // Map of current set of available outputs. Only access this on GL thread.
@@ -68,6 +71,8 @@
 
     // Only access this on GL thread.
     private int mInputSurfaceCount = 0;
+    // Only access this on GL thread.
+    private boolean mIsReleased = false;
 
     /** Constructs {@link DefaultSurfaceProcessor} with default shaders. */
     DefaultSurfaceProcessor() {
@@ -99,11 +104,11 @@
      */
     @Override
     public void onInputSurface(@NonNull SurfaceRequest surfaceRequest) {
-        if (mIsReleased.get()) {
+        if (mIsReleaseRequested.get()) {
             surfaceRequest.willNotProvideSurface();
             return;
         }
-        mGlExecutor.execute(() -> {
+        executeSafely(() -> {
             mInputSurfaceCount++;
             SurfaceTexture surfaceTexture = new SurfaceTexture(mGlRenderer.getTextureName());
             surfaceTexture.setDefaultBufferSize(surfaceRequest.getResolution().getWidth(),
@@ -117,7 +122,7 @@
                 checkReadyToRelease();
             });
             surfaceTexture.setOnFrameAvailableListener(this, mGlHandler);
-        });
+        }, surfaceRequest::willNotProvideSurface);
     }
 
     /**
@@ -125,11 +130,11 @@
      */
     @Override
     public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
-        if (mIsReleased.get()) {
+        if (mIsReleaseRequested.get()) {
             surfaceOutput.close();
             return;
         }
-        mGlExecutor.execute(() -> {
+        executeSafely(() -> {
             Surface surface = surfaceOutput.getSurface(mGlExecutor, event -> {
                 surfaceOutput.close();
                 Surface removedSurface = mOutputSurfaces.remove(surfaceOutput);
@@ -139,7 +144,7 @@
             });
             mGlRenderer.registerOutputSurface(surface);
             mOutputSurfaces.put(surfaceOutput, surface);
-        });
+        }, surfaceOutput::close);
     }
 
     /**
@@ -147,10 +152,13 @@
      */
     @Override
     public void release() {
-        if (mIsReleased.getAndSet(true)) {
+        if (mIsReleaseRequested.getAndSet(true)) {
             return;
         }
-        mGlExecutor.execute(this::checkReadyToRelease);
+        executeSafely(() -> {
+            mIsReleased = true;
+            checkReadyToRelease();
+        });
     }
 
     /**
@@ -158,7 +166,7 @@
      */
     @Override
     public void onFrameAvailable(@NonNull SurfaceTexture surfaceTexture) {
-        if (mIsReleased.get()) {
+        if (mIsReleaseRequested.get()) {
             // Ignore frame update if released.
             return;
         }
@@ -183,7 +191,7 @@
 
     @WorkerThread
     private void checkReadyToRelease() {
-        if (mIsReleased.get() && mInputSurfaceCount == 0) {
+        if (mIsReleased && mInputSurfaceCount == 0) {
             // Once release is called, we can stop sending frame to output surfaces.
             for (SurfaceOutput surfaceOutput : mOutputSurfaces.keySet()) {
                 surfaceOutput.close();
@@ -196,7 +204,7 @@
 
     private void initGlRenderer(@NonNull ShaderProvider shaderProvider) {
         ListenableFuture<Void> initFuture = CallbackToFutureAdapter.getFuture(completer -> {
-            mGlExecutor.execute(() -> {
+            executeSafely(() -> {
                 try {
                     mGlRenderer.init(shaderProvider);
                     completer.set(null);
@@ -220,6 +228,27 @@
         }
     }
 
+    private void executeSafely(@NonNull Runnable runnable) {
+        executeSafely(runnable, () -> {
+            // Do nothing.
+        });
+    }
+
+    private void executeSafely(@NonNull Runnable runnable, @NonNull Runnable onFailure) {
+        try {
+            mGlExecutor.execute(() -> {
+                if (mIsReleased) {
+                    onFailure.run();
+                } else {
+                    runnable.run();
+                }
+            });
+        } catch (RejectedExecutionException e) {
+            Logger.w(TAG, "Unable to executor runnable", e);
+            onFailure.run();
+        }
+    }
+
     /**
      * Factory class that produces {@link DefaultSurfaceProcessor}.
      *
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 36df55c..a6289c1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -16,8 +16,8 @@
 
 package androidx.camera.core;
 
-import static androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON;
 import static androidx.camera.core.MirrorMode.MIRROR_MODE_OFF;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -187,7 +187,7 @@
 
     @Test(expected = UnsupportedOperationException.class)
     public void setMirrorMode_throwException() {
-        new ImageAnalysis.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON);
+        new ImageAnalysis.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY);
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index bff9468..65f6131 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -33,7 +33,7 @@
 import androidx.camera.core.ImageCapture.ImageCaptureRequest
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
 import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
 import androidx.camera.core.impl.CameraConfig
 import androidx.camera.core.impl.CameraFactory
@@ -193,7 +193,7 @@
 
     @Test(expected = UnsupportedOperationException::class)
     fun setMirrorMode_throwException() {
-        ImageCapture.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+        ImageCapture.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 2968588..b6933d2 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -30,7 +30,7 @@
 import androidx.camera.core.CameraEffect.PREVIEW
 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
@@ -203,14 +203,14 @@
     }
 
     @Test
-    fun defaultMirrorModeIsFrontOn() {
+    fun defaultMirrorModeIsOnFrontOnly() {
         val preview = Preview.Builder().build()
-        assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_FRONT_ON)
+        assertThat(preview.mirrorModeInternal).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
     }
 
     @Test(expected = UnsupportedOperationException::class)
     fun setMirrorMode_throwException() {
-        Preview.Builder().setMirrorMode(MIRROR_MODE_FRONT_ON)
+        Preview.Builder().setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
     }
 
     @Test
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
index e7265d0..96211ee 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/SupportedOutputSizesSorterTest.kt
@@ -80,6 +80,8 @@
     Size(640, 480),
     Size(360, 480)
 )
+private val LANDSCAPE_ACTIVE_ARRAY_SIZE = Size(4032, 3024)
+private val PORTRAIT_ACTIVE_ARRAY_SIZE = Size(1440, 1920)
 
 /**
  * Unit tests for [SupportedOutputSizesSorter].
@@ -92,7 +94,8 @@
         setSupportedResolutions(ImageFormat.JPEG, DEFAULT_SUPPORTED_SIZES)
         setSupportedHighResolutions(ImageFormat.JPEG, HIGH_RESOLUTION_SUPPORTED_SIZES)
     }
-    private val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+    private val supportedOutputSizesSorter =
+        SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
 
     @Test
     fun canSelectCustomOrderedResolutions() {
@@ -101,7 +104,8 @@
         val cameraInfoInternal = FakeCameraInfoInternal().apply {
             setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
         }
-        val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+        val supportedOutputSizesSorter =
+            SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
         // Sets up the custom ordered resolutions
         val useCaseConfig =
             FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
@@ -122,7 +126,8 @@
         val cameraInfoInternal = FakeCameraInfoInternal().apply {
             setSupportedResolutions(imageFormat, DEFAULT_SUPPORTED_SIZES)
         }
-        val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+        val supportedOutputSizesSorter =
+            SupportedOutputSizesSorter(cameraInfoInternal, LANDSCAPE_ACTIVE_ARRAY_SIZE)
         // Sets up the custom supported resolutions
         val useCaseConfig =
             FakeUseCaseConfig.Builder(CaptureType.IMAGE_CAPTURE, imageFormat).apply {
@@ -367,7 +372,8 @@
         val cameraInfoInternal = FakeCameraInfoInternal().apply {
             setSupportedResolutions(ImageFormat.JPEG, PORTRAIT_SUPPORTED_SIZES)
         }
-        val supportedOutputSizesSorter = SupportedOutputSizesSorter(cameraInfoInternal)
+        val supportedOutputSizesSorter =
+            SupportedOutputSizesSorter(cameraInfoInternal, PORTRAIT_ACTIVE_ARRAY_SIZE)
         verifySupportedOutputSizesWithResolutionSelectorSettings(
             outputSizesSorter = supportedOutputSizesSorter,
             preferredAspectRatio = AspectRatio.RATIO_16_9,
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
index 4c84b93..170b11b 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/EncoderProfilesUtil.java
@@ -155,7 +155,25 @@
             int videoFrameWidth,
             int videoFrameHeight
     ) {
-        VideoProfileProxy videoProfile = VideoProfileProxy.create(
+        VideoProfileProxy videoProfile = createFakeVideoProfileProxy(videoFrameWidth,
+                videoFrameHeight);
+        AudioProfileProxy audioProfile = createFakeAudioProfileProxy();
+
+        return ImmutableEncoderProfilesProxy.create(
+                DEFAULT_DURATION,
+                DEFAULT_OUTPUT_FORMAT,
+                Collections.singletonList(audioProfile),
+                Collections.singletonList(videoProfile)
+        );
+    }
+
+    /** A utility method to create a VideoProfileProxy with some default values. */
+    @NonNull
+    public static VideoProfileProxy createFakeVideoProfileProxy(
+            int videoFrameWidth,
+            int videoFrameHeight
+    ) {
+        return VideoProfileProxy.create(
                 DEFAULT_VIDEO_CODEC,
                 DEFAULT_VIDEO_MEDIA_TYPE,
                 DEFAULT_VIDEO_BITRATE,
@@ -167,7 +185,12 @@
                 DEFAULT_VIDEO_CHROMA_SUBSAMPLING,
                 DEFAULT_VIDEO_HDR_FORMAT
         );
-        AudioProfileProxy audioProfile = AudioProfileProxy.create(
+    }
+
+    /** A utility method to create an AudioProfileProxy with some default values. */
+    @NonNull
+    public static AudioProfileProxy createFakeAudioProfileProxy() {
+        return AudioProfileProxy.create(
                 DEFAULT_AUDIO_CODEC,
                 DEFAULT_AUDIO_MEDIA_TYPE,
                 DEFAULT_AUDIO_BITRATE,
@@ -175,13 +198,6 @@
                 DEFAULT_AUDIO_CHANNELS,
                 DEFAULT_AUDIO_PROFILE
         );
-
-        return ImmutableEncoderProfilesProxy.create(
-                DEFAULT_DURATION,
-                DEFAULT_OUTPUT_FORMAT,
-                Collections.singletonList(audioProfile),
-                Collections.singletonList(videoProfile)
-        );
     }
 
     // This class is not instantiable.
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
index dae1356..dd5ac4e 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/LabTestRule.kt
@@ -18,6 +18,7 @@
 
 import android.util.Log
 import androidx.annotation.RequiresApi
+import androidx.camera.core.CameraSelector
 import androidx.camera.testing.LabTestRule.LabTestFrontCamera
 import androidx.camera.testing.LabTestRule.LabTestOnly
 import androidx.camera.testing.LabTestRule.LabTestRearCamera
@@ -142,5 +143,23 @@
         fun isInLabTest(): Boolean {
             return Log.isLoggable("MH", Log.DEBUG)
         }
+
+        /**
+         * Checks if it is CameraX lab environment where the enabled camera uses the specified
+         * [lensFacing] direction.
+         *
+         * For example, if [lensFacing] is [CameraSelector.LENS_FACING_BACK], this method will
+         * return true if the rear camera is enabled on a device in CameraX lab environment.
+         *
+         * @param lensFacing the required camera direction relative to the device screen.
+         * @return if enabled camera is in same direction as [lensFacing] in CameraX lab environment
+         */
+        @JvmStatic
+        fun isLensFacingEnabledInLabTest(@CameraSelector.LensFacing lensFacing: Int) =
+            when (lensFacing) {
+                CameraSelector.LENS_FACING_BACK -> Log.isLoggable("rearCameraE2E", Log.DEBUG)
+                CameraSelector.LENS_FACING_FRONT -> Log.isLoggable("frontCameraE2E", Log.DEBUG)
+                else -> false
+            }
     }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
index 29855d5..41abf57 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraInfoInternal.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.testing.fakes;
 
+import static androidx.camera.core.DynamicRange.SDR;
+
 import android.util.Range;
 import android.util.Rational;
 import android.util.Size;
@@ -26,6 +28,7 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraState;
+import androidx.camera.core.DynamicRange;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.TorchState;
@@ -47,8 +50,10 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Executor;
 
 /**
@@ -64,6 +69,7 @@
                     new Range<>(30, 30),
                     new Range<>(60, 60))
     );
+    private static final Set<DynamicRange> DEFAULT_DYNAMIC_RANGES = Collections.singleton(SDR);
     private final String mCameraId;
     private final int mSensorRotation;
     @CameraSelector.LensFacing
@@ -73,6 +79,8 @@
     private final Map<Integer, List<Size>> mSupportedResolutionMap = new HashMap<>();
     private final Map<Integer, List<Size>> mSupportedHighResolutionMap = new HashMap<>();
     private MutableLiveData<CameraState> mCameraStateLiveData;
+
+    private final Set<DynamicRange> mSupportedDynamicRanges = new HashSet<>(DEFAULT_DYNAMIC_RANGES);
     private String mImplementationType = IMPLEMENTATION_TYPE_FAKE;
 
     // Leave uninitialized to support camera-core:1.0.0 dependencies.
@@ -206,6 +214,12 @@
         return resolutions != null ? resolutions : Collections.emptyList();
     }
 
+    @NonNull
+    @Override
+    public Set<DynamicRange> getSupportedDynamicRanges() {
+        return mSupportedDynamicRanges;
+    }
+
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
@@ -294,6 +308,12 @@
         mIntrinsicZoomRatio = zoomRatio;
     }
 
+    /** Set the supported dynamic ranges for testing */
+    public void setSupportedDynamicRanges(@NonNull Set<DynamicRange> dynamicRanges) {
+        mSupportedDynamicRanges.clear();
+        mSupportedDynamicRanges.addAll(dynamicRanges);
+    }
+
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     static final class FakeExposureState implements ExposureState {
         @Override
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
index b5d9233..7855897 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraInfoTest.java
@@ -79,4 +79,9 @@
         assertThat(mFakeCameraInfo.getSupportedFpsRanges()).isNotEmpty();
 
     }
+
+    @Test
+    public void canRetrieveSupportedDynamicRanges() {
+        assertThat(mFakeCameraInfo.getSupportedDynamicRanges()).isNotEmpty();
+    }
 }
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -132,7 +132,9 @@
   }
 
   @RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+    method public int getMirrorMode();
     method public T getOutput();
+    method public android.util.Range<java.lang.Integer!> getTargetFramerate();
     method public int getTargetRotation();
     method public void setTargetRotation(int);
     method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
   @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture> {
     ctor public VideoCapture.Builder(T);
     method public androidx.camera.video.VideoCapture<T!> build();
+    method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+    method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFramerate(android.util.Range<java.lang.Integer!>);
     method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
   }
 
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -132,7 +132,9 @@
   }
 
   @RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+    method public int getMirrorMode();
     method public T getOutput();
+    method public android.util.Range<java.lang.Integer!> getTargetFramerate();
     method public int getTargetRotation();
     method public void setTargetRotation(int);
     method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
   @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture> {
     ctor public VideoCapture.Builder(T);
     method public androidx.camera.video.VideoCapture<T!> build();
+    method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+    method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFramerate(android.util.Range<java.lang.Integer!>);
     method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
   }
 
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index 208d9cd..379c11e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -132,7 +132,9 @@
   }
 
   @RequiresApi(21) public final class VideoCapture<T extends androidx.camera.video.VideoOutput> extends androidx.camera.core.UseCase {
+    method public int getMirrorMode();
     method public T getOutput();
+    method public android.util.Range<java.lang.Integer!> getTargetFramerate();
     method public int getTargetRotation();
     method public void setTargetRotation(int);
     method public void setTargetRotationDegrees(int);
@@ -142,6 +144,8 @@
   @RequiresApi(21) public static final class VideoCapture.Builder<T extends androidx.camera.video.VideoOutput> implements androidx.camera.core.ExtendableBuilder<androidx.camera.video.VideoCapture> {
     ctor public VideoCapture.Builder(T);
     method public androidx.camera.video.VideoCapture<T!> build();
+    method public androidx.camera.video.VideoCapture.Builder<T!> setMirrorMode(int);
+    method public androidx.camera.video.VideoCapture.Builder<T!> setTargetFramerate(android.util.Range<java.lang.Integer!>);
     method public androidx.camera.video.VideoCapture.Builder<T!> setTargetRotation(int);
   }
 
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
index 3eb0bf2..eb803dd 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCapture.java
@@ -32,6 +32,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_FRAME_RATE;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.isMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.rectToString;
@@ -72,6 +73,7 @@
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Logger;
 import androidx.camera.core.MirrorMode;
+import androidx.camera.core.Preview;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
@@ -253,6 +255,21 @@
     }
 
     /**
+     * Returns the target frame rate range for the associated VideoCapture use case.
+     *
+     * <p>The rotation can be set prior to constructing a VideoCapture using
+     * {@link VideoCapture.Builder#setTargetFramerate(Range)}
+     * If not set, the target frame rate defaults to the value of
+     * {@link StreamSpec#FRAME_RATE_RANGE_UNSPECIFIED}
+     *
+     * @return The rotation of the intended target.
+     */
+    @NonNull
+    public Range<Integer> getTargetFramerate() {
+        return getTargetFramerateInternal();
+    }
+
+    /**
      * Sets the desired rotation of the output video.
      *
      * <p>Valid values include: {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90},
@@ -366,17 +383,14 @@
         setTargetRotation(orientationDegreesToSurfaceRotation(degrees));
     }
 
-    // TODO: to public API
     /**
      * Returns the mirror mode.
      *
      * <p>The mirror mode is set by {@link VideoCapture.Builder#setMirrorMode(int)}. If not set,
-     * it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+     * it defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
      *
      * @return The mirror mode of the intended target.
-     *
      */
-    @RestrictTo(Scope.LIBRARY_GROUP)
     @MirrorMode.Mirror
     public int getMirrorMode() {
         return getMirrorModeInternal();
@@ -384,6 +398,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @SuppressWarnings("unchecked")
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -409,6 +424,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @Override
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -419,6 +435,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Override
@@ -445,6 +462,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @Override
@@ -465,6 +483,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     @NonNull
@@ -479,6 +498,7 @@
 
     /**
      * {@inheritDoc}
+     *
      */
     @NonNull
     @RestrictTo(Scope.LIBRARY_GROUP)
@@ -682,6 +702,7 @@
     }
 
     /**
+     *
      */
     @Nullable
     @RestrictTo(Scope.TESTS)
@@ -694,6 +715,7 @@
      *
      * <p>These values may be overridden by the implementation. They only provide a minimum set of
      * defaults that are implementation independent.
+     *
      */
     @RestrictTo(Scope.LIBRARY_GROUP)
     public static final class Defaults implements ConfigProvider<VideoCaptureConfig<?>> {
@@ -1309,6 +1331,7 @@
 
         /**
          * {@inheritDoc}
+         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Override
@@ -1319,6 +1342,7 @@
 
         /**
          * {@inheritDoc}
+         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -1390,6 +1414,7 @@
          * setTargetAspectRatio is not supported on VideoCapture
          *
          * <p>To set aspect ratio, see {@link Recorder.Builder#setAspectRatio(int)}.
+         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -1432,19 +1457,21 @@
             return this;
         }
 
-        // TODO: to public API
         /**
          * Sets the mirror mode.
          *
          * <p>Valid values include: {@link MirrorMode#MIRROR_MODE_OFF},
-         * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_FRONT_ON}.
-         * If not set, it is defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+         * {@link MirrorMode#MIRROR_MODE_ON} and {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY}.
+         * If not set, it defaults to {@link MirrorMode#MIRROR_MODE_OFF}.
+         *
+         * <p>This API only changes the mirroring behavior on VideoCapture, but does not affect
+         * other UseCases. If the application wants to be consistent with the default
+         * {@link Preview} behavior where the rear camera is not mirrored but the front camera is
+         * mirrored, then {@link MirrorMode#MIRROR_MODE_ON_FRONT_ONLY} is recommended.
          *
          * @param mirrorMode The mirror mode of the intended target.
          * @return The current Builder.
-         *
          */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         @Override
         public Builder<T> setMirrorMode(@MirrorMode.Mirror int mirrorMode) {
@@ -1456,6 +1483,7 @@
          * setTargetResolution is not supported on VideoCapture
          *
          * <p>To set resolution, see {@link Recorder.Builder#setQualitySelector(QualitySelector)}.
+         *
          */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -1606,5 +1634,22 @@
             getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
             return this;
         }
+
+        /**
+         * Sets the target frame rate range for the associated VideoCapture use case.
+         *
+         * <p>This target will be used as a part of the heuristics for the algorithm that determines
+         * the final frame rate range and resolution of all concurrently bound use cases.
+         * <p>It is not guaranteed that this target frame rate will be the final range,
+         * as other use cases as well as frame rate restrictions of the device may affect the
+         * outcome of the algorithm that chooses the actual frame rate.
+         *
+         * @param targetFrameRate the target frame rate range.
+         */
+        @NonNull
+        public Builder<T> setTargetFramerate(@NonNull Range<Integer> targetFrameRate) {
+            getMutableConfig().insertOption(OPTION_TARGET_FRAME_RATE, targetFrameRate);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java
new file mode 100644
index 0000000..b8c05c1
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProvider.java
@@ -0,0 +1,123 @@
+/*
+ * 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.camera.video.internal;
+
+import static androidx.camera.video.internal.utils.DynamicRangeUtil.DR_TO_VP_BIT_DEPTH_MAP;
+import static androidx.camera.video.internal.utils.DynamicRangeUtil.DR_TO_VP_FORMAT_MAP;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.DynamicRange;
+import androidx.camera.core.impl.EncoderProfilesProvider;
+import androidx.camera.core.impl.EncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy;
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation that provides {@link EncoderProfilesProxy} containing video information
+ * matched with the target {@link DynamicRange}.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class DynamicRangeMatchedEncoderProfilesProvider implements EncoderProfilesProvider {
+
+    private final EncoderProfilesProvider mEncoderProfilesProvider;
+    private final DynamicRange mDynamicRange;
+    private final Map<Integer, EncoderProfilesProxy> mEncoderProfilesCache = new HashMap<>();
+
+    public DynamicRangeMatchedEncoderProfilesProvider(@NonNull EncoderProfilesProvider provider,
+            @NonNull DynamicRange dynamicRange) {
+        mEncoderProfilesProvider = provider;
+        mDynamicRange = dynamicRange;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean hasProfile(int quality) {
+        if (!mEncoderProfilesProvider.hasProfile(quality)) {
+            return false;
+        }
+
+        return getProfilesInternal(quality) != null;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable
+    @Override
+    public EncoderProfilesProxy getAll(int quality) {
+        return getProfilesInternal(quality);
+    }
+
+    @Nullable
+    private EncoderProfilesProxy getProfilesInternal(int quality) {
+        if (mEncoderProfilesCache.containsKey(quality)) {
+            return mEncoderProfilesCache.get(quality);
+        }
+
+        EncoderProfilesProxy profiles = null;
+        if (mEncoderProfilesProvider.hasProfile(quality)) {
+            EncoderProfilesProxy baseProfiles = mEncoderProfilesProvider.getAll(quality);
+            profiles = filterUnmatchedDynamicRange(baseProfiles, mDynamicRange);
+            mEncoderProfilesCache.put(quality, profiles);
+        }
+
+        return profiles;
+    }
+
+    @Nullable
+    private static EncoderProfilesProxy filterUnmatchedDynamicRange(
+            @Nullable EncoderProfilesProxy encoderProfiles, @NonNull DynamicRange dynamicRange) {
+        if (encoderProfiles == null) {
+            return null;
+        }
+
+        List<VideoProfileProxy> validVideoProfiles = new ArrayList<>();
+        for (VideoProfileProxy videoProfile : encoderProfiles.getVideoProfiles()) {
+            if (isBitDepthMatched(videoProfile, dynamicRange) && isHdrFormatMatched(videoProfile,
+                    dynamicRange)) {
+                validVideoProfiles.add(videoProfile);
+            }
+        }
+
+        return validVideoProfiles.isEmpty() ? null : ImmutableEncoderProfilesProxy.create(
+                encoderProfiles.getDefaultDurationSeconds(),
+                encoderProfiles.getRecommendedFileFormat(),
+                encoderProfiles.getAudioProfiles(),
+                validVideoProfiles
+        );
+    }
+
+    private static boolean isBitDepthMatched(@NonNull VideoProfileProxy videoProfile,
+            @NonNull DynamicRange dynamicRange) {
+        Set<Integer> matchedBitDepths = DR_TO_VP_BIT_DEPTH_MAP.get(dynamicRange.getBitDepth());
+
+        return matchedBitDepths != null && matchedBitDepths.contains(videoProfile.getBitDepth());
+    }
+
+    private static boolean isHdrFormatMatched(@NonNull VideoProfileProxy videoProfile,
+            @NonNull DynamicRange dynamicRange) {
+        Set<Integer> matchedHdrFormats = DR_TO_VP_FORMAT_MAP.get(dynamicRange.getFormat());
+
+        return matchedHdrFormats != null && matchedHdrFormats.contains(videoProfile.getHdrFormat());
+    }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
new file mode 100644
index 0000000..2b081c7
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/utils/DynamicRangeUtil.java
@@ -0,0 +1,76 @@
+/*
+ * 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.camera.video.internal.utils;
+
+import static android.media.EncoderProfiles.VideoProfile.HDR_DOLBY_VISION;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HDR10;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HDR10PLUS;
+import static android.media.EncoderProfiles.VideoProfile.HDR_HLG;
+import static android.media.EncoderProfiles.VideoProfile.HDR_NONE;
+
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT;
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_8_BIT;
+import static androidx.camera.core.DynamicRange.BIT_DEPTH_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR10;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS;
+import static androidx.camera.core.DynamicRange.FORMAT_HDR_UNSPECIFIED;
+import static androidx.camera.core.DynamicRange.FORMAT_HLG;
+import static androidx.camera.core.DynamicRange.FORMAT_SDR;
+import static androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_10;
+import static androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+
+import androidx.annotation.RequiresApi;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Utility class for dynamic range related operations.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class DynamicRangeUtil {
+
+    public static final Map<Integer, Set<Integer>> DR_TO_VP_BIT_DEPTH_MAP = new HashMap<>();
+    public static final Map<Integer, Set<Integer>> DR_TO_VP_FORMAT_MAP = new HashMap<>();
+
+    private DynamicRangeUtil() {
+    }
+
+    static {
+        // DynamicRange bit depth to VideoProfile bit depth.
+        DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_8_BIT, new HashSet<>(singletonList(BIT_DEPTH_8)));
+        DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_10_BIT, new HashSet<>(singletonList(BIT_DEPTH_10)));
+        DR_TO_VP_BIT_DEPTH_MAP.put(BIT_DEPTH_UNSPECIFIED,
+                new HashSet<>(asList(BIT_DEPTH_8, BIT_DEPTH_10)));
+
+        // DynamicRange format to VideoProfile HDR format.
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_SDR, new HashSet<>(singletonList(HDR_NONE)));
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR_UNSPECIFIED,
+                new HashSet<>(asList(HDR_HLG, HDR_HDR10, HDR_HDR10PLUS, HDR_DOLBY_VISION)));
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_HLG, new HashSet<>(singletonList(HDR_HLG)));
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR10, new HashSet<>(singletonList(HDR_HDR10)));
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_HDR10_PLUS, new HashSet<>(singletonList(HDR_HDR10PLUS)));
+        DR_TO_VP_FORMAT_MAP.put(FORMAT_DOLBY_VISION,
+                new HashSet<>(singletonList(HDR_DOLBY_VISION)));
+    }
+}
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
index c03128a..d0e5fb9 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureTest.kt
@@ -43,7 +43,7 @@
 import androidx.camera.core.CameraSelector.LENS_FACING_BACK
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.CameraXConfig
-import androidx.camera.core.MirrorMode.MIRROR_MODE_FRONT_ON
+import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
 import androidx.camera.core.MirrorMode.MIRROR_MODE_OFF
 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON
 import androidx.camera.core.SurfaceRequest
@@ -796,8 +796,8 @@
 
     @Test
     fun canGetSetMirrorMode() {
-        val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_FRONT_ON)
-        assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_FRONT_ON)
+        val videoCapture = createVideoCapture(mirrorMode = MIRROR_MODE_ON_FRONT_ONLY)
+        assertThat(videoCapture.mirrorMode).isEqualTo(MIRROR_MODE_ON_FRONT_ONLY)
     }
 
     @Test
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt
new file mode 100644
index 0000000..7c9a766
--- /dev/null
+++ b/camera/camera-video/src/test/java/androidx/camera/video/internal/DynamicRangeMatchedEncoderProfilesProviderTest.kt
@@ -0,0 +1,216 @@
+/*
+ * 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.camera.video.internal
+
+import android.media.CamcorderProfile.QUALITY_1080P
+import android.media.EncoderProfiles.VideoProfile.HDR_DOLBY_VISION
+import android.media.EncoderProfiles.VideoProfile.HDR_HDR10
+import android.media.EncoderProfiles.VideoProfile.HDR_HDR10PLUS
+import android.media.EncoderProfiles.VideoProfile.HDR_HLG
+import android.media.EncoderProfiles.VideoProfile.HDR_NONE
+import android.os.Build
+import androidx.camera.core.DynamicRange
+import androidx.camera.core.DynamicRange.BIT_DEPTH_10_BIT
+import androidx.camera.core.DynamicRange.FORMAT_DOLBY_VISION
+import androidx.camera.core.DynamicRange.FORMAT_HDR10
+import androidx.camera.core.DynamicRange.FORMAT_HDR10_PLUS
+import androidx.camera.core.DynamicRange.FORMAT_HLG
+import androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT
+import androidx.camera.core.DynamicRange.SDR
+import androidx.camera.core.impl.EncoderProfilesProvider
+import androidx.camera.core.impl.EncoderProfilesProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.ImmutableEncoderProfilesProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_10
+import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy.BIT_DEPTH_8
+import androidx.camera.testing.EncoderProfilesUtil
+import androidx.camera.testing.EncoderProfilesUtil.RESOLUTION_1080P
+import androidx.camera.testing.EncoderProfilesUtil.createFakeAudioProfileProxy
+import androidx.camera.testing.EncoderProfilesUtil.createFakeVideoProfileProxy
+import androidx.camera.testing.fakes.FakeEncoderProfilesProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class DynamicRangeMatchedEncoderProfilesProviderTest {
+
+    private val defaultProvider = createFakeEncoderProfilesProvider(
+        arrayOf(Pair(QUALITY_1080P, PROFILES_1080P_FULL_DYNAMIC_RANGE))
+    )
+
+    @Test
+    fun hasNoProfile_canNotGetProfiles() {
+        val emptyProvider = createFakeEncoderProfilesProvider()
+        val sdrProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, SDR)
+        val hlgProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HLG)
+        val hdr10Provider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR10)
+        val hdr10PlusProvider =
+            DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR10_PLUS)
+        val dolbyProvider = DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, DOLBY_VISION)
+        val hdrUnspecifiedProvider =
+            DynamicRangeMatchedEncoderProfilesProvider(emptyProvider, HDR_UNSPECIFIED_10_BIT)
+
+        assertThat(sdrProvider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(hlgProvider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(hdr10Provider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(hdr10PlusProvider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(dolbyProvider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(hdrUnspecifiedProvider.hasProfile(QUALITY_1080P)).isFalse()
+        assertThat(sdrProvider.getAll(QUALITY_1080P)).isNull()
+        assertThat(hlgProvider.getAll(QUALITY_1080P)).isNull()
+        assertThat(hdr10Provider.getAll(QUALITY_1080P)).isNull()
+        assertThat(hdr10PlusProvider.getAll(QUALITY_1080P)).isNull()
+        assertThat(dolbyProvider.getAll(QUALITY_1080P)).isNull()
+        assertThat(hdrUnspecifiedProvider.getAll(QUALITY_1080P)).isNull()
+    }
+
+    @Test
+    fun sdr_onlyContainsSdrProfile() {
+        val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, SDR)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 1).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_NONE).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_8).isTrue()
+    }
+
+    @Test
+    fun hlg_onlyContainsHlgProfile() {
+        val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HLG)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 1).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_HLG).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+    }
+
+    @Test
+    fun hdr10_onlyContainsHdr10Profile() {
+        val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR10)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 1).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_HDR10).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+    }
+
+    @Test
+    fun hdr10Plus_onlyContainsHdr10PlusProfile() {
+        val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR10_PLUS)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 1).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_HDR10PLUS).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+    }
+
+    @Test
+    fun dolbyVision_onlyContainsDolbyVisionProfile() {
+        val provider = DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, DOLBY_VISION)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 1).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_DOLBY_VISION).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+    }
+
+    @Test
+    fun hdrUnspecified_containsAllHdrProfiles() {
+        val provider =
+            DynamicRangeMatchedEncoderProfilesProvider(defaultProvider, HDR_UNSPECIFIED_10_BIT)
+
+        assertThat(provider.hasProfile(QUALITY_1080P)).isTrue()
+        val videoProfiles = provider.getAll(QUALITY_1080P)!!.videoProfiles
+        assertThat(videoProfiles.size == 4).isTrue()
+        assertThat(videoProfiles[0].hdrFormat == HDR_HLG).isTrue()
+        assertThat(videoProfiles[1].hdrFormat == HDR_HDR10).isTrue()
+        assertThat(videoProfiles[2].hdrFormat == HDR_HDR10PLUS).isTrue()
+        assertThat(videoProfiles[3].hdrFormat == HDR_DOLBY_VISION).isTrue()
+        assertThat(videoProfiles[0].bitDepth == BIT_DEPTH_10).isTrue()
+        assertThat(videoProfiles[1].bitDepth == BIT_DEPTH_10).isTrue()
+        assertThat(videoProfiles[2].bitDepth == BIT_DEPTH_10).isTrue()
+        assertThat(videoProfiles[3].bitDepth == BIT_DEPTH_10).isTrue()
+    }
+
+    private fun createFakeEncoderProfilesProvider(
+        qualityToProfilesPairs: Array<Pair<Int, EncoderProfilesProxy>> = emptyArray()
+    ): EncoderProfilesProvider {
+        return FakeEncoderProfilesProvider.Builder().also { builder ->
+            for (pair in qualityToProfilesPairs) {
+                builder.add(pair.first, pair.second)
+            }
+        }.build()
+    }
+
+    companion object {
+        private val HLG = DynamicRange(FORMAT_HLG, BIT_DEPTH_10_BIT)
+        private val HDR10 = DynamicRange(FORMAT_HDR10, BIT_DEPTH_10_BIT)
+        private val HDR10_PLUS = DynamicRange(FORMAT_HDR10_PLUS, BIT_DEPTH_10_BIT)
+        private val DOLBY_VISION = DynamicRange(FORMAT_DOLBY_VISION, BIT_DEPTH_10_BIT)
+        private val VIDEO_PROFILES_1080P_SDR =
+            createFakeVideoProfileProxy(RESOLUTION_1080P.width, RESOLUTION_1080P.height)
+        private val VIDEO_PROFILES_1080P_HLG =
+            VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HLG, BIT_DEPTH_10)
+        private val VIDEO_PROFILES_1080P_HDR10 =
+            VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HDR10, BIT_DEPTH_10)
+        private val VIDEO_PROFILES_1080P_HDR10_PLUS =
+            VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_HDR10PLUS, BIT_DEPTH_10)
+        private val VIDEO_PROFILES_1080P_DOLBY_VISION =
+            VIDEO_PROFILES_1080P_SDR.modifyDynamicRangeInfo(HDR_DOLBY_VISION, BIT_DEPTH_10)
+        private val PROFILES_1080P_FULL_DYNAMIC_RANGE = ImmutableEncoderProfilesProxy.create(
+            EncoderProfilesUtil.DEFAULT_DURATION,
+            EncoderProfilesUtil.DEFAULT_OUTPUT_FORMAT,
+            listOf(createFakeAudioProfileProxy()),
+            listOf(
+                VIDEO_PROFILES_1080P_SDR,
+                VIDEO_PROFILES_1080P_HLG,
+                VIDEO_PROFILES_1080P_HDR10,
+                VIDEO_PROFILES_1080P_HDR10_PLUS,
+                VIDEO_PROFILES_1080P_DOLBY_VISION
+            )
+        )
+
+        private fun VideoProfileProxy.modifyDynamicRangeInfo(
+            hdrFormat: Int,
+            bitDepth: Int
+        ): VideoProfileProxy {
+            return VideoProfileProxy.create(
+                this.codec,
+                this.mediaType,
+                this.bitrate,
+                this.frameRate,
+                this.width,
+                this.height,
+                this.profile,
+                bitDepth,
+                this.chromaSubsampling,
+                hdrFormat
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
index 88c8b26..870c376 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/SignalGeneratorViewModelTest.kt
@@ -35,6 +35,7 @@
 import android.content.Context
 import android.os.Build
 import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraSelector
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeLifecycleOwner
@@ -121,6 +122,8 @@
 
     @Test
     fun initialRecorder_canMakeRecorderReady(): Unit = runBlocking {
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
         viewModel.initialRecorder(context, lifecycleOwner)
 
         assertThat(viewModel.isRecorderReady).isTrue()
@@ -167,6 +170,8 @@
 
     @Test
     fun startAndStopRecording_canWorkCorrectlyAfterRecorderReady(): Unit = runBlocking {
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
         // Arrange.
         viewModel.initialRecorder(context, lifecycleOwner)
 
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 f96b0b1..42ed11b 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
@@ -39,6 +39,7 @@
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.LabTestRule.Companion.isLensFacingEnabledInLabTest
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
@@ -311,6 +312,85 @@
         assertFutureCompletes(future)
     }
 
+    /**
+     * The following tests check if a device can complete 3A convergence, by setting an auto
+     * cancellation with [FocusMeteringAction.Builder.setAutoCancelDuration] which ensures throwing
+     * an exception in case of a timeout.
+     *
+     * Since some devices may require a long time to complete convergence, we are setting a long
+     * [FocusMeteringAction.mAutoCancelDurationInMillis] in these tests.
+     */
+
+    @Test
+    fun futureCompletes_whenFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+        Assume.assumeTrue(
+            "Not CameraX lab environment," +
+                " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+            isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+        )
+
+        Assume.assumeTrue(
+            "No AF/AE/AWB region available on this device!",
+            hasMeteringRegion(cameraSelector)
+        )
+
+        val focusMeteringAction = FocusMeteringAction.Builder(validMeteringPoint)
+            .setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+            .build()
+
+        val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+        assertFutureCompletes(resultFuture)
+    }
+
+    @Test
+    fun futureCompletes_whenOnlyAfFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+        Assume.assumeTrue(
+            "Not CameraX lab environment," +
+                " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+            isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+        )
+
+        Assume.assumeTrue(
+            "No AF region available on this device!",
+            hasMeteringRegion(cameraSelector, FLAG_AF)
+        )
+
+        val focusMeteringAction = FocusMeteringAction.Builder(
+            validMeteringPoint,
+            FLAG_AF
+        ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+            .build()
+
+        val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+        assertFutureCompletes(resultFuture)
+    }
+
+    @Test
+    fun futureCompletes_whenAeAwbFocusMeteringStartedWithLongCancelDuration() = runBlocking {
+        Assume.assumeTrue(
+            "Not CameraX lab environment," +
+                " or lensFacing:${cameraSelector.lensFacing!!} camera is not enabled",
+            isLensFacingEnabledInLabTest(lensFacing = cameraSelector.lensFacing!!)
+        )
+
+        Assume.assumeTrue(
+            "No AE/AWB region available on this device!",
+            hasMeteringRegion(cameraSelector, FLAG_AE or FLAG_AWB)
+        )
+
+        val focusMeteringAction = FocusMeteringAction.Builder(
+            validMeteringPoint,
+            FLAG_AE or FLAG_AWB
+        ).setAutoCancelDuration(5_000, TimeUnit.MILLISECONDS)
+            .build()
+
+        val resultFuture = camera.cameraControl.startFocusAndMetering(focusMeteringAction)
+
+        assertFutureCompletes(resultFuture)
+    }
+
     private fun hasMeteringRegion(
         selector: CameraSelector,
         @FocusMeteringAction.MeteringMode flags: Int = FLAG_AF or FLAG_AE or FLAG_AWB
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
index e8e96f9..96508df 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageAnalysisTest.kt
@@ -140,6 +140,8 @@
 
     @Test
     fun exceedMaxImagesWithoutClosing_doNotCrash() = runBlocking {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
         // Arrange.
         val queueDepth = 3
         val semaphore = Semaphore(0)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 129edde..c8c1d21 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -1182,6 +1182,8 @@
 
     @Test
     fun useCaseCanBeReusedInDifferentCamera() = runBlocking {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
         val useCase = defaultBuilder.build()
         withContext(Dispatchers.Main) {
             cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index b744eea..f7b75c4 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -108,6 +108,8 @@
     @Before
     @Throws(ExecutionException::class, InterruptedException::class)
     fun setUp() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+
         context = ApplicationProvider.getApplicationContext()
         CameraXUtil.initialize(context!!, cameraConfig).get()
 
@@ -424,6 +426,8 @@
     @Test
     @Throws(InterruptedException::class)
     fun useCaseCanBeReusedInDifferentCamera() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT))
+
         val preview = defaultBuilder!!.build()
         instrumentation.runOnMainSync { preview.setSurfaceProvider(getSurfaceProvider(null)) }
 
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 04bdd7d..c745a9f 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -24,6 +24,7 @@
 import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
+import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
@@ -1514,7 +1515,9 @@
             if (mVideoQuality != QUALITY_AUTO) {
                 builder.setQualitySelector(QualitySelector.from(mVideoQuality));
             }
-            VideoCapture<Recorder> videoCapture = VideoCapture.withOutput(builder.build());
+            VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(builder.build())
+                    .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
+                    .build();
             useCases.add(videoCapture);
         }
         return useCases;
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
index df83e57..fe3031d 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifest.xml
@@ -47,8 +47,7 @@
 
   <application
     android:label="@string/app_name"
-    android:icon="@drawable/ic_launcher"
-    android:extractNativeLibs="false">
+    android:icon="@drawable/ic_launcher">
 
     <meta-data
         android:name="com.android.automotive"
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
index 0fb4867..091708d 100644
--- a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -56,8 +56,7 @@
 
   <application
     android:label="@string/app_name"
-    android:icon="@drawable/ic_launcher"
-    android:extractNativeLibs="false">
+    android:icon="@drawable/ic_launcher">
 
     <meta-data
         android:name="com.android.automotive"
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
index b9ba68b..5ea244a 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifest.xml
@@ -30,8 +30,7 @@
 
   <application
       android:label="@string/app_name"
-      android:icon="@drawable/ic_launcher"
-      android:extractNativeLibs="false">
+      android:icon="@drawable/ic_launcher">
 
     <activity
         android:name="androidx.car.app.sample.navigation.common.app.MainActivity"
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 3fda109c..d88bd0d 100644
--- a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -56,8 +56,7 @@
 
     <application
         android:icon="@drawable/ic_launcher"
-        android:label="@string/app_name"
-        android:extractNativeLibs="false">
+        android:label="@string/app_name">
 
          <meta-data
              android:name="com.android.automotive"
@@ -105,4 +104,4 @@
         </activity>
 
     </application>
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
index 92aba90..5dac37e 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifest.xml
@@ -60,7 +60,6 @@
   <application
       android:label="@string/app_name"
       android:icon="@drawable/ic_launcher"
-      android:extractNativeLibs="false"
       android:supportsRtl="true">
 
     <meta-data
diff --git a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
index e1efe8c..bb049ca 100644
--- a/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -68,8 +68,7 @@
 
   <application
       android:label="@string/app_name"
-      android:icon="@drawable/ic_launcher"
-      android:extractNativeLibs="false">
+      android:icon="@drawable/ic_launcher">
 
     <meta-data
         android:name="com.android.automotive"
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
index d72c73a..59a17ab 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifest.xml
@@ -41,7 +41,6 @@
   <application
       android:label="@string/app_name"
       android:icon="@drawable/ic_launcher"
-      android:extractNativeLibs="false"
       android:supportsRtl="true">
 
     <meta-data
diff --git a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
index 25976cb..ea3b0c54 100644
--- a/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
+++ b/car/app/app-samples/showcase/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -48,8 +48,7 @@
 
   <application
       android:label="@string/app_name"
-      android:icon="@drawable/ic_launcher"
-      android:extractNativeLibs="false">
+      android:icon="@drawable/ic_launcher">
 
     <meta-data
         android:name="com.google.android.gms.car.application"
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 25638b0..5b45600 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -866,7 +866,7 @@
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class CarMessage {
     method public androidx.car.app.model.CarText getBody();
     method public long getReceivedTimeEpochMillis();
-    method public androidx.core.app.Person getSender();
+    method public androidx.core.app.Person? getSender();
     method public boolean isRead();
   }
 
@@ -876,7 +876,7 @@
     method public androidx.car.app.messaging.model.CarMessage.Builder setBody(androidx.car.app.model.CarText);
     method public androidx.car.app.messaging.model.CarMessage.Builder setRead(boolean);
     method public androidx.car.app.messaging.model.CarMessage.Builder setReceivedTimeEpochMillis(long);
-    method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person);
+    method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person?);
   }
 
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi public interface ConversationCallback {
@@ -894,6 +894,7 @@
     method public androidx.car.app.model.CarIcon? getIcon();
     method public String getId();
     method public java.util.List<androidx.car.app.messaging.model.CarMessage!> getMessages();
+    method public androidx.core.app.Person getSelf();
     method public androidx.car.app.model.CarText getTitle();
     method public boolean isGroupConversation();
   }
@@ -906,6 +907,7 @@
     method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setMessages(java.util.List<androidx.car.app.messaging.model.CarMessage!>);
+    method public androidx.car.app.messaging.model.ConversationItem.Builder setSelf(androidx.core.app.Person);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setTitle(androidx.car.app.model.CarText);
   }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
index 26429f7..8614a79 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/CarMessage.java
@@ -37,7 +37,7 @@
 @RequiresCarApi(6)
 @KeepFields
 public class CarMessage {
-    @NonNull
+    @Nullable
     private final Bundle mSender;
     @NonNull
     private final CarText mBody;
@@ -47,7 +47,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(
-                getPersonHashCode(getSender()),
+                PersonsEqualityHelper.getPersonHashCode(getSender()),
                 mBody,
                 mReceivedTimeEpochMillis,
                 mIsRead
@@ -65,53 +65,14 @@
         CarMessage otherCarMessage = (CarMessage) other;
 
         return
-                arePeopleEqual(getSender(), otherCarMessage.getSender())
+                PersonsEqualityHelper.arePersonsEqual(getSender(), otherCarMessage.getSender())
                         && Objects.equals(mBody, otherCarMessage.mBody)
                         && mReceivedTimeEpochMillis == otherCarMessage.mReceivedTimeEpochMillis
-                        && mIsRead == otherCarMessage.mIsRead
-                ;
-    }
-
-    // TODO(b/266877597): Move to androidx.core.app.Person
-    private static boolean arePeopleEqual(Person person1, Person person2) {
-        // If a unique ID was provided, use it
-        String key1 = person1.getKey();
-        String key2 = person2.getKey();
-        if (key1 != null || key2 != null) {
-            return Objects.equals(key1, key2);
-        }
-
-        // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
-        String name1 = Objects.toString(person1.getName());
-        String name2 = Objects.toString(person2.getName());
-
-        // Fallback: Compare field-by-field
-        return
-                Objects.equals(name1, name2)
-                        && Objects.equals(person1.getUri(), person2.getUri())
-                        && Objects.equals(person1.isBot(), person2.isBot())
-                        && Objects.equals(person1.isImportant(), person2.isImportant());
-    }
-
-    // TODO(b/266877597): Move to androidx.core.app.Person
-    private static int getPersonHashCode(Person person) {
-        // If a unique ID was provided, use it
-        String key = person.getKey();
-        if (key != null) {
-            return key.hashCode();
-        }
-
-        // Fallback: Use hash code for individual fields
-        return Objects.hash(
-                person.getName(),
-                person.getUri(),
-                person.isBot(),
-                person.isImportant()
-        );
+                        && mIsRead == otherCarMessage.mIsRead;
     }
 
     CarMessage(@NonNull Builder builder) {
-        this.mSender = requireNonNull(builder.mSender).toBundle();
+        this.mSender = builder.mSender == null ? null : requireNonNull(builder.mSender).toBundle();
         this.mBody = requireNonNull(builder.mBody);
         this.mReceivedTimeEpochMillis = builder.mReceivedTimeEpochMillis;
         this.mIsRead = builder.mIsRead;
@@ -119,17 +80,22 @@
 
     /** Default constructor for serialization. */
     private CarMessage() {
-        this.mSender = new Person.Builder().setName("").build().toBundle();
+        this.mSender = null;
         this.mBody = new CarText.Builder("").build();
         this.mReceivedTimeEpochMillis = 0;
         this.mIsRead = false;
     }
 
 
-    /** Returns a {@link Person} representing the message sender */
-    @NonNull
+    /**
+     * Returns a {@link Person} representing the message sender.
+     *
+     * <p> For self-sent messages, this method will return {@code null} or
+     * {@link ConversationItem#getSelf()}.
+     */
+    @Nullable
     public Person getSender() {
-        return Person.fromBundle(mSender);
+        return mSender == null ? null : Person.fromBundle(mSender);
     }
 
     /** Returns a {@link CarText} representing the message body */
@@ -158,7 +124,7 @@
         boolean mIsRead;
 
         /** Sets a {@link Person} representing the message sender */
-        public @NonNull Builder setSender(@NonNull Person sender) {
+        public @NonNull Builder setSender(@Nullable Person sender) {
             mSender = sender;
             return this;
         }
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 4c98df4..5956a59 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -21,6 +21,7 @@
 import static java.util.Objects.requireNonNull;
 
 import android.annotation.SuppressLint;
+import android.os.Bundle;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -32,6 +33,7 @@
 import androidx.car.app.model.CarText;
 import androidx.car.app.model.Item;
 import androidx.car.app.utils.CollectionUtils;
+import androidx.core.app.Person;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -47,6 +49,8 @@
     private final String mId;
     @NonNull
     private final CarText mTitle;
+    @NonNull
+    private final Bundle mSelf;
     @Nullable
     private final CarIcon mIcon;
     private final boolean mIsGroupConversation;
@@ -58,6 +62,7 @@
     @Override
     public int hashCode() {
         return Objects.hash(
+                PersonsEqualityHelper.getPersonHashCode(getSelf()),
                 mId,
                 mTitle,
                 mIcon,
@@ -80,6 +85,8 @@
                 Objects.equals(mId, otherConversationItem.mId)
                         && Objects.equals(mTitle, otherConversationItem.mTitle)
                         && Objects.equals(mIcon, otherConversationItem.mIcon)
+                        && PersonsEqualityHelper
+                            .arePersonsEqual(getSelf(), otherConversationItem.getSelf())
                         && mIsGroupConversation == otherConversationItem.mIsGroupConversation
                         && Objects.equals(mMessages, otherConversationItem.mMessages)
                 ;
@@ -88,6 +95,7 @@
     ConversationItem(@NonNull Builder builder) {
         this.mId = requireNonNull(builder.mId);
         this.mTitle = requireNonNull(builder.mTitle);
+        this.mSelf = requireNonNull(builder.mSelf).toBundle();
         this.mIcon = builder.mIcon;
         this.mIsGroupConversation = builder.mIsGroupConversation;
         this.mMessages = requireNonNull(CollectionUtils.unmodifiableCopy(builder.mMessages));
@@ -100,6 +108,7 @@
     private ConversationItem() {
         mId = "";
         mTitle = new CarText.Builder("").build();
+        mSelf = new Person.Builder().setName("").build().toBundle();
         mIcon = null;
         mIsGroupConversation = false;
         mMessages = new ArrayList<>();
@@ -133,6 +142,12 @@
         return mTitle;
     }
 
+    /** Returns a {@link Person} for the conversation */
+    @NonNull
+    public Person getSelf() {
+        return Person.fromBundle(mSelf);
+    }
+
     /** Returns a {@link CarIcon} for the conversation, or {@code null} if not set */
     @Nullable
     public CarIcon getIcon() {
@@ -167,6 +182,8 @@
         @Nullable
         CarText mTitle;
         @Nullable
+        Person mSelf;
+        @Nullable
         CarIcon mIcon;
         boolean mIsGroupConversation;
         @Nullable
@@ -204,6 +221,13 @@
             return this;
         }
 
+        /** Sets a {@link Person} for the conversation */
+        @NonNull
+        public Builder setSelf(@NonNull Person self) {
+            mSelf = self;
+            return this;
+        }
+
         /**
          * Specifies whether this conversation involves 3+ participants (a "group" conversation)
          *
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java
new file mode 100644
index 0000000..e5268d6
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/PersonsEqualityHelper.java
@@ -0,0 +1,72 @@
+/*
+ * 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.car.app.messaging.model;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.Person;
+
+import java.util.Objects;
+
+/** Helper functions to compare two {@link Person} object. */
+class PersonsEqualityHelper {
+
+    /** Calculate the hashcode for {@link Person} object. */
+    // TODO(b/266877597): Move to androidx.core.app.Person
+    public static int getPersonHashCode(@Nullable Person person) {
+        if (person == null) {
+            return 0;
+        }
+
+        // If a unique ID was provided, use it
+        String key = person.getKey();
+        if (key != null) {
+            return key.hashCode();
+        }
+
+        // Fallback: Use hash code for individual fields
+        return Objects.hash(person.getName(), person.getUri(), person.isBot(),
+                person.isImportant());
+    }
+
+    /** Compare two {@link Person} objects. */
+    // TODO(b/266877597): Move to androidx.core.app.Person
+    public static boolean arePersonsEqual(@Nullable Person person1, @Nullable Person person2) {
+        if (person1 == null && person2 == null) {
+            return true;
+        } else if (person1 == null || person2 == null) {
+            return false;
+        }
+
+        // If a unique ID was provided, use it
+        String key1 = person1.getKey();
+        String key2 = person2.getKey();
+        if (key1 != null || key2 != null) {
+            return Objects.equals(key1, key2);
+        }
+
+        // CharSequence doesn't have well-defined "equals" behavior -- convert to String instead
+        String name1 = Objects.toString(person1.getName());
+        String name2 = Objects.toString(person2.getName());
+
+        // Fallback: Compare field-by-field
+        return
+                Objects.equals(name1, name2)
+                        && Objects.equals(person1.getUri(), person2.getUri())
+                        && Objects.equals(person1.isBot(), person2.isBot())
+                        && Objects.equals(person1.isImportant(), person2.isImportant());
+    }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
index f250727..a7a2fb2 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/CarMessageTest.java
@@ -51,18 +51,6 @@
     // Ignore nullability, so we can null out a builder field
     @SuppressWarnings("ConstantConditions")
     @Test
-    public void build_throwsException_ifSenderMissing() {
-        assertThrows(
-                NullPointerException.class,
-                () -> TestConversationFactory.createMinimalMessageBuilder()
-                        .setSender(null)
-                        .build()
-        );
-    }
-
-    // Ignore nullability, so we can null out a builder field
-    @SuppressWarnings("ConstantConditions")
-    @Test
     public void build_throwsException_ifMessageBodyMissing() {
         assertThrows(
                 NullPointerException.class,
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
index 9f61ade..2090047 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/ConversationItemTest.java
@@ -110,6 +110,12 @@
                         .createFullyPopulatedConversationItemBuilder()
                         .setGroupConversation(!fullyPopulatedItem.isGroupConversation())
                         .build();
+        ConversationItem modifiedSelf =
+                TestConversationFactory
+                        .createFullyPopulatedConversationItemBuilder()
+                        .setSelf(
+                                TestConversationFactory.createFullyPopulatedPersonBuilder().build())
+                        .build();
         List<CarMessage> modifiedMessages = new ArrayList<>(1);
         modifiedMessages.add(
                 TestConversationFactory
@@ -125,6 +131,7 @@
         ConversationItem modifiedConversationCallback =
                 TestConversationFactory
                         .createFullyPopulatedConversationItemBuilder()
+                        .setSelf(TestConversationFactory.createMinimalPersonBuilder().build())
                         .setConversationCallback(new ConversationCallback() {
                             @Override
                             public void onMarkAsRead() {
@@ -144,6 +151,7 @@
         assertNotEqual(fullyPopulatedItem, modifiedIcon);
         assertNotEqual(fullyPopulatedItem, modifiedGroupStatus);
         assertNotEqual(fullyPopulatedItem, modifiedMessageList);
+        assertNotEqual(fullyPopulatedItem, modifiedSelf);
 
         // NOTE: Conversation Callback does not affect equality
         assertEqual(fullyPopulatedItem, modifiedConversationCallback);
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java
new file mode 100644
index 0000000..13b4d4f
--- /dev/null
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/PersonsEqualityHelperTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.car.app.messaging.model;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.net.Uri;
+
+import androidx.core.app.Person;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.internal.DoNotInstrument;
+
+/** Tests for {@link PersonsEqualityHelper}. */
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+public class PersonsEqualityHelperTest {
+
+    @Test
+    public void equalsAndHashCode_minimalPersons_areEqual() {
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isTrue();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+
+    @Test
+    public void equalsAndHashCode_nullPersons_areEqual() {
+        assertThat(PersonsEqualityHelper.getPersonHashCode(null)).isEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(null));
+        assertThat(PersonsEqualityHelper.arePersonsEqual(null, null)).isTrue();
+    }
+
+    @Test
+    public void equalsAndHashCode_differentName_areNotEqual() {
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().setName("Person1").build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().setName("Person2").build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+
+    @Test
+    public void equalsAndHashCode_differentKey_areNotEqual() {
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().setKey("Person1").build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().setKey("Person2").build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+
+    @Test
+    public void equalsAndHashCode_differentUri_areNotEqual() {
+        Uri uri1 =
+                Uri.parse("http://foo.com/test/sender/uri1");
+        Uri uri2 =
+                Uri.parse("http://foo.com/test/sender/uri2");
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().setUri(
+                        uri1.toString()).build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().setName(
+                        uri2.toString()).build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+
+    @Test
+    public void equalsAndHashCode_differentBot_areNotEqual() {
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().setBot(true).build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().setBot(false).build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+
+    @Test
+    public void equalsAndHashCode_differentImportant_areNotEqual() {
+        Person person1 =
+                TestConversationFactory.createMinimalPersonBuilder().setImportant(true).build();
+        Person person2 =
+                TestConversationFactory.createMinimalPersonBuilder().setImportant(false).build();
+
+        assertThat(PersonsEqualityHelper.arePersonsEqual(person1, person2)).isFalse();
+        assertThat(PersonsEqualityHelper.getPersonHashCode(person1)).isNotEqualTo(
+                PersonsEqualityHelper.getPersonHashCode(person2));
+    }
+}
diff --git a/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
index 19359bb..dc3097b 100644
--- a/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
+++ b/car/app/app/src/test/java/androidx/car/app/messaging/model/TestConversationFactory.java
@@ -98,7 +98,6 @@
      */
     public static CarMessage.Builder createMinimalMessageBuilder() {
         return new CarMessage.Builder()
-                .setSender(createMinimalPerson())
                 .setBody(CarText.create("Message body"));
     }
 
@@ -118,6 +117,7 @@
      */
     public static CarMessage.Builder createFullyPopulatedMessageBuilder() {
         return createMinimalMessageBuilder()
+                .setSender(createFullyPopulatedPerson())
                 .setRead(true)
                 .setReceivedTimeEpochMillis(12345);
     }
@@ -146,6 +146,7 @@
         return new ConversationItem.Builder()
                 .setId("conversation_id")
                 .setTitle(CarText.create("Conversation Title"))
+                .setSelf(createMinimalPerson())
                 .setMessages(messages)
                 .setConversationCallback(EMPTY_CONVERSATION_CALLBACK);
     }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
index 3bfd3f1..4aad0a8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RunComposableTests.kt
@@ -94,6 +94,264 @@
         }
     }
 
+    @Test
+    fun testExpectWithGetExpectedPropertyInDefaultValueExpression() {
+        runCompose(
+            testFunBody = """
+                ExpectComposable { value ->
+                    results["defaultValue"] = value
+                }
+                ExpectComposable({ expectedProperty + expectedProperty.reversed() }) { value ->
+                    results["anotherValue"] = value
+                }
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    expect val expectedProperty: String
+
+                    @Composable
+                    expect fun ExpectComposable(
+                        value: () -> String = { expectedProperty },
+                        content: @Composable (v: String) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    actual val expectedProperty = "actualExpectedProperty"
+
+                    @Composable
+                    actual fun ExpectComposable(
+                        value: () -> String,
+                        content: @Composable (v: String) -> Unit
+                    ) {
+                        content(value())
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("actualExpectedProperty", results["defaultValue"])
+            assertEquals(
+                "actualExpectedProperty" + "actualExpectedProperty".reversed(),
+                results["anotherValue"]
+            )
+        }
+    }
+
+    @Test
+    fun testExpectWithComposableExpressionInDefaultValue() {
+        runCompose(
+            testFunBody = """
+                ExpectComposable { value ->
+                    results["defaultValue"] = value
+                }
+                ExpectComposable("anotherValue") { value ->
+                    results["anotherValue"] = value
+                }
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    fun defaultValueComposable(): String {
+                        return "defaultValueComposable"
+                    }
+
+                    @Composable
+                    expect fun ExpectComposable(
+                        value: String = defaultValueComposable(),
+                        content: @Composable (v: String) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    actual fun ExpectComposable(
+                        value: String,
+                        content: @Composable (v: String) -> Unit
+                    ) {
+                        content(value)
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("defaultValueComposable", results["defaultValue"])
+            assertEquals("anotherValue", results["anotherValue"])
+        }
+    }
+
+    @Test
+    fun testExpectWithTypedParameter() {
+        runCompose(
+            testFunBody = """
+                ExpectComposable<String>("aeiouy") { value ->
+                    results["defaultValue"] = value
+                }
+                ExpectComposable<String>("aeiouy", { "anotherValue" }) { value ->
+                    results["anotherValue"] = value
+                }
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    expect fun <T> ExpectComposable(
+                        value: T,
+                        composeValue: @Composable () -> T = { value },
+                        content: @Composable (T) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    actual fun <T> ExpectComposable(
+                        value: T,
+                        composeValue: @Composable () -> T,
+                        content: @Composable (T) -> Unit
+                    ) {
+                        content(composeValue())
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("aeiouy", results["defaultValue"])
+            assertEquals("anotherValue", results["anotherValue"])
+        }
+    }
+
+    @Test
+    fun testExpectWithRememberInDefaultValueExpression() {
+        runCompose(
+            testFunBody = """
+                ExpectComposable { value ->
+                    results["defaultValue"] = value
+                }
+                ExpectComposable(remember { "anotherRememberedValue" }) { value ->
+                    results["anotherValue"] = value
+                }
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    expect fun ExpectComposable(
+                        value: String = remember { "rememberedDefaultValue" },
+                        content: @Composable (v: String) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    actual fun ExpectComposable(
+                        value: String,
+                        content: @Composable (v: String) -> Unit
+                    ) {
+                        content(value)
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("rememberedDefaultValue", results["defaultValue"])
+            assertEquals("anotherRememberedValue", results["anotherValue"])
+        }
+    }
+
+    @Test
+    fun testExpectWithDefaultValueUsingAnotherArgument() {
+        runCompose(
+            testFunBody = """
+                ExpectComposable("AbccbA") { value ->
+                    results["defaultValue"] = value
+                }
+                ExpectComposable("123", { s -> s + s.reversed() }) { value ->
+                    results["anotherValue"] = value
+                }
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    expect fun ExpectComposable(
+                        value: String,
+                        composeText: (String) -> String = { value },
+                        content: @Composable (v: String) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    actual fun ExpectComposable(
+                        value: String,
+                        composeText: (String) -> String,
+                        content: @Composable (v: String) -> Unit
+                    ) {
+                        content(composeText(value))
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("AbccbA", results["defaultValue"])
+            assertEquals("123321", results["anotherValue"])
+        }
+    }
+
+    @Test
+    fun testNonComposableFunWithComposableParam() {
+        runCompose(
+            testFunBody = """
+                savedContentLambda = null
+                ExpectFunWithComposableParam { value ->
+                    results["defaultValue"] = value
+                }
+                savedContentLambda!!.invoke()
+
+                savedContentLambda = null
+                ExpectFunWithComposableParam("3.14") { value ->
+                    results["anotherValue"] = value
+                }
+                savedContentLambda!!.invoke()
+            """.trimIndent(),
+            files = mapOf(
+                "Expect.kt" to """
+                    import androidx.compose.runtime.*
+
+                    var savedContentLambda: (@Composable () -> Unit)? = null
+
+                    expect fun ExpectFunWithComposableParam(
+                        value: String = "000",
+                        content: @Composable (v: String) -> Unit
+                    )
+                """.trimIndent(),
+                "Actual.kt" to """
+                    import androidx.compose.runtime.*
+
+                    @Composable
+                    actual fun ExpectFunWithComposableParam(
+                        value: String,
+                        content: @Composable (v: String) -> Unit
+                    ) {
+                        savedContentLambda = {
+                            content(value)
+                        }
+                    }
+                """.trimIndent()
+            )
+        ) { results ->
+            assertEquals("000", results["defaultValue"])
+            assertEquals("3.14", results["anotherValue"])
+        }
+    }
+
     // This method was partially borrowed/copy-pasted from RobolectricComposeTester
     // where some of the code was commented out. Those commented out parts are needed here.
     private fun runCompose(
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index bbb5435..4629463 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -108,7 +108,7 @@
             metrics
         ).lower(moduleFragment)
 
-        CopyDefaultValuesFromExpectLowering().lower(moduleFragment)
+        CopyDefaultValuesFromExpectLowering(pluginContext).lower(moduleFragment)
 
         val mangler = when {
             pluginContext.platform.isJs() -> JsManglerIr
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
index 439cfbe..1ce5f87 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/CopyDefaultValuesFromExpectLowering.kt
@@ -16,15 +16,42 @@
 
 package androidx.compose.compiler.plugins.kotlin.lower
 
+import androidx.compose.compiler.plugins.kotlin.ComposeFqNames
 import androidx.compose.compiler.plugins.kotlin.hasComposableAnnotation
-import org.jetbrains.kotlin.descriptors.FunctionDescriptor
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.descriptors.MemberDescriptor
 import org.jetbrains.kotlin.ir.IrStatement
 import org.jetbrains.kotlin.ir.ObsoleteDescriptorBasedAPI
+import org.jetbrains.kotlin.ir.declarations.IrClass
+import org.jetbrains.kotlin.ir.declarations.IrEnumEntry
 import org.jetbrains.kotlin.ir.declarations.IrFunction
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.declarations.IrProperty
+import org.jetbrains.kotlin.ir.declarations.IrTypeParameter
+import org.jetbrains.kotlin.ir.declarations.IrValueParameter
+import org.jetbrains.kotlin.ir.expressions.IrExpressionBody
+import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
+import org.jetbrains.kotlin.ir.symbols.IrClassifierSymbol
+import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol
+import org.jetbrains.kotlin.ir.symbols.IrEnumEntrySymbol
+import org.jetbrains.kotlin.ir.symbols.IrFunctionSymbol
+import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol
+import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
+import org.jetbrains.kotlin.ir.symbols.IrTypeParameterSymbol
+import org.jetbrains.kotlin.ir.symbols.IrValueParameterSymbol
+import org.jetbrains.kotlin.ir.symbols.IrValueSymbol
+import org.jetbrains.kotlin.ir.util.DeepCopyIrTreeWithSymbols
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.ir.util.DeepCopyTypeRemapper
+import org.jetbrains.kotlin.ir.util.hasAnnotation
+import org.jetbrains.kotlin.ir.util.patchDeclarationParents
+import org.jetbrains.kotlin.ir.util.referenceFunction
 import org.jetbrains.kotlin.ir.visitors.IrElementTransformerVoid
+import org.jetbrains.kotlin.ir.visitors.acceptVoid
 import org.jetbrains.kotlin.ir.visitors.transformChildrenVoid
-import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleExpectsForActual
+import org.jetbrains.kotlin.resolve.descriptorUtil.module
+import org.jetbrains.kotlin.resolve.descriptorUtil.propertyIfAccessor
+import org.jetbrains.kotlin.resolve.multiplatform.findCompatibleActualsForExpected
 
 /**
  * [ComposableFunctionBodyTransformer] relies on presence of default values in
@@ -37,55 +64,207 @@
  * This lowering needs to run before [ComposableFunctionBodyTransformer] and
  * before [ComposerParamTransformer].
  *
- * Fixes https://github.com/JetBrains/compose-jb/issues/1407
+ * Fixes:
+ * https://github.com/JetBrains/compose-jb/issues/1407
+ * https://github.com/JetBrains/compose-multiplatform/issues/2816
+ * https://github.com/JetBrains/compose-multiplatform/issues/2806
+ *
+ * This implementation is borrowed from Kotlin's ExpectToActualDefaultValueCopier.
+ * Currently, it heavily relies on descriptors to find expect for actuals or vice versa:
+ * findCompatibleActualsForExpected.
+ * Unlike ExpectToActualDefaultValueCopier, this lowering performs its transformations
+ * only for functions marked with @Composable annotation or
+ * for functions with @Composable lambdas in parameters.
+ *
+ * TODO(karpovich): When adding support for FIR we'll need to use different API.
+ * Likely: fun FirBasedSymbol<*>.getSingleCompatibleExpectForActualOrNull(): FirBasedSymbol<*>?
  */
 @OptIn(ObsoleteDescriptorBasedAPI::class)
-class CopyDefaultValuesFromExpectLowering : ModuleLoweringPass {
+class CopyDefaultValuesFromExpectLowering(
+    pluginContext: IrPluginContext
+) : ModuleLoweringPass, IrElementTransformerVoid() {
+
+    private val symbolTable = pluginContext.symbolTable
+
+    private fun isApplicable(declaration: IrFunction): Boolean {
+        return declaration.hasComposableAnnotation() ||
+            declaration.valueParameters.any {
+                it.type.hasAnnotation(ComposeFqNames.Composable)
+            }
+    }
+
+    override fun visitFunction(declaration: IrFunction): IrStatement {
+        val original = super.visitFunction(declaration) as? IrFunction ?: return declaration
+
+        if (!original.isExpect || !isApplicable(original)) {
+            return original
+        }
+
+        val actualForExpected = original.findActualForExpected()
+
+        original.valueParameters.forEachIndexed { index, expectValueParameter ->
+            val actualValueParameter = actualForExpected.valueParameters[index]
+            val expectDefaultValue = expectValueParameter.defaultValue
+            if (expectDefaultValue != null) {
+                actualValueParameter.defaultValue = expectDefaultValue
+                    .remapExpectValueSymbols()
+                    .patchDeclarationParents(actualForExpected)
+
+                // Remove a default value in the expect fun in order to prevent
+                // Kotlin expect/actual-related lowerings trying to copy the default values again
+                expectValueParameter.defaultValue = null
+            }
+        }
+        return original
+    }
 
     override fun lower(module: IrModuleFragment) {
-        // it uses FunctionDescriptor since current API (findCompatibleExpectedForActual)
-        // can return only a descriptor
-        val expectComposables = mutableMapOf<FunctionDescriptor, IrFunction>()
+        module.transformChildrenVoid(this)
+    }
 
-        // first pass to find expect functions with default values
-        module.transformChildrenVoid(object : IrElementTransformerVoid() {
-            override fun visitFunction(declaration: IrFunction): IrStatement {
-                if (declaration.isExpect && declaration.hasComposableAnnotation()) {
-                    val hasDefaultValues = declaration.valueParameters.any {
-                        it.defaultValue != null
+    private inline fun <reified T : IrFunction> T.findActualForExpected(): T =
+        symbolTable.referenceFunction(descriptor.findActualForExpect()).owner as T
+
+    private fun IrProperty.findActualForExpected(): IrProperty =
+        symbolTable.referenceProperty(descriptor.findActualForExpect()).owner
+
+    private fun IrClass.findActualForExpected(): IrClass =
+        symbolTable.referenceClass(descriptor.findActualForExpect()).owner
+
+    private fun IrEnumEntry.findActualForExpected(): IrEnumEntry =
+        symbolTable.referenceEnumEntry(descriptor.findActualForExpect()).owner
+
+    private inline fun <reified T : MemberDescriptor> T.findActualForExpect(): T {
+        if (!this.isExpect) error(this)
+        return (findCompatibleActualsForExpected(module).singleOrNull() ?: error(this)) as T
+    }
+
+    private fun IrExpressionBody.remapExpectValueSymbols(): IrExpressionBody {
+        class SymbolRemapper : DeepCopySymbolRemapper() {
+            override fun getReferencedClass(symbol: IrClassSymbol) =
+                if (symbol.descriptor.isExpect)
+                    symbol.owner.findActualForExpected().symbol
+                else super.getReferencedClass(symbol)
+
+            override fun getReferencedClassOrNull(symbol: IrClassSymbol?) =
+                symbol?.let { getReferencedClass(it) }
+
+            override fun getReferencedClassifier(symbol: IrClassifierSymbol): IrClassifierSymbol =
+                when (symbol) {
+                    is IrClassSymbol -> getReferencedClass(symbol)
+                    is IrTypeParameterSymbol -> remapExpectTypeParameter(symbol).symbol
+                    else -> error("Unexpected symbol $symbol ${symbol.descriptor}")
+                }
+
+            override fun getReferencedConstructor(symbol: IrConstructorSymbol) =
+                if (symbol.descriptor.isExpect)
+                    symbol.owner.findActualForExpected().symbol
+                else super.getReferencedConstructor(symbol)
+
+            override fun getReferencedFunction(symbol: IrFunctionSymbol): IrFunctionSymbol =
+                when (symbol) {
+                    is IrSimpleFunctionSymbol -> getReferencedSimpleFunction(symbol)
+                    is IrConstructorSymbol -> getReferencedConstructor(symbol)
+                    else -> error("Unexpected symbol $symbol ${symbol.descriptor}")
+                }
+
+            override fun getReferencedSimpleFunction(symbol: IrSimpleFunctionSymbol) = when {
+                symbol.descriptor.isExpect -> symbol.owner.findActualForExpected().symbol
+
+                symbol.descriptor.propertyIfAccessor.isExpect -> {
+                    val property = symbol.owner.correspondingPropertySymbol!!.owner
+                    val actualPropertyDescriptor = property.descriptor.findActualForExpect()
+                    val accessorDescriptor = when (symbol.owner) {
+                        property.getter -> actualPropertyDescriptor.getter!!
+                        property.setter -> actualPropertyDescriptor.setter!!
+                        else -> error("Unexpected accessor of $symbol ${symbol.descriptor}")
                     }
-                    if (hasDefaultValues) {
-                        expectComposables[declaration.descriptor] = declaration
+                    symbolTable.referenceFunction(accessorDescriptor) as IrSimpleFunctionSymbol
+                }
+
+                else -> super.getReferencedSimpleFunction(symbol)
+            }
+
+            override fun getReferencedProperty(symbol: IrPropertySymbol) =
+                if (symbol.descriptor.isExpect)
+                    symbol.owner.findActualForExpected().symbol
+                else
+                    super.getReferencedProperty(symbol)
+
+            override fun getReferencedEnumEntry(symbol: IrEnumEntrySymbol): IrEnumEntrySymbol =
+                if (symbol.descriptor.isExpect)
+                    symbol.owner.findActualForExpected().symbol
+                else
+                    super.getReferencedEnumEntry(symbol)
+
+            override fun getReferencedValue(symbol: IrValueSymbol) =
+                remapExpectValue(symbol)?.symbol ?: super.getReferencedValue(symbol)
+        }
+
+        val symbolRemapper = SymbolRemapper()
+        acceptVoid(symbolRemapper)
+
+        return transform(
+            transformer = DeepCopyIrTreeWithSymbols(
+                symbolRemapper, DeepCopyTypeRemapper(symbolRemapper)
+            ),
+            data = null
+        )
+    }
+
+    private fun remapExpectTypeParameter(symbol: IrTypeParameterSymbol): IrTypeParameter {
+        val parameter = symbol.owner
+        val parent = parameter.parent
+
+        return when (parent) {
+            is IrClass ->
+                if (!parent.descriptor.isExpect)
+                    parameter
+                else parent.findActualForExpected().typeParameters[parameter.index]
+
+            is IrFunction ->
+                if (!parent.descriptor.isExpect)
+                    parameter
+                else parent.findActualForExpected().typeParameters[parameter.index]
+
+            else -> error(parent)
+        }
+    }
+
+    private fun remapExpectValue(symbol: IrValueSymbol): IrValueParameter? {
+        if (symbol !is IrValueParameterSymbol) {
+            return null
+        }
+
+        val parameter = symbol.owner
+        val parent = parameter.parent
+
+        return when (parent) {
+            is IrClass ->
+                if (!parent.descriptor.isExpect)
+                    null
+                else {
+                    assert(parameter == parent.thisReceiver)
+                    parent.findActualForExpected().thisReceiver!!
+                }
+
+            is IrFunction ->
+                if (!parent.descriptor.isExpect)
+                    null
+                else when (parameter) {
+                    parent.dispatchReceiverParameter ->
+                        parent.findActualForExpected().dispatchReceiverParameter!!
+
+                    parent.extensionReceiverParameter ->
+                        parent.findActualForExpected().extensionReceiverParameter!!
+
+                    else -> {
+                        assert(parent.valueParameters[parameter.index] == parameter)
+                        parent.findActualForExpected().valueParameters[parameter.index]
                     }
                 }
-                return super.visitFunction(declaration)
-            }
-        })
 
-        // second pass to set corresponding default values
-        module.transformChildrenVoid(object : IrElementTransformerVoid() {
-            override fun visitFunction(declaration: IrFunction): IrStatement {
-                if (declaration.descriptor.isActual && declaration.hasComposableAnnotation()) {
-                    val compatibleExpects = declaration.descriptor.findCompatibleExpectsForActual {
-                        module.descriptor == it
-                    }
-                    if (compatibleExpects.isNotEmpty()) {
-                        val expectFun = compatibleExpects.firstOrNull {
-                            it in expectComposables
-                        }?.let {
-                            expectComposables[it]
-                        }
-
-                        if (expectFun != null) {
-                            declaration.valueParameters.forEachIndexed { index, it ->
-                                it.defaultValue =
-                                    it.defaultValue ?: expectFun.valueParameters[index].defaultValue
-                            }
-                        }
-                    }
-                }
-                return super.visitFunction(declaration)
-            }
-        })
+            else -> error(parent)
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
index e353496..3f36fd9 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
@@ -76,9 +76,9 @@
             text = buildAnnotatedString {
                 append("This word is a link: ")
                 withAnnotation(UrlAnnotation("https://google.com")) {
-                    append("Google\n")
+                    append("Google")
                 }
-                append("This word is not a link: google.com")
+                append("\nThis word is not a link: google.com")
             },
             style = TextStyle(fontSize = fontSize8)
         )
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
index b0f117c..0443df8 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/TransformableSample.kt
@@ -92,11 +92,11 @@
                 "\uD83C\uDF55",
                 fontSize = 32.sp,
                 // apply other transformations like rotation and zoom on the pizza slice emoji
-                modifier = Modifier.graphicsLayer(
-                    scaleX = scale,
-                    scaleY = scale,
+                modifier = Modifier.graphicsLayer {
+                    scaleX = scale
+                    scaleY = scale
                     rotationZ = rotation
-                )
+                }
             )
         }
     }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
new file mode 100644
index 0000000..f7b269f
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtilsKtTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.modifiers
+
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class LayoutUtilsKtTest {
+
+    @Test
+    fun finalConstraints_returnsTightWidth() {
+        val subject = finalConstraints(
+            Constraints(500, 500, 0, 50),
+            true,
+            TextOverflow.Ellipsis,
+            42f
+        )
+        assertThat(subject.maxWidth).isEqualTo(500)
+    }
+
+    @Test
+    fun finalConstraints_returnsMaxIntrinsicWhenUnbound() {
+        val subject = finalConstraints(
+            Constraints(500, 500, 0, 50),
+            false,
+            TextOverflow.Clip,
+            1234.1f
+        )
+        assertThat(subject.maxWidth).isEqualTo(1235)
+    }
+
+    @Test
+    fun finalMaxWidth_returnsTightWidth() {
+        val subject = finalMaxWidth(
+            Constraints(500, 500, 0, 50),
+            true,
+            TextOverflow.Ellipsis,
+            42f
+        )
+        assertThat(subject).isEqualTo(500)
+    }
+
+    @Test
+    fun finalMaxWidth_returnsMaxIntrinsicWhenUnbound() {
+        val subject = finalMaxWidth(
+            Constraints(500, 500, 0, 50),
+            false,
+            TextOverflow.Clip,
+            1234.1f
+        )
+        assertThat(subject).isEqualTo(1235)
+    }
+
+    @Test
+    fun finalMaxLines_negative() {
+        val subject = finalMaxLines(true, TextOverflow.Clip, -1)
+        assertThat(subject).isEqualTo(1)
+    }
+
+    @Test
+    fun finalMaxLines_positive_noOverride() {
+        val subject = finalMaxLines(true, TextOverflow.Clip, 4)
+        assertThat(subject).isEqualTo(4)
+    }
+
+    @Test
+    fun finalMaxLines_overrideOn_TextOverflowEllipsis_andSoftwrapFalse() {
+        val subject = finalMaxLines(false, TextOverflow.Ellipsis, 4)
+        assertThat(subject).isEqualTo(1)
+    }
+
+    @Test
+    fun canChangeBreak_canWrap_false() {
+        val subject = canChangeBreaks(
+            canWrap = false,
+            newConstraints = Constraints(0),
+            oldConstraints = Constraints(0),
+            maxIntrinsicWidth = 42f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isFalse()
+    }
+
+    @Test
+    fun canChangeBreak_sameWidth() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(50),
+            oldConstraints = Constraints.fixedWidth(50),
+            maxIntrinsicWidth = 1234f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isFalse()
+    }
+
+    @Test
+    fun canChangeBreak_textSmallerThanConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(50),
+            oldConstraints = Constraints.fixedWidth(40),
+            maxIntrinsicWidth = 12f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isFalse()
+    }
+
+    @Test
+    fun canChangeBreak_textBiggerThanConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(100),
+            oldConstraints = Constraints.fixedWidth(200),
+            maxIntrinsicWidth = 300f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isTrue()
+    }
+
+    @Test
+    fun canChangeBreak_shrinking_textSmallerThanNewConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(50),
+            oldConstraints = Constraints.fixedWidth(60),
+            maxIntrinsicWidth = 45f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isFalse()
+    }
+
+    @Test
+    fun canChangeBreak_shrinking_textBiggerThanNewConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(50),
+            oldConstraints = Constraints.fixedWidth(60),
+            maxIntrinsicWidth = 59f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isTrue()
+    }
+
+    @Test
+    fun canChangeBreak_growing_textSmallerThanNewConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(60),
+            oldConstraints = Constraints.fixedWidth(50),
+            maxIntrinsicWidth = 45f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isFalse()
+    }
+
+    @Test
+    fun canChangeBreak_growing_textBiggerThanNewConstraints() {
+        val subject = canChangeBreaks(
+            canWrap = true,
+            newConstraints = Constraints.fixedWidth(60),
+            oldConstraints = Constraints.fixedWidth(50),
+            maxIntrinsicWidth = 59f,
+            softWrap = true,
+            overflow = TextOverflow.Ellipsis
+        )
+        assertThat(subject).isTrue()
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
index 19a823d..4da7839 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.android.kt
@@ -17,32 +17,21 @@
 package androidx.compose.foundation.relocation
 
 import android.graphics.Rect as AndroidRect
-import android.view.View
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.remember
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.platform.LocalView
 
-@Composable
-internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
-    val view = LocalView.current
-    return remember(view) { AndroidBringIntoViewParent(view) }
-}
-
-/**
- * A [BringIntoViewParent] that delegates to the [View] hosting the composition.
- */
-private class AndroidBringIntoViewParent(private val view: View) : BringIntoViewParent {
-    override suspend fun bringChildIntoView(
-        childCoordinates: LayoutCoordinates,
-        boundsProvider: () -> Rect?
-    ) {
+internal actual fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+    BringIntoViewParent =
+    BringIntoViewParent { childCoordinates, boundsProvider ->
+        val view = currentValueOf(LocalView)
         val childOffset = childCoordinates.positionInRoot()
-        val rootRect = boundsProvider()?.translate(childOffset) ?: return
-        view.requestRectangleOnScreen(rootRect.toRect(), false)
+        val rootRect = boundsProvider()?.translate(childOffset)
+        if (rootRect != null) {
+            view.requestRectangleOnScreen(rootRect.toRect(), false)
+        }
     }
-}
 
 private fun Rect.toRect() = AndroidRect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
index 7e5402e..4c532f3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoView.kt
@@ -15,28 +15,25 @@
  */
 package androidx.compose.foundation.relocation
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.OnPlacedModifier
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ModifierLocalNode
 import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
+import androidx.compose.ui.node.LayoutAwareModifierNode
 
 /**
  * The Key for the ModifierLocal that can be used to access the [BringIntoViewParent].
  */
-@OptIn(ExperimentalFoundationApi::class)
 internal val ModifierLocalBringIntoViewParent = modifierLocalOf<BringIntoViewParent?> { null }
 
 /**
  * Platform-specific "root" of the [BringIntoViewParent] chain to call into when there are no
- * [ModifierLocalBringIntoViewParent]s above a [BringIntoViewChildModifier]. The value returned by
- * this function should be passed to the [BringIntoViewChildModifier] constructor.
+ * [ModifierLocalBringIntoViewParent]s above a [BringIntoViewChildNode].
  */
-@Composable
-internal expect fun rememberDefaultBringIntoViewParent(): BringIntoViewParent
+internal expect fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+    BringIntoViewParent
 
 /**
  * A node that can respond to [bringChildIntoView] requests from its children by scrolling its
@@ -67,16 +64,15 @@
  * [BringIntoViewParent]: either one read from the [ModifierLocalBringIntoViewParent], or if no
  * modifier local is specified then the [defaultParent].
  *
- * @param defaultParent The [BringIntoViewParent] to use if there is no
+ * @property defaultParent The [BringIntoViewParent] to use if there is no
  * [ModifierLocalBringIntoViewParent] available to read. This parent should always be obtained by
- * calling [rememberDefaultBringIntoViewParent] to support platform-specific integration.
+ * calling [defaultBringIntoViewParent] to support platform-specific integration.
  */
-internal abstract class BringIntoViewChildModifier(
-    private val defaultParent: BringIntoViewParent
-) : ModifierLocalConsumer,
-    OnPlacedModifier {
+internal abstract class BringIntoViewChildNode : Modifier.Node(),
+    ModifierLocalNode, LayoutAwareModifierNode, CompositionLocalConsumerModifierNode {
+    private val defaultParent = defaultBringIntoViewParent()
 
-    private var localParent: BringIntoViewParent? = null
+    private val localParent: BringIntoViewParent? get() = ModifierLocalBringIntoViewParent.current
 
     /** The [LayoutCoordinates] of this modifier, if attached. */
     protected var layoutCoordinates: LayoutCoordinates? = null
@@ -86,12 +82,6 @@
     protected val parent: BringIntoViewParent
         get() = localParent ?: defaultParent
 
-    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
-        with(scope) {
-            localParent = ModifierLocalBringIntoViewParent.current
-        }
-    }
-
     override fun onPlaced(coordinates: LayoutCoordinates) {
         layoutCoordinates = coordinates
     }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
index 0636f8c..290860c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewRequester.kt
@@ -17,14 +17,12 @@
 package androidx.compose.foundation.relocation
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.toRect
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.unit.toSize
 
 /**
@@ -97,29 +95,15 @@
  *     [bringIntoView][BringIntoViewRequester.bringIntoView] requests to parents
  *     of the current composable.
  */
+@Suppress("ModifierInspectorInfo")
 @ExperimentalFoundationApi
 fun Modifier.bringIntoViewRequester(
     bringIntoViewRequester: BringIntoViewRequester
-): Modifier = composed(debugInspectorInfo {
-    name = "bringIntoViewRequester"
-    properties["bringIntoViewRequester"] = bringIntoViewRequester
-}) {
-    val defaultResponder = rememberDefaultBringIntoViewParent()
-    val modifier = remember(defaultResponder) {
-        BringIntoViewRequesterModifier(defaultResponder)
-    }
-    if (bringIntoViewRequester is BringIntoViewRequesterImpl) {
-        DisposableEffect(bringIntoViewRequester) {
-            bringIntoViewRequester.modifiers += modifier
-            onDispose { bringIntoViewRequester.modifiers -= modifier }
-        }
-    }
-    return@composed modifier
-}
+): Modifier = this.then(BringIntoViewRequesterElement(bringIntoViewRequester))
 
 @ExperimentalFoundationApi
 private class BringIntoViewRequesterImpl : BringIntoViewRequester {
-    val modifiers = mutableVectorOf<BringIntoViewRequesterModifier>()
+    val modifiers = mutableVectorOf<BringIntoViewRequesterNode>()
 
     override suspend fun bringIntoView(rect: Rect?) {
         modifiers.forEach {
@@ -128,15 +112,64 @@
     }
 }
 
+@ExperimentalFoundationApi
+private class BringIntoViewRequesterElement(
+    private val requester: BringIntoViewRequester
+) : ModifierNodeElement<BringIntoViewRequesterNode>() {
+    override fun create(): BringIntoViewRequesterNode {
+        return BringIntoViewRequesterNode(requester)
+    }
+
+    override fun update(node: BringIntoViewRequesterNode): BringIntoViewRequesterNode =
+        node.also {
+            it.updateRequester(requester)
+        }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "bringIntoViewRequester"
+        properties["bringIntoViewRequester"] = requester
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return (this === other) ||
+            (other is BringIntoViewRequesterElement) && (requester == other.requester)
+    }
+
+    override fun hashCode(): Int {
+        return requester.hashCode()
+    }
+}
+
 /**
  * A modifier that holds state and modifier implementations for [bringIntoViewRequester]. It has
- * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier], and uses that parent
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildNode], and uses that parent
  * to respond to requests to [bringIntoView].
  */
 @ExperimentalFoundationApi
-private class BringIntoViewRequesterModifier(
-    defaultParent: BringIntoViewParent
-) : BringIntoViewChildModifier(defaultParent) {
+internal class BringIntoViewRequesterNode(
+    private var requester: BringIntoViewRequester
+) : BringIntoViewChildNode() {
+    init {
+        updateRequester(requester)
+    }
+
+    fun updateRequester(requester: BringIntoViewRequester) {
+        disposeRequester()
+        if (requester is BringIntoViewRequesterImpl) {
+            requester.modifiers += this
+        }
+        this.requester = requester
+    }
+
+    private fun disposeRequester() {
+        if (requester is BringIntoViewRequesterImpl) {
+            (requester as BringIntoViewRequesterImpl).modifiers -= this
+        }
+    }
+
+    override fun onDetach() {
+        disposeRequester()
+    }
 
     /**
      * Requests that [rect] (if non-null) or the entire bounds of this modifier's node (if [rect]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
index 3d95cca..9e31be2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.kt
@@ -17,14 +17,12 @@
 package androidx.compose.foundation.relocation
 
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.ProvidableModifierLocal
-import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.modifier.modifierLocalMapOf
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
@@ -96,40 +94,48 @@
  *
  * @see BringIntoViewRequester
  */
+@Suppress("ModifierInspectorInfo")
 @ExperimentalFoundationApi
 fun Modifier.bringIntoViewResponder(
     responder: BringIntoViewResponder
-): Modifier = composed(debugInspectorInfo {
-    name = "bringIntoViewResponder"
-    properties["responder"] = responder
-}) {
-    val defaultParent = rememberDefaultBringIntoViewParent()
-    val modifier = remember(defaultParent) {
-        BringIntoViewResponderModifier(defaultParent)
+): Modifier = this.then(BringIntoViewResponderElement(responder))
+
+@ExperimentalFoundationApi
+private class BringIntoViewResponderElement(
+    private val responder: BringIntoViewResponder
+) : ModifierNodeElement<BringIntoViewResponderNode>() {
+    override fun create(): BringIntoViewResponderNode = BringIntoViewResponderNode(responder)
+
+    override fun update(node: BringIntoViewResponderNode) = node.also {
+        it.responder = responder
     }
-    modifier.responder = responder
-    return@composed modifier
+    override fun equals(other: Any?): Boolean {
+        return (this === other) ||
+            (other is BringIntoViewResponderElement) && (responder == other.responder)
+    }
+
+    override fun hashCode(): Int {
+        return responder.hashCode()
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        name = "bringIntoViewResponder"
+        properties["responder"] = responder
+    }
 }
 
 /**
  * A modifier that holds state and modifier implementations for [bringIntoViewResponder]. It has
- * access to the next [BringIntoViewParent] via [BringIntoViewChildModifier] and additionally
+ * access to the next [BringIntoViewParent] via [BringIntoViewChildNode] and additionally
  * provides itself as the [BringIntoViewParent] for subsequent modifiers. This class is responsible
  * for recursively propagating requests up the responder chain.
  */
 @OptIn(ExperimentalFoundationApi::class)
-private class BringIntoViewResponderModifier(
-    defaultParent: BringIntoViewParent
-) : BringIntoViewChildModifier(defaultParent),
-    ModifierLocalProvider<BringIntoViewParent?>,
-    BringIntoViewParent {
+private class BringIntoViewResponderNode(
+    var responder: BringIntoViewResponder
+) : BringIntoViewChildNode(), BringIntoViewParent {
 
-    lateinit var responder: BringIntoViewResponder
-
-    override val key: ProvidableModifierLocal<BringIntoViewParent?>
-        get() = ModifierLocalBringIntoViewParent
-    override val value: BringIntoViewParent
-        get() = this
+    override val providedValues = modifierLocalMapOf(ModifierLocalBringIntoViewParent to this)
 
     /**
      * Responds to a child's request by first converting [boundsProvider] into this node's [LayoutCoordinates]
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
new file mode 100644
index 0000000..bb111a3
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/LayoutUtils.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.text.modifiers
+
+import androidx.compose.foundation.text.ceilToIntPx
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Constraints
+
+/**
+ * Find the constraints to pass to Paragraph based on all the parameters.
+ */
+internal fun finalConstraints(
+    constraints: Constraints,
+    softWrap: Boolean,
+    overflow: TextOverflow,
+    maxIntrinsicWidth: Float
+): Constraints = Constraints(
+        maxWidth = finalMaxWidth(constraints, softWrap, overflow, maxIntrinsicWidth),
+        maxHeight = constraints.maxHeight
+    )
+
+/**
+ * Find the final max width a Paragraph would use based on all parameters.
+ */
+internal fun finalMaxWidth(
+    constraints: Constraints,
+    softWrap: Boolean,
+    overflow: TextOverflow,
+    maxIntrinsicWidth: Float
+): Int {
+    val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
+    val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
+        constraints.maxWidth
+    } else {
+        Constraints.Infinity
+    }
+
+    // if minWidth == maxWidth the width is fixed.
+    //    therefore we can pass that value to our paragraph and use it
+    // if minWidth != maxWidth there is a range
+    //    then we should check if the max intrinsic width is in this range to decide the
+    //    width to be passed to Paragraph
+    //        if max intrinsic width is between minWidth and maxWidth
+    //           we can use it to layout
+    //        else if max intrinsic width is greater than maxWidth, we can only use maxWidth
+    //        else if max intrinsic width is less than minWidth, we should use minWidth
+    return if (constraints.minWidth == maxWidth) {
+        maxWidth
+    } else {
+        maxIntrinsicWidth.ceilToIntPx().coerceIn(constraints.minWidth, maxWidth)
+    }
+}
+
+/**
+ * Find the maxLines to pass to text layout based on all parameters
+ */
+internal fun finalMaxLines(softWrap: Boolean, overflow: TextOverflow, maxLinesIn: Int): Int {
+    // This is a fallback behavior because native text layout doesn't support multiple
+    // ellipsis in one text layout.
+    // When softWrap is turned off and overflow is ellipsis, it's expected that each line
+    // that exceeds maxWidth will be ellipsized.
+    // For example,
+    // input text:
+    //     "AAAA\nAAAA"
+    // maxWidth:
+    //     3 * fontSize that only allow 3 characters to be displayed each line.
+    // expected output:
+    //     AA…
+    //     AA…
+    // Here we assume there won't be any '\n' character when softWrap is false. And make
+    // maxLines 1 to implement the similar behavior.
+    val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
+    return if (overwriteMaxLines) 1 else maxLinesIn.coerceAtLeast(1)
+}
+
+/**
+ * Assuming we're laying out the same text in two different constraints, see if breaks could change
+ *
+ * If text or other text-layout attributes change, this method will not return accurate results.
+ */
+internal fun canChangeBreaks(
+    canWrap: Boolean,
+    newConstraints: Constraints,
+    oldConstraints: Constraints,
+    maxIntrinsicWidth: Float,
+    softWrap: Boolean,
+    overflow: TextOverflow,
+): Boolean {
+    // no breaks
+    if (!canWrap) return false
+    // we can assume maxIntrinsicWidth is the same, or other invalidate would have happened
+    // earlier (resetting para, etc)
+    val prevMaxWidth = finalMaxWidth(oldConstraints, softWrap, overflow, maxIntrinsicWidth)
+    val newMaxWidth = finalMaxWidth(newConstraints, softWrap, overflow, maxIntrinsicWidth)
+
+    if (prevMaxWidth != newMaxWidth) {
+        if (prevMaxWidth >= maxIntrinsicWidth && newMaxWidth >= maxIntrinsicWidth) {
+            // nothing can change, layout >= text width.
+            return false
+        }
+
+        return if (prevMaxWidth < newMaxWidth) {
+            // we're growing, so return if we could have broken last time
+            prevMaxWidth < maxIntrinsicWidth
+        } else {
+            // we're shrinking, so return if we could have broken this time
+            newMaxWidth < maxIntrinsicWidth
+        }
+    }
+
+    // widths haven't changed, layouts will be same
+    return false
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
index 53b531f..fc4e604 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/MultiParagraphLayoutCache.kt
@@ -27,7 +27,6 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.resolveDefaults
-import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -64,11 +63,17 @@
     internal var density: Density? = null
         set(value) {
             val localField = field
-            if (value == null || localField == null) {
+            if (localField == null) {
                 field = value
                 return
             }
 
+            if (value == null) {
+                field = value
+                markDirty()
+                return
+            }
+
             if (localField.density != value.density || localField.fontScale != value.fontScale) {
                 field = value
                 // none of our results are correct if density changed
@@ -92,12 +97,14 @@
     private var layoutCache: TextLayoutResult? = null
 
     /**
-     * Last intrinsic height computation
-     *
-     * - first = input width
-     * - second = output height
+     * Input width for the last call to [intrinsicHeight]
      */
-    private var cachedIntrinsicHeight: Pair<Int, Int>? = null
+    private var cachedIntrinsicHeightInputWidth: Int = -1
+
+    /**
+     * Output height for last call to [intrinsicHeight] at [cachedIntrinsicHeightInputWidth]
+     */
+    private var cachedIntrinsicHeight: Int = -1
 
     /**
      * The last computed TextLayoutResult, or throws if not initialized.
@@ -121,9 +128,6 @@
         constraints: Constraints,
         layoutDirection: LayoutDirection
     ): Boolean {
-        if (!layoutCache.newLayoutWillBeDifferent(constraints, layoutDirection)) {
-            return false
-        }
         val finalConstraints = if (maxLines != Int.MAX_VALUE || minLines > 1) {
             val localMinMax = MinMaxLinesCoercer.from(
                 minMaxLinesCoercer,
@@ -142,16 +146,28 @@
         } else {
             constraints
         }
+        if (!layoutCache.newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+            if (finalConstraints == layoutCache!!.layoutInput.constraints) return false
+            // we need to regen the input, constraints aren't the same
+            layoutCache = textLayoutResult(
+                layoutDirection = layoutDirection,
+                finalConstraints = finalConstraints,
+                multiParagraph = layoutCache!!.multiParagraph
+            )
+            return true
+        }
         val multiParagraph = layoutText(finalConstraints, layoutDirection)
 
-        val size = finalConstraints.constrain(
-            IntSize(
-                multiParagraph.width.ceilToIntPx(),
-                multiParagraph.height.ceilToIntPx()
-            )
-        )
+        layoutCache = textLayoutResult(layoutDirection, finalConstraints, multiParagraph)
+        return true
+    }
 
-        layoutCache = TextLayoutResult(
+    private fun textLayoutResult(
+        layoutDirection: LayoutDirection,
+        finalConstraints: Constraints,
+        multiParagraph: MultiParagraph
+    ): TextLayoutResult {
+        return TextLayoutResult(
             TextLayoutInput(
                 text,
                 style,
@@ -165,24 +181,29 @@
                 finalConstraints
             ),
             multiParagraph,
-            size
+            finalConstraints.constrain(
+                IntSize(
+                    multiParagraph.width.ceilToIntPx(),
+                    multiParagraph.height.ceilToIntPx()
+                )
+            )
         )
-        return true
     }
 
     /**
      * The natural height of text at [width] in [layoutDirection]
      */
     fun intrinsicHeight(width: Int, layoutDirection: LayoutDirection): Int {
-        cachedIntrinsicHeight?.let { (prevWidth, prevHeight) ->
-            if (width == prevWidth) return prevHeight
-        }
+        val localWidth = cachedIntrinsicHeightInputWidth
+        val localHeght = cachedIntrinsicHeight
+        if (width == localWidth && localWidth != -1) return localHeght
         val result = layoutText(
             Constraints(0, width, 0, Constraints.Infinity),
             layoutDirection
         ).height.ceilToIntPx()
 
-        cachedIntrinsicHeight = width to result
+        cachedIntrinsicHeightInputWidth = width
+        cachedIntrinsicHeight = result
         return result
     }
 
@@ -250,51 +271,16 @@
     ): MultiParagraph {
         val localParagraphIntrinsics = setLayoutDirection(layoutDirection)
 
-        val minWidth = constraints.minWidth
-        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
-        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
-            constraints.maxWidth
-        } else {
-            Constraints.Infinity
-        }
-
-        // This is a fallback behavior because native text layout doesn't support multiple
-        // ellipsis in one text layout.
-        // When softWrap is turned off and overflow is ellipsis, it's expected that each line
-        // that exceeds maxWidth will be ellipsized.
-        // For example,
-        // input text:
-        //     "AAAA\nAAAA"
-        // maxWidth:
-        //     3 * fontSize that only allow 3 characters to be displayed each line.
-        // expected output:
-        //     AA…
-        //     AA…
-        // Here we assume there won't be any '\n' character when softWrap is false. And make
-        // maxLines 1 to implement the similar behavior.
-        val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
-        val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
-
-        // if minWidth == maxWidth the width is fixed.
-        //    therefore we can pass that value to our paragraph and use it
-        // if minWidth != maxWidth there is a range
-        //    then we should check if the max intrinsic width is in this range to decide the
-        //    width to be passed to Paragraph
-        //        if max intrinsic width is between minWidth and maxWidth
-        //           we can use it to layout
-        //        else if max intrinsic width is greater than maxWidth, we can only use maxWidth
-        //        else if max intrinsic width is less than minWidth, we should use minWidth
-        val width = if (minWidth == maxWidth) {
-            maxWidth
-        } else {
-            localParagraphIntrinsics.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
-        }
-
         return MultiParagraph(
             intrinsics = localParagraphIntrinsics,
-            constraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight),
+            constraints = finalConstraints(
+                constraints,
+                softWrap,
+                overflow,
+                localParagraphIntrinsics.maxIntrinsicWidth
+            ),
             // This is a fallback behavior for ellipsis. Native
-            maxLines = finalMaxLines,
+            maxLines = finalMaxLines(softWrap, overflow, maxLines),
             ellipsis = overflow == TextOverflow.Ellipsis
         )
     }
@@ -319,36 +305,15 @@
         // if we were passed identical constraints just skip more work
         if (constraints == layoutInput.constraints) return false
 
-        // only be clever if we can predict line break behavior exactly, which is only possible with
-        // simple geometry math for the greedy layout case
-        if (style.lineBreak != LineBreak.Simple) {
-            return true
-        }
-
-        // see if width would produce the same wraps (greedy wraps only)
-        val canWrap = softWrap && maxLines > 1
-        if (canWrap && size.width != multiParagraph.maxIntrinsicWidth.ceilToIntPx()) {
-            // some soft wrapping happened, check to see if we're between the previous measure and
-            // the next wrap
-            val prevActualMaxWidth = maxWidth(layoutInput.constraints)
-            val newMaxWidth = maxWidth(constraints)
-            if (newMaxWidth > prevActualMaxWidth) {
-                // we've grown the potential layout area, and may break longer lines
-                return true
-            }
-            if (newMaxWidth <= size.width) {
-                // it's possible to shrink this text (possible opt: check minIntrinsicWidth
-                return true
-            }
-        }
-
-        // check any constraint width changes for single line text
-        if (!canWrap &&
-            (constraints.maxWidth != layoutInput.constraints.maxWidth ||
-                (constraints.minWidth != layoutInput.constraints.minWidth))) {
-            // no soft wrap and width is different, always invalidate
-            return true
-        }
+        // see if width would produce the same wraps
+        if (canChangeBreaks(
+                canWrap = softWrap && maxLines > 1,
+                newConstraints = constraints,
+                oldConstraints = layoutInput.constraints,
+                maxIntrinsicWidth = this.multiParagraph.intrinsics.maxIntrinsicWidth,
+                softWrap = softWrap,
+                overflow = overflow
+            )) return true
 
         // if we get here width won't change, height may be clipped
         if (constraints.maxHeight < multiParagraph.height) {
@@ -365,20 +330,12 @@
      *
      * Falls back to [paragraphIntrinsics.maxIntrinsicWidth] when not exact constraints.
      */
-    private fun maxWidth(constraints: Constraints): Int {
-        val minWidth = constraints.minWidth
-        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
-        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
-            constraints.maxWidth
-        } else {
-            Constraints.Infinity
-        }
-        return if (minWidth == maxWidth) {
-            maxWidth
-        } else {
-            paragraphIntrinsics!!.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
-        }
-    }
+    private fun maxWidth(constraints: Constraints): Int = finalMaxWidth(
+            constraints,
+            softWrap,
+            overflow,
+            paragraphIntrinsics!!.maxIntrinsicWidth
+        )
 
     private fun markDirty() {
         paragraphIntrinsics = null
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
index dadc584..b724079 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/modifiers/ParagraphLayoutCache.kt
@@ -28,7 +28,6 @@
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.resolveDefaults
-import androidx.compose.ui.text.style.LineBreak
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -158,6 +157,20 @@
             constraints
         }
         if (!newLayoutWillBeDifferent(finalConstraints, layoutDirection)) {
+            if (finalConstraints != prevConstraints) {
+                // ensure size and overflow is still accurate
+                val localParagraph = paragraph!!
+                val localSize = finalConstraints.constrain(
+                    IntSize(
+                        localParagraph.width.ceilToIntPx(),
+                        localParagraph.height.ceilToIntPx()
+                    )
+                )
+                layoutSize = localSize
+                didOverflow = overflow != TextOverflow.Visible &&
+                    (localSize.width < localParagraph.width ||
+                        localSize.height < localParagraph.height)
+            }
             return false
         }
         paragraph = layoutText(finalConstraints, layoutDirection).also {
@@ -252,52 +265,16 @@
     ): Paragraph {
         val localParagraphIntrinsics = setLayoutDirection(layoutDirection)
 
-        val minWidth = constraints.minWidth
-        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
-        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
-            constraints.maxWidth
-        } else {
-            Constraints.Infinity
-        }
-
-        // This is a fallback behavior because native text layout doesn't support multiple
-        // ellipsis in one text layout.
-        // When softWrap is turned off and overflow is ellipsis, it's expected that each line
-        // that exceeds maxWidth will be ellipsized.
-        // For example,
-        // input text:
-        //     "AAAA\nAAAA"
-        // maxWidth:
-        //     3 * fontSize that only allow 3 characters to be displayed each line.
-        // expected output:
-        //     AA…
-        //     AA…
-        // Here we assume there won't be any '\n' character when softWrap is false. And make
-        // maxLines 1 to implement the similar behavior.
-        val overwriteMaxLines = !softWrap && overflow == TextOverflow.Ellipsis
-        val finalMaxLines = if (overwriteMaxLines) 1 else maxLines.coerceAtLeast(1)
-
-        // if minWidth == maxWidth the width is fixed.
-        //    therefore we can pass that value to our paragraph and use it
-        // if minWidth != maxWidth there is a range
-        //    then we should check if the max intrinsic width is in this range to decide the
-        //    width to be passed to Paragraph
-        //        if max intrinsic width is between minWidth and maxWidth
-        //           we can use it to layout
-        //        else if max intrinsic width is greater than maxWidth, we can only use maxWidth
-        //        else if max intrinsic width is less than minWidth, we should use minWidth
-        val width = if (minWidth == maxWidth) {
-            maxWidth
-        } else {
-            localParagraphIntrinsics.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
-        }
-
-        val finalConstraints = Constraints(maxWidth = width, maxHeight = constraints.maxHeight)
         return Paragraph(
-            paragraphIntrinsics = paragraphIntrinsics!!,
-            constraints = finalConstraints,
+            paragraphIntrinsics = localParagraphIntrinsics,
+            constraints = finalConstraints(
+                constraints,
+                softWrap,
+                overflow,
+                localParagraphIntrinsics.maxIntrinsicWidth
+            ),
             // This is a fallback behavior for ellipsis. Native
-            maxLines = finalMaxLines,
+            maxLines = finalMaxLines(softWrap, overflow, maxLines),
             ellipsis = overflow == TextOverflow.Ellipsis
         )
     }
@@ -310,6 +287,7 @@
         constraints: Constraints,
         layoutDirection: LayoutDirection
     ): Boolean {
+        // paragarph and paragraphIntrinsics are from previous run
         val localParagraph = paragraph ?: return true
         val localParagraphIntrinsics = paragraphIntrinsics ?: return true
         // no layout yet
@@ -323,28 +301,15 @@
         // if we were passed identical constraints just skip more work
         if (constraints == prevConstraints) return false
 
-        // only be clever if we can predict line break behavior exactly, which is only possible with
-        // simple geometry math for the greedy layout case
-        if (style.lineBreak != LineBreak.Simple) {
-            return true
-        }
-
-        // see if width would produce the same wraps (greedy wraps only)
-        val canWrap = softWrap && maxLines > 1
-        if (canWrap && layoutSize.width != localParagraph.maxIntrinsicWidth.ceilToIntPx()) {
-            // some soft wrapping happened, check to see if we're between the previous measure and
-            // the next wrap
-            val prevActualMaxWidth = maxWidth(prevConstraints)
-            val newMaxWidth = maxWidth(constraints)
-            if (newMaxWidth > prevActualMaxWidth) {
-                // we've grown the potential layout area, and may break longer lines
-                return true
-            }
-            if (newMaxWidth <= layoutSize.width) {
-                // it's possible to shrink this text (possible opt: check minIntrinsicWidth
-                return true
-            }
-        }
+        // see if width would produce the same wraps
+        if (canChangeBreaks(
+                canWrap = softWrap && maxLines > 1,
+                newConstraints = constraints,
+                oldConstraints = prevConstraints,
+                maxIntrinsicWidth = localParagraphIntrinsics.maxIntrinsicWidth,
+                softWrap = softWrap,
+                overflow = overflow
+            )) return true
 
         // if we get here width won't change, height may be clipped
         if (constraints.maxHeight < localParagraph.height) {
@@ -356,26 +321,6 @@
         return false
     }
 
-    /**
-     * Compute the maxWidth for text layout from [Constraints]
-     *
-     * Falls back to [paragraphIntrinsics.maxIntrinsicWidth] when not exact constraints.
-     */
-    private fun maxWidth(constraints: Constraints): Int {
-        val minWidth = constraints.minWidth
-        val widthMatters = softWrap || overflow == TextOverflow.Ellipsis
-        val maxWidth = if (widthMatters && constraints.hasBoundedWidth) {
-            constraints.maxWidth
-        } else {
-            Constraints.Infinity
-        }
-        return if (minWidth == maxWidth) {
-            maxWidth
-        } else {
-            paragraphIntrinsics!!.maxIntrinsicWidth.ceilToIntPx().coerceIn(minWidth, maxWidth)
-        }
-    }
-
     private fun markDirty() {
         paragraph = null
         paragraphIntrinsics = null
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
index ea02955..736956c 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/relocation/BringIntoViewResponder.desktop.kt
@@ -16,17 +16,14 @@
 
 package androidx.compose.foundation.relocation
 
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.Composable
+import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
 
 /**
  * Platform specific internal API to bring a rectangle into view.
  */
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-internal actual fun rememberDefaultBringIntoViewParent(): BringIntoViewParent {
-    return BringIntoViewParent { _, _ ->
+internal actual fun CompositionLocalConsumerModifierNode.defaultBringIntoViewParent():
+    BringIntoViewParent =
+     BringIntoViewParent { _, _ ->
         // TODO(b/203204124): Implement this if desktop has a
         //  concept similar to Android's View.requestRectangleOnScreen.
-    }
-}
\ No newline at end of file
+    }
\ No newline at end of file
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index 5e3f0aa..2ecb589 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -61,8 +61,11 @@
         testImplementation(libs.truth)
 
         androidTestImplementation(project(":compose:material3:material3:material3-samples"))
-        androidTestImplementation(project(":compose:foundation:foundation-layout"))
-        androidTestImplementation(project(":compose:test-utils"))
+        androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.4.0-beta02")
+        androidTestImplementation("androidx.compose.ui:ui-test:1.4.0-beta02")
+        androidTestImplementation(project(":compose:test-utils")){
+            transitive = false
+        }
         androidTestImplementation(project(":test:screenshot:screenshot"))
         androidTestImplementation(project(":core:core"))
         androidTestImplementation(libs.testRules)
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt
new file mode 100644
index 0000000..e336c3e
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/InternalMutatorMutex.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.material3
+
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.runtime.Stable
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/*** This is an internal copy of androidx.compose.foundation.MutatorMutex with an additional
+ * tryMutate method. Do not modify, except for tryMutate. ***/
+
+expect class InternalAtomicReference<V>(value: V) {
+    fun get(): V
+    fun set(value: V)
+    fun getAndSet(value: V): V
+    fun compareAndSet(expect: V, newValue: V): Boolean
+}
+
+/**
+ * Mutual exclusion for UI state mutation over time.
+ *
+ * [mutate] permits interruptible state mutation over time using a standard [MutatePriority].
+ * A [InternalMutatorMutex] enforces that only a single writer can be active at a time for a particular
+ * state resource. Instead of queueing callers that would acquire the lock like a traditional
+ * [Mutex], new attempts to [mutate] the guarded state will either cancel the current mutator or
+ * if the current mutator has a higher priority, the new caller will throw [CancellationException].
+ *
+ * [InternalMutatorMutex] should be used for implementing hoisted state objects that many mutators may
+ * want to manipulate over time such that those mutators can coordinate with one another. The
+ * [InternalMutatorMutex] instance should be hidden as an implementation detail. For example:
+ *
+ */
+@Stable
+internal class InternalMutatorMutex {
+    private class Mutator(val priority: MutatePriority, val job: Job) {
+        fun canInterrupt(other: Mutator) = priority >= other.priority
+
+        fun cancel() = job.cancel()
+    }
+
+    private val currentMutator = InternalAtomicReference<Mutator?>(null)
+    private val mutex = Mutex()
+
+    private fun tryMutateOrCancel(mutator: Mutator) {
+        while (true) {
+            val oldMutator = currentMutator.get()
+            if (oldMutator == null || mutator.canInterrupt(oldMutator)) {
+                if (currentMutator.compareAndSet(oldMutator, mutator)) {
+                    oldMutator?.cancel()
+                    break
+                }
+            } else throw CancellationException("Current mutation had a higher priority")
+        }
+    }
+
+    /**
+     * Enforce that only a single caller may be active at a time.
+     *
+     * If [mutate] is called while another call to [mutate] or [mutateWith] is in progress, their
+     * [priority] values are compared. If the new caller has a [priority] equal to or higher than
+     * the call in progress, the call in progress will be cancelled, throwing
+     * [CancellationException] and the new caller's [block] will be invoked. If the call in
+     * progress had a higher [priority] than the new caller, the new caller will throw
+     * [CancellationException] without invoking [block].
+     *
+     * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+     * Higher priority mutations will interrupt lower priority mutations.
+     * @param block mutation code to run mutually exclusive with any other call to [mutate],
+     * [mutateWith] or [tryMutate].
+     */
+    suspend fun <R> mutate(
+        priority: MutatePriority = MutatePriority.Default,
+        block: suspend () -> R
+    ) = coroutineScope {
+        val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+        tryMutateOrCancel(mutator)
+
+        mutex.withLock {
+            try {
+                block()
+            } finally {
+                currentMutator.compareAndSet(mutator, null)
+            }
+        }
+    }
+
+    /**
+     * Enforce that only a single caller may be active at a time.
+     *
+     * If [mutateWith] is called while another call to [mutate] or [mutateWith] is in progress,
+     * their [priority] values are compared. If the new caller has a [priority] equal to or
+     * higher than the call in progress, the call in progress will be cancelled, throwing
+     * [CancellationException] and the new caller's [block] will be invoked. If the call in
+     * progress had a higher [priority] than the new caller, the new caller will throw
+     * [CancellationException] without invoking [block].
+     *
+     * This variant of [mutate] calls its [block] with a [receiver], removing the need to create
+     * an additional capturing lambda to invoke it with a receiver object. This can be used to
+     * expose a mutable scope to the provided [block] while leaving the rest of the state object
+     * read-only. For example:
+     *
+     * @param receiver the receiver `this` that [block] will be called with
+     * @param priority the priority of this mutation; [MutatePriority.Default] by default.
+     * Higher priority mutations will interrupt lower priority mutations.
+     * @param block mutation code to run mutually exclusive with any other call to [mutate],
+     * [mutateWith] or [tryMutate].
+     */
+    suspend fun <T, R> mutateWith(
+        receiver: T,
+        priority: MutatePriority = MutatePriority.Default,
+        block: suspend T.() -> R
+    ) = coroutineScope {
+        val mutator = Mutator(priority, coroutineContext[Job]!!)
+
+        tryMutateOrCancel(mutator)
+
+        mutex.withLock {
+            try {
+                receiver.block()
+            } finally {
+                currentMutator.compareAndSet(mutator, null)
+            }
+        }
+    }
+
+    /**
+     * Attempt to mutate synchronously if there is no other active caller.
+     * If there is no other active caller, the [block] will be executed in a lock. If there is
+     * another active caller, this method will return false, indicating that the active caller
+     * needs to be cancelled through a [mutate] or [mutateWith] call with an equal or higher
+     * mutation priority.
+     *
+     * Calls to [mutate] and [mutateWith] will suspend until execution of the [block] has finished.
+     *
+     * @param block mutation code to run mutually exclusive with any other call to [mutate],
+     * [mutateWith] or [tryMutate].
+     * @return true if the [block] was executed, false if there was another active caller and the
+     * [block] was not executed.
+     */
+    fun tryMutate(block: () -> Unit): Boolean {
+        val didLock = mutex.tryLock()
+        if (didLock) {
+            try {
+                block()
+            } finally {
+                mutex.unlock()
+            }
+        }
+        return didLock
+    }
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
index 31776bd..854505f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt
@@ -285,7 +285,7 @@
     bottomPadding: Float,
     onDragStopped: CoroutineScope.(velocity: Float) -> Unit,
 ) = draggable(
-            state = sheetState.swipeableState.draggableState,
+            state = sheetState.swipeableState.swipeDraggableState,
             orientation = Orientation.Vertical,
             enabled = sheetState.isVisible,
             startDragImmediately = sheetState.swipeableState.isAnimationRunning,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
index fca009d..7696bfe 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SheetDefaults.kt
@@ -338,7 +338,7 @@
     override suspend fun onPreFling(available: Velocity): Velocity {
         val toFling = available.toFloat()
         val currentOffset = sheetState.requireOffset()
-        return if (toFling < 0 && currentOffset > sheetState.swipeableState.minBound) {
+        return if (toFling < 0 && currentOffset > sheetState.swipeableState.minOffset) {
             onFling(toFling)
             // since we go to the anchor with tween settling, consume all for the best UX
             available
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
index d57af0c..77bdf92 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SwipeableV2.kt
@@ -21,6 +21,8 @@
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.animate
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.DragScope
 import androidx.compose.foundation.gestures.DraggableState
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.draggable
@@ -50,6 +52,7 @@
 import androidx.compose.ui.unit.dp
 import kotlin.math.abs
 import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 
 /**
@@ -79,7 +82,7 @@
     reverseDirection: Boolean = false,
     interactionSource: MutableInteractionSource? = null
 ) = draggable(
-    state = state.draggableState,
+    state = state.swipeDraggableState,
     orientation = orientation,
     enabled = enabled,
     interactionSource = interactionSource,
@@ -122,7 +125,11 @@
             val previousTarget = state.targetValue
             val stateRequiresCleanup = state.updateAnchors(newAnchors)
             if (stateRequiresCleanup) {
-                anchorChangeHandler?.onAnchorsChanged(previousTarget, previousAnchors, newAnchors)
+                anchorChangeHandler?.onAnchorsChanged(
+                    previousTarget,
+                    previousAnchors,
+                    newAnchors
+                )
             }
         }
     },
@@ -165,6 +172,27 @@
     internal val velocityThreshold: Dp = SwipeableV2Defaults.VelocityThreshold,
 ) {
 
+    private val swipeMutex = InternalMutatorMutex()
+
+    internal val swipeDraggableState = object : DraggableState {
+        private val dragScope = object : DragScope {
+            override fun dragBy(pixels: Float) {
+                [email protected](pixels)
+            }
+        }
+
+        override suspend fun drag(
+            dragPriority: MutatePriority,
+            block: suspend DragScope.() -> Unit
+        ) {
+            swipe(dragPriority) { dragScope.block() }
+        }
+
+        override fun dispatchRawDelta(delta: Float) {
+            [email protected](delta)
+        }
+    }
+
     /**
      * The current value of the [SwipeableV2State].
      */
@@ -242,13 +270,19 @@
     var lastVelocity: Float by mutableStateOf(0f)
         private set
 
-    val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
-    val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
+    /**
+     * The minimum offset this state can reach. This will be the smallest anchor, or
+     * [Float.NEGATIVE_INFINITY] if the anchors are not initialized yet.
+     */
+    val minOffset by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
+
+    /**
+     * The maximum offset this state can reach. This will be the biggest anchor, or
+     * [Float.POSITIVE_INFINITY] if the anchors are not initialized yet.
+     */
+    val maxOffset by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
 
     private var animationTarget: T? by mutableStateOf(null)
-    internal val draggableState = DraggableState {
-        offset = ((offset ?: 0f) + it).coerceIn(minBound, maxBound)
-    }
 
     internal var anchors by mutableStateOf(emptyMap<T, Float>())
 
@@ -267,9 +301,10 @@
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
         val initialValueHasAnchor = if (previousAnchorsEmpty) {
-            val initialValueAnchor = anchors[currentValue]
+            val initialValue = currentValue
+            val initialValueAnchor = anchors[initialValue]
             val initialValueHasAnchor = initialValueAnchor != null
-            if (initialValueHasAnchor) offset = initialValueAnchor
+            if (initialValueHasAnchor) trySnapTo(initialValue)
             initialValueHasAnchor
         } else true
         return !initialValueHasAnchor || !previousAnchorsEmpty
@@ -282,6 +317,8 @@
 
     /**
      * Snap to a [targetValue] without any animation.
+     * If the [targetValue] is not in the set of anchors, the [currentValue] will be updated to the
+     * [targetValue] without updating the offset.
      *
      * @throws CancellationException if the interaction interrupted by another interaction like a
      * gesture interaction or another programmatic interaction like a [animateTo] or [snapTo] call.
@@ -289,16 +326,7 @@
      * @param targetValue The target value of the animation
      */
     suspend fun snapTo(targetValue: T) {
-        val targetOffset = anchors.requireAnchor(targetValue)
-        try {
-            draggableState.drag {
-                animationTarget = targetValue
-                dragBy(targetOffset - requireOffset())
-            }
-            this.currentValue = targetValue
-        } finally {
-            animationTarget = null
-        }
+        swipe { snap(targetValue) }
     }
 
     /**
@@ -319,7 +347,7 @@
         val targetOffset = anchors[targetValue]
         if (targetOffset != null) {
             try {
-                draggableState.drag {
+                swipe {
                     animationTarget = targetValue
                     var prev = offset ?: 0f
                     animate(prev, targetOffset, velocity, animationSpec) { value, velocity ->
@@ -366,17 +394,17 @@
     }
 
     /**
-     * Swipe by the [delta], coerce it in the bounds and dispatch it to the [draggableState].
+     * Swipe by the [delta], coerce it in the bounds and dispatch it to the [SwipeableV2State].
      *
-     * @return The delta the [draggableState] will consume
+     * @return The delta the consumed by the [SwipeableV2State]
      */
     fun dispatchRawDelta(delta: Float): Float {
         val currentDragPosition = offset ?: 0f
         val potentiallyConsumed = currentDragPosition + delta
-        val clamped = potentiallyConsumed.coerceIn(minBound, maxBound)
+        val clamped = potentiallyConsumed.coerceIn(minOffset, maxOffset)
         val deltaToConsume = clamped - currentDragPosition
-        if (abs(deltaToConsume) > 0) {
-            draggableState.dispatchRawDelta(deltaToConsume)
+        if (abs(deltaToConsume) >= 0) {
+            offset = ((offset ?: 0f) + deltaToConsume).coerceIn(minOffset, maxOffset)
         }
         return deltaToConsume
     }
@@ -428,6 +456,31 @@
             "this=$this SwipeableState?"
     }
 
+    private suspend fun swipe(
+        swipePriority: MutatePriority = MutatePriority.Default,
+        action: suspend () -> Unit
+    ): Unit = coroutineScope { swipeMutex.mutate(swipePriority, action) }
+
+    /**
+     * Attempt to snap synchronously. Snapping can happen synchronously when there is no other swipe
+     * transaction like a drag or an animation is progress. If there is another interaction in
+     * progress, the suspending [snapTo] overload needs to be used.
+     *
+     * @return true if the synchronous snap was successful, or false if we couldn't snap synchronous
+     */
+    internal fun trySnapTo(targetValue: T): Boolean = swipeMutex.tryMutate { snap(targetValue) }
+
+    private fun snap(targetValue: T) {
+        val targetOffset = anchors[targetValue]
+        if (targetOffset != null) {
+            dispatchRawDelta(targetOffset - (offset ?: 0f))
+            currentValue = targetValue
+            animationTarget = null
+        } else {
+            currentValue = targetValue
+        }
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [SwipeableV2State].
@@ -636,6 +689,3 @@
 
 private fun <T> Map<T, Float>.minOrNull() = minOfOrNull { (_, offset) -> offset }
 private fun <T> Map<T, Float>.maxOrNull() = maxOfOrNull { (_, offset) -> offset }
-private fun <T> Map<T, Float>.requireAnchor(value: T) = requireNotNull(this[value]) {
-    "Required anchor $value was not found in anchors. Current anchors: ${this.toMap()}"
-}
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt b/compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
similarity index 60%
rename from compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt
rename to compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
index 12a9f5a..65a109b 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/extensions/UrlAnnotationExtensions.android.kt
+++ b/compose/material3/material3/src/jvmMain/kotlin/androidx/compose/material3/ActualJvm.kt
@@ -1,5 +1,7 @@
+// ktlint-disable filename
+
 /*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,11 +16,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.text.platform.extensions
+package androidx.compose.material3
 
-import android.text.style.URLSpan
-import androidx.compose.ui.text.ExperimentalTextApi
-import androidx.compose.ui.text.UrlAnnotation
-
-@ExperimentalTextApi
-fun UrlAnnotation.toSpan(): URLSpan = URLSpan(url)
\ No newline at end of file
+/* Copy of androidx.compose.material.ActualJvm, mirrored from Foundation. This is used for the
+   M2/M3-internal copy of MutatorMutex.
+ */
+internal actual typealias InternalAtomicReference<V> =
+    java.util.concurrent.atomic.AtomicReference<V>
\ No newline at end of file
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index fb53cf4..11478bf 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -1260,6 +1260,15 @@
 
 }
 
+package androidx.compose.ui.text.platform {
+
+  @androidx.compose.ui.text.InternalTextApi public final class URLSpanCache {
+    ctor public URLSpanCache();
+    method public android.text.style.URLSpan toURLSpan(androidx.compose.ui.text.UrlAnnotation urlAnnotation);
+  }
+
+}
+
 package androidx.compose.ui.text.platform.extensions {
 
   public final class TtsAnnotationExtensions_androidKt {
@@ -1267,10 +1276,6 @@
     method public static android.text.style.TtsSpan toSpan(androidx.compose.ui.text.VerbatimTtsAnnotation);
   }
 
-  public final class UrlAnnotationExtensions_androidKt {
-    method @androidx.compose.ui.text.ExperimentalTextApi public static android.text.style.URLSpan toSpan(androidx.compose.ui.text.UrlAnnotation);
-  }
-
 }
 
 package androidx.compose.ui.text.style {
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index 55fdaed..9c0efcb 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -58,17 +58,18 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 
-@OptIn(InternalTextApi::class)
+@OptIn(InternalTextApi::class, ExperimentalTextApi::class)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class AndroidAccessibilitySpannableStringTest {
     private val context = InstrumentationRegistry.getInstrumentation().context
     private val density = Density(context)
-    private val resourceLoader = UncachedFontFamilyResolver(context)
+    private val fontFamilyResolver = UncachedFontFamilyResolver(context)
+    private val urlSpanCache = URLSpanCache()
 
     @Test
     fun toAccessibilitySpannableString_with_locale() {
@@ -80,8 +81,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -102,8 +106,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -123,8 +130,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -144,8 +154,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -164,8 +177,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -184,8 +200,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -205,11 +224,14 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
-        Truth.assertThat(
+        assertThat(
             spannableString.getSpans(
                 0,
                 spannableString.length,
@@ -227,8 +249,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -245,8 +270,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -266,8 +294,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -287,8 +318,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -298,7 +332,6 @@
         }
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun toAccessibilitySpannableString_with_verbatimTtsAnnotation() {
         val annotatedString = buildAnnotatedString {
@@ -308,8 +341,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -320,7 +356,6 @@
         }
     }
 
-    @OptIn(ExperimentalTextApi::class)
     @Test
     fun toAccessibilitySpannableString_with_urlAnnotation() {
         val annotatedString = buildAnnotatedString {
@@ -330,8 +365,11 @@
             }
         }
 
-        val spannableString =
-            annotatedString.toAccessibilitySpannableString(density, resourceLoader)
+        val spannableString = annotatedString.toAccessibilitySpannableString(
+            density,
+            fontFamilyResolver,
+            urlSpanCache
+        )
 
         assertThat(spannableString).isInstanceOf(SpannableString::class.java)
         assertThat(spannableString).hasSpan(
@@ -341,7 +379,6 @@
         }
     }
 
-    @OptIn(InternalTextApi::class)
     @Test
     fun fontsInSpanStyles_areIgnored() {
         // b/232238615
@@ -356,9 +393,32 @@
         val fontFamilyResolver = createFontFamilyResolver(context)
 
         // see if font span is added
-        string.toAccessibilitySpannableString(density, fontFamilyResolver)
+        string.toAccessibilitySpannableString(density, fontFamilyResolver, URLSpanCache())
 
         // toAccessibilitySpannableString should _not_ make any font requests
-        Truth.assertThat(loader.blockingRequests).isEmpty()
+        assertThat(loader.blockingRequests).isEmpty()
+    }
+
+    /**
+     * A11y services rely on ClickableSpans being the same instance for the same source spans across
+     * multiple calls of [toAccessibilitySpannableString] for the same source string. If this is not
+     * the case, their onClick handler will not be invoked. See b/253292081.
+     */
+    @Test
+    fun urlSpansAreSameInstanceForSameAnnotatedString() {
+        val string = buildAnnotatedString {
+            pushUrlAnnotation(UrlAnnotation("google.com"))
+            append("link")
+        }
+
+        val spannable1 =
+            string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+        val spannable2 =
+            string.toAccessibilitySpannableString(density, fontFamilyResolver, urlSpanCache)
+        val urlSpan1 = spannable1.getSpans(0, string.length, URLSpan::class.java).single()
+        val urlSpan2 = spannable2.getSpans(0, string.length, URLSpan::class.java).single()
+
+        assertThat(spannable1).isNotSameInstanceAs(spannable2)
+        assertThat(urlSpan1).isSameInstanceAs(urlSpan2)
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
index 83666c8..f9f160e 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableString.android.kt
@@ -32,13 +32,11 @@
 import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.font.GenericFontFamily
-import androidx.compose.ui.text.font.createFontFamilyResolver
 import androidx.compose.ui.text.font.getAndroidTypefaceStyle
 import androidx.compose.ui.text.platform.extensions.setBackground
 import androidx.compose.ui.text.platform.extensions.setColor
@@ -49,27 +47,13 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastForEach
 
-/**
- * Convert an AnnotatedString into SpannableString for Android text to speech support.
- *
- * @suppress
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@InternalTextApi // used in ui:ui
-fun AnnotatedString.toAccessibilitySpannableString(
-    density: Density,
-    @Suppress("DEPRECATION") resourceLoader: Font.ResourceLoader
-): SpannableString {
-    @Suppress("DEPRECATION")
-    return toAccessibilitySpannableString(density, createFontFamilyResolver(resourceLoader))
-}
-
 @OptIn(ExperimentalTextApi::class)
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @InternalTextApi // used in ui:ui
 fun AnnotatedString.toAccessibilitySpannableString(
     density: Density,
-    fontFamilyResolver: FontFamily.Resolver
+    fontFamilyResolver: FontFamily.Resolver,
+    urlSpanCache: URLSpanCache
 ): SpannableString {
     val spannableString = SpannableString(text)
     spanStylesOrNull?.fastForEach { (style, start, end) ->
@@ -90,7 +74,7 @@
 
     getUrlAnnotations(0, length).fastForEach { (urlAnnotation, start, end) ->
         spannableString.setSpan(
-            urlAnnotation.toSpan(),
+            urlSpanCache.toURLSpan(urlAnnotation),
             start,
             end,
             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt
new file mode 100644
index 0000000..a8444ba
--- /dev/null
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/URLSpanCache.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.text.platform
+
+import android.text.style.URLSpan
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.UrlAnnotation
+import java.util.WeakHashMap
+
+/**
+ * This class converts [UrlAnnotation]s to [URLSpan]s, ensuring that the same instance of [URLSpan]
+ * will be returned for every instance of [UrlAnnotation]. This is required for [URLSpan]s (and
+ * any ClickableSpan) to be handled correctly by accessibility services, which require every
+ * ClickableSpan to have a stable ID across reads from the accessibility node. A11y services convert
+ * these spans to parcelable ones, then look them up later using their ID. Since the ID is a hidden
+ * property, the only way to satisfy this constraint is to actually use the same [URLSpan] instance
+ * every time.
+ *
+ * See b/253292081.
+ */
+// "URL" violates naming guidelines, but that is intentional to match the platform API.
+@Suppress("AcronymName")
+@OptIn(ExperimentalTextApi::class)
+@InternalTextApi
+class URLSpanCache {
+    private val spansByAnnotation = WeakHashMap<UrlAnnotation, URLSpan>()
+
+    @Suppress("AcronymName")
+    fun toURLSpan(urlAnnotation: UrlAnnotation): URLSpan =
+        spansByAnnotation.getOrPut(urlAnnotation) { URLSpan(urlAnnotation.url) }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index fc1e5aa..cab650d 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1764,6 +1764,11 @@
     method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
   }
 
+  public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+    method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+    method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+  }
+
   public final class RotaryScrollEvent {
     method public float getHorizontalScrollPixels();
     method public long getUptimeMillis();
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 5c219f3..3d089cf 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -1573,6 +1573,16 @@
     method public static int getNativeKeyCode(long);
   }
 
+  @androidx.compose.ui.ExperimentalComposeUiApi public interface SoftKeyboardInterceptionModifierNode extends androidx.compose.ui.node.DelegatableNode {
+    method public boolean onInterceptKeyBeforeSoftKeyboard(android.view.KeyEvent event);
+    method public boolean onPreInterceptKeyBeforeSoftKeyboard(android.view.KeyEvent event);
+  }
+
+  public final class SoftwareKeyboardInterceptionModifierKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier onInterceptKeyBeforeSoftKeyboard(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onInterceptKeyBeforeSoftKeyboard);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static androidx.compose.ui.Modifier onPreInterceptKeyBeforeSoftKeyboard(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.key.KeyEvent,java.lang.Boolean> onPreInterceptKeyBeforeSoftKeyboard);
+  }
+
 }
 
 package androidx.compose.ui.input.nestedscroll {
@@ -1910,7 +1920,7 @@
     method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
   }
 
-  @androidx.compose.ui.ExperimentalComposeUiApi public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+  public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
     method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
     method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
   }
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 6839e9b..272ecd7 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1764,6 +1764,11 @@
     method public static androidx.compose.ui.Modifier onRotaryScrollEvent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.rotary.RotaryScrollEvent,java.lang.Boolean> onRotaryScrollEvent);
   }
 
+  public interface RotaryInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
+    method public boolean onPreRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+    method public boolean onRotaryScrollEvent(androidx.compose.ui.input.rotary.RotaryScrollEvent event);
+  }
+
   public final class RotaryScrollEvent {
     method public float getHorizontalScrollPixels();
     method public long getUptimeMillis();
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
index 7641c8a..cdb4b23 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/ClipDrawTest.kt
@@ -20,8 +20,11 @@
 import android.os.Build
 import androidx.activity.compose.setContent
 import androidx.annotation.RequiresApi
+import androidx.compose.foundation.layout.Box
 import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.AtLeastSize
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Padding
@@ -41,24 +44,29 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.layout
 import androidx.compose.ui.padding
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.runOnUiThreadIR
 import androidx.compose.ui.test.TestActivity
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
 import androidx.compose.ui.waitAndScreenShot
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.junit.Assert
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -511,6 +519,48 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    @Test
+    fun resizingIsReflectedInGraphicsLayer() {
+        var sizePx by mutableStateOf(20)
+        rule.runOnUiThread {
+            activity.setContent {
+                Box(
+                    modifier = Modifier
+                        .background(Color.White)
+                        .layout { measurable, constraints ->
+                            val placeable = measurable.measure(constraints)
+                            layout(30, 30) {
+                                placeable.place(IntOffset.Zero)
+                            }
+                        }
+                        .layout { measurable, constraints ->
+                            val placeable = measurable.measure(Constraints.fixed(sizePx, sizePx))
+                            layout(
+                                constraints.constrainWidth(sizePx),
+                                constraints.constrainHeight(sizePx)
+                            ) {
+                                placeable.place(IntOffset.Zero)
+                            }
+                        }
+                        .clipToBounds()
+                        .fillColor(Color.Red)
+                )
+            }
+        }
+
+        takeScreenShot(30).apply {
+            assertRect(Color.Red, size = 20, centerX = 10, centerY = 10)
+        }
+
+        drawLatch = CountDownLatch(1)
+        sizePx = 30
+
+        takeScreenShot(30).apply {
+            assertRect(Color.Red, size = 30)
+        }
+    }
+
     private fun Modifier.fillColor(color: Color): Modifier {
         return drawBehind {
             drawRect(
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
index e4256b6..3baf4aa 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/focus/FocusAwareEventPropagationTest.kt
@@ -17,8 +17,9 @@
 package androidx.compose.ui.input.focus
 
 import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.KEYCODE_A
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
@@ -26,21 +27,24 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.focusTarget
 import androidx.compose.ui.focus.setFocusableContent
+import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.InterruptedSoftKeyboardInput
 import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.KeyInput
 import androidx.compose.ui.input.focus.FocusAwareEventPropagationTest.NodeType.RotaryInput
 import androidx.compose.ui.input.key.KeyEvent
-import androidx.compose.ui.input.key.KeyInputInputModifierNodeImpl
-import androidx.compose.ui.input.rotary.RotaryInputModifierNodeImpl
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.input.key.onPreInterceptKeyBeforeSoftKeyboard
+import androidx.compose.ui.input.key.onPreviewKeyEvent
 import androidx.compose.ui.input.rotary.RotaryScrollEvent
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.input.rotary.onPreRotaryScrollEvent
+import androidx.compose.ui.input.rotary.onRotaryScrollEvent
 import androidx.compose.ui.test.ExperimentalTestApi
 import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performKeyPress
 import androidx.compose.ui.test.performRotaryScrollInput
-import androidx.compose.ui.unit.dp
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -59,10 +63,8 @@
     val rule = createComposeRule()
 
     private val sentEvent: Any = when (nodeType) {
-        KeyInput ->
-            KeyEvent(AndroidKeyEvent(AndroidKeyEvent.ACTION_DOWN, AndroidKeyEvent.KEYCODE_A))
-        RotaryInput ->
-            RotaryScrollEvent(1f, 1f, 0L)
+        KeyInput, InterruptedSoftKeyboardInput -> KeyEvent(AndroidKeyEvent(ACTION_DOWN, KEYCODE_A))
+        RotaryInput -> RotaryScrollEvent(1f, 1f, 0L)
     }
     private var receivedEvent: Any? = null
     private val initialFocus = FocusRequester()
@@ -70,7 +72,7 @@
     companion object {
         @JvmStatic
         @Parameterized.Parameters(name = "node = {0}")
-        fun initParameters() = arrayOf(KeyInput, RotaryInput)
+        fun initParameters() = arrayOf(KeyInput, InterruptedSoftKeyboardInput, RotaryInput)
     }
 
     @Before
@@ -102,7 +104,8 @@
         // Assert.
         assertThat(receivedEvent).isNull()
         when (nodeType) {
-            KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
+            KeyInput, InterruptedSoftKeyboardInput ->
+                assertThat(error!!.message).contains("do not have an active focus target")
             RotaryInput -> assertThat(error).isNull()
         }
     }
@@ -133,14 +136,14 @@
         assertThat(receivedEvent).isNull()
         when (nodeType) {
             KeyInput -> assertThat(error!!.message).contains("do not have an active focus target")
-            RotaryInput -> assertThat(error).isNull()
+            InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
         }
     }
 
     @Test
     fun onFocusAwareEvent_afterFocusable_isNotTriggered() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .focusable(initiallyFocused = true)
@@ -158,14 +161,14 @@
         // Assert.
         when (nodeType) {
             KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
-            RotaryInput -> assertThat(receivedEvent).isNull()
+            InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
         }
     }
 
     @Test
     fun onPreFocusAwareEvent_afterFocusable_isNotTriggered() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .focusable(initiallyFocused = true)
@@ -182,14 +185,14 @@
         // Assert.
         when (nodeType) {
             KeyInput -> assertThat(receivedEvent).isEqualTo(sentEvent)
-            RotaryInput -> assertThat(receivedEvent).isNull()
+            InterruptedSoftKeyboardInput, RotaryInput -> assertThat(receivedEvent).isNull()
         }
     }
 
     @Test
     fun onFocusAwareEvent_isTriggered() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -210,7 +213,7 @@
     @Test
     fun onPreFocusAwareEvent_triggered() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onPreFocusAwareEvent {
@@ -231,7 +234,7 @@
     @Test
     fun onFocusAwareEventNotTriggered_ifOnPreFocusAwareEventConsumesEvent_1() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -257,7 +260,7 @@
     @Test
     fun onFocusAwareEventNotTriggered_ifOnPreFocusAwareEventConsumesEvent_2() {
         // Arrange.
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onPreFocusAwareEvent {
@@ -286,7 +289,7 @@
         var triggerIndex = 1
         var onFocusAwareEventTrigger = 0
         var onPreFocusAwareEventTrigger = 0
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -317,7 +320,7 @@
         var triggerIndex = 1
         var onFocusAwareEventTrigger = 0
         var onPreFocusAwareEventTrigger = 0
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onPreFocusAwareEvent {
@@ -350,7 +353,7 @@
         var parentOnPreFocusAwareEventTrigger = 0
         var childOnFocusAwareEventTrigger = 0
         var childOnPreFocusAwareEventTrigger = 0
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -398,7 +401,7 @@
         var parentOnPreFocusAwareEventTrigger = 0
         var childOnFocusAwareEventTrigger = 0
         var childOnPreFocusAwareEventTrigger = 0
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -447,7 +450,7 @@
         var parentOnPreFocusAwareEventTrigger = 0
         var childOnFocusAwareEventTrigger = 0
         var childOnPreFocusAwareEventTrigger = 0
-        ContentWithInitialFocus {
+        rule.setContentWithInitialFocus {
             Box(
                 modifier = Modifier
                     .onFocusAwareEvent {
@@ -508,7 +511,7 @@
 
     private fun SemanticsNodeInteraction.performFocusAwareInput(sentEvent: Any) {
         when (nodeType) {
-            KeyInput -> {
+            KeyInput, InterruptedSoftKeyboardInput -> {
                 check(sentEvent is KeyEvent)
                 performKeyPress(sentEvent)
             }
@@ -522,55 +525,24 @@
         }
     }
 
-    private fun ContentWithInitialFocus(content: @Composable () -> Unit) {
-        rule.setContent {
-            Box(modifier = Modifier.requiredSize(10.dp, 10.dp)) { content() }
-        }
-        rule.runOnIdle { initialFocus.requestFocus() }
+    private fun ComposeContentTestRule.setContentWithInitialFocus(content: @Composable () -> Unit) {
+        setFocusableContent(content)
+        runOnIdle { initialFocus.requestFocus() }
     }
 
-    private fun Modifier.onFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
-        FocusAwareEventElement(onEvent, nodeType, EventType.OnEvent)
-    )
-
-    private fun Modifier.onPreFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = this.then(
-        FocusAwareEventElement(onEvent, nodeType, EventType.PreEvent)
-    )
-
     @OptIn(ExperimentalComposeUiApi::class)
-    private data class FocusAwareEventElement(
-        private val callback: (Any) -> Boolean,
-        private val nodeType: NodeType,
-        private val eventType: EventType
-    ) : ModifierNodeElement<Modifier.Node>() {
-        override fun create() = when (nodeType) {
-            KeyInput -> KeyInputInputModifierNodeImpl(
-                onEvent = callback.takeIf { eventType == EventType.OnEvent },
-                onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
-            )
-            RotaryInput -> RotaryInputModifierNodeImpl(
-                onEvent = callback.takeIf { eventType == EventType.OnEvent },
-                onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
-            )
-        }
-
-        override fun update(node: Modifier.Node) = when (nodeType) {
-            KeyInput -> (node as KeyInputInputModifierNodeImpl).apply {
-                onEvent = callback.takeIf { eventType == EventType.OnEvent }
-                onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
-            }
-            RotaryInput -> (node as RotaryInputModifierNodeImpl).apply {
-                onEvent = callback.takeIf { eventType == EventType.OnEvent }
-                onPreEvent = callback.takeIf { eventType == EventType.PreEvent }
-            }
-        }
-
-        override fun InspectorInfo.inspectableProperties() {
-            name = "onEvent"
-            properties["onEvent"] = callback
-        }
+    private fun Modifier.onFocusAwareEvent(onEvent: (Any) -> Boolean): Modifier = when (nodeType) {
+        KeyInput -> onKeyEvent(onEvent)
+        InterruptedSoftKeyboardInput -> onInterceptKeyBeforeSoftKeyboard(onEvent)
+        RotaryInput -> onRotaryScrollEvent(onEvent)
     }
 
-    enum class NodeType { KeyInput, RotaryInput }
-    enum class EventType { PreEvent, OnEvent }
+    @OptIn(ExperimentalComposeUiApi::class)
+    private fun Modifier.onPreFocusAwareEvent(onPreEvent: (Any) -> Boolean) = when (nodeType) {
+        KeyInput -> onPreviewKeyEvent(onPreEvent)
+        InterruptedSoftKeyboardInput -> onPreInterceptKeyBeforeSoftKeyboard(onPreEvent)
+        RotaryInput -> onPreRotaryScrollEvent(onPreEvent)
+    }
+
+    enum class NodeType { KeyInput, InterruptedSoftKeyboardInput, RotaryInput }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
new file mode 100644
index 0000000..5b4c1ba
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/key/HardwareKeyInputTest.kt
@@ -0,0 +1,316 @@
+/*
+ * 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.key
+
+import android.view.KeyEvent as AndroidKeyEvent
+import android.view.KeyEvent.KEYCODE_A as KeyCodeA
+import android.view.KeyEvent.ACTION_DOWN
+import android.view.KeyEvent.ACTION_UP
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.focus.focusTarget
+import androidx.compose.ui.focus.setFocusableContent
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
+import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performKeyPress
+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 kotlin.test.Test
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalComposeUiApi::class)
+class HardwareKeyInputTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    val initialFocus = FocusRequester()
+
+    @Test
+    fun onKeyToSoftKeyboardInterceptedEventTriggered() {
+        // Arrange.
+        var receivedKeyEvent: KeyEvent? = null
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onInterceptKeyBeforeSoftKeyboard {
+                        receivedKeyEvent = it
+                        true
+                    }
+                    .focusRequester(initialFocus)
+                    .focusTarget()
+            )
+        }
+
+        // Act.
+        val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            val keyEvent = checkNotNull(receivedKeyEvent)
+            assertThat(keyEvent.key).isEqualTo(Key.A)
+            assertThat(keyEvent.type).isEqualTo(KeyUp)
+            assertThat(keyConsumed).isTrue()
+        }
+    }
+
+    @Test
+    fun onPreKeyToSoftKeyboardInterceptedEventTriggered() {
+        // Arrange.
+        var receivedKeyEvent: KeyEvent? = null
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onPreInterceptKeyBeforeSoftKeyboard {
+                        receivedKeyEvent = it
+                        true
+                    }
+                    .focusRequester(initialFocus)
+                    .focusTarget()
+            )
+        }
+
+        // Act.
+        val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            val keyEvent = checkNotNull(receivedKeyEvent)
+            assertThat(keyEvent.key).isEqualTo(Key.A)
+            assertThat(keyEvent.type).isEqualTo(KeyUp)
+            assertThat(keyConsumed).isTrue()
+        }
+    }
+
+    @Test
+    fun onKeyEventNotTriggered_ifOnPreKeyEventConsumesEvent() {
+        // Arrange.
+        var receivedPreKeyEvent: KeyEvent? = null
+        var receivedKeyEvent: KeyEvent? = null
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onInterceptKeyBeforeSoftKeyboard {
+                        receivedKeyEvent = it
+                        true
+                    }
+                    .onPreInterceptKeyBeforeSoftKeyboard {
+                        receivedPreKeyEvent = it
+                        true
+                    }
+                    .focusRequester(initialFocus)
+                    .focusTarget()
+            )
+        }
+
+        // Act.
+        val keyConsumed = rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            val keyEvent = checkNotNull(receivedPreKeyEvent)
+            assertThat(keyEvent.key).isEqualTo(Key.A)
+            assertThat(keyEvent.type).isEqualTo(KeyUp)
+            assertThat(keyConsumed).isTrue()
+
+            assertThat(receivedKeyEvent).isNull()
+        }
+    }
+
+    @Test
+    fun onKeyEvent_triggeredAfter_onPreviewKeyEvent() {
+        // Arrange.
+        var triggerIndex = 1
+        var onInterceptedKeyEventTrigger = 0
+        var onInterceptedPreKeyEventTrigger = 0
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onInterceptKeyBeforeSoftKeyboard {
+                        onInterceptedKeyEventTrigger = triggerIndex++
+                        true
+                    }
+                    .onPreInterceptKeyBeforeSoftKeyboard {
+                        onInterceptedPreKeyEventTrigger = triggerIndex++
+                        false
+                    }
+                    .focusRequester(initialFocus)
+                    .focusTarget()
+            )
+        }
+
+        // Act.
+        rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(onInterceptedPreKeyEventTrigger).isEqualTo(1)
+            assertThat(onInterceptedKeyEventTrigger).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun onKeyEvent_onKeyToSoftKeyboardInterceptedEvent_interaction() {
+        // Arrange.
+        var triggerIndex = 1
+        var onInterceptedKeyEventTrigger = 0
+        var onInterceptedPreKeyEventTrigger = 0
+        var onKeyEventTrigger = 0
+        var onPreviewKeyEventTrigger = 0
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onKeyEvent {
+                        onKeyEventTrigger = triggerIndex++
+                        false
+                    }
+                    .onPreviewKeyEvent {
+                        onPreviewKeyEventTrigger = triggerIndex++
+                        false
+                    }
+                    .onInterceptKeyBeforeSoftKeyboard {
+                        onInterceptedKeyEventTrigger = triggerIndex++
+                        false
+                    }
+                    .onPreInterceptKeyBeforeSoftKeyboard {
+                        onInterceptedPreKeyEventTrigger = triggerIndex++
+                        false
+                    }
+                    .focusRequester(initialFocus)
+                    .focusTarget()
+            )
+        }
+
+        // Act.
+        rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(onInterceptedPreKeyEventTrigger).isEqualTo(1)
+            assertThat(onInterceptedKeyEventTrigger).isEqualTo(2)
+            assertThat(onPreviewKeyEventTrigger).isEqualTo(3)
+            assertThat(onKeyEventTrigger).isEqualTo(4)
+        }
+    }
+
+    @Test
+    fun onKeyEvent_onKeyToSoftKeyboardInterceptedEvent_parentChildInteraction() {
+        // Arrange.
+        var triggerIndex = 1
+        var onInterceptedKeyEventChildTrigger = 0
+        var onInterceptedKeyEventParentTrigger = 0
+        var onPreInterceptedKeyEventChildTrigger = 0
+        var onPreInterceptedKeyEvenParentTrigger = 0
+        var onKeyEventChildTrigger = 0
+        var onKeyEventParentTrigger = 0
+        var onPreKeyEventChildTrigger = 0
+        var onPreKeyEventParentTrigger = 0
+        rule.setContentWithInitialFocus {
+            Box(
+                Modifier
+                    .onKeyEvent {
+                        onKeyEventParentTrigger = triggerIndex++
+                        false
+                    }
+                    .onPreviewKeyEvent {
+                        onPreKeyEventParentTrigger = triggerIndex++
+                        false
+                    }
+                    .onInterceptKeyBeforeSoftKeyboard {
+                        onInterceptedKeyEventParentTrigger = triggerIndex++
+                        false
+                    }
+                    .onPreInterceptKeyBeforeSoftKeyboard {
+                        onPreInterceptedKeyEvenParentTrigger = triggerIndex++
+                        false
+                    }
+            ) {
+                Box(
+                    Modifier
+                        .onKeyEvent {
+                            onKeyEventChildTrigger = triggerIndex++
+                            false
+                        }
+                        .onPreviewKeyEvent {
+                            onPreKeyEventChildTrigger = triggerIndex++
+                            false
+                        }
+                        .onInterceptKeyBeforeSoftKeyboard {
+                            onInterceptedKeyEventChildTrigger = triggerIndex++
+                            false
+                        }
+                        .onPreInterceptKeyBeforeSoftKeyboard {
+                            onPreInterceptedKeyEventChildTrigger = triggerIndex++
+                            false
+                        }
+                        .focusRequester(initialFocus)
+                        .focusable()
+                )
+            }
+        }
+
+        // Act.
+        rule.onRoot().performKeyPress(keyEvent(KeyCodeA, KeyUp))
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(onPreInterceptedKeyEvenParentTrigger).isEqualTo(1)
+            assertThat(onPreInterceptedKeyEventChildTrigger).isEqualTo(2)
+            assertThat(onInterceptedKeyEventChildTrigger).isEqualTo(3)
+            assertThat(onInterceptedKeyEventParentTrigger).isEqualTo(4)
+            assertThat(onPreKeyEventParentTrigger).isEqualTo(5)
+            assertThat(onPreKeyEventChildTrigger).isEqualTo(6)
+            assertThat(onKeyEventChildTrigger).isEqualTo(7)
+            assertThat(onKeyEventParentTrigger).isEqualTo(8)
+        }
+    }
+
+    private fun ComposeContentTestRule.setContentWithInitialFocus(content: @Composable () -> Unit) {
+        setFocusableContent {
+            Box(modifier = Modifier.requiredSize(100.dp, 100.dp)) { content() }
+        }
+        runOnIdle {
+            initialFocus.requestFocus()
+        }
+    }
+
+    /**
+     * The [KeyEvent] is usually created by the system. This function creates an instance of
+     * [KeyEvent] that can be used in tests.
+     */
+    private fun keyEvent(keycode: Int, keyEventType: KeyEventType): KeyEvent {
+        val action = when (keyEventType) {
+            KeyDown -> ACTION_DOWN
+            KeyUp -> ACTION_UP
+            else -> error("Unknown key event type")
+        }
+        return KeyEvent(AndroidKeyEvent(0L, 0L, action, keycode, 0, 0))
+    }
+}
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 02081d9..60fe14e 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
@@ -624,22 +624,42 @@
         }
     }
 
-    override fun sendKeyEvent(keyEvent: KeyEvent): Boolean {
-        return focusOwner.dispatchKeyEvent(keyEvent)
-    }
+    /**
+     * This function is used by the testing framework to send key events.
+     */
+    override fun sendKeyEvent(keyEvent: KeyEvent): Boolean =
+        // First dispatch the key event to mimic the event being intercepted before it is sent to
+        // the soft keyboard.
+        focusOwner.dispatchInterceptedSoftKeyboardEvent(keyEvent) ||
+            // Next, send the key event to the Soft Keyboard.
+            // TODO(b/272600716): Send the key event to the IME.
 
-    override fun dispatchKeyEvent(event: AndroidKeyEvent) =
+            // Finally, dispatch the key event to onPreKeyEvent/onKeyEvent listeners.
+            focusOwner.dispatchKeyEvent(keyEvent)
+
+    override fun dispatchKeyEvent(event: AndroidKeyEvent): Boolean {
         if (isFocused) {
             // Focus lies within the Compose hierarchy, so we dispatch the key event to the
             // appropriate place.
             _windowInfo.keyboardModifiers = PointerKeyboardModifiers(event.metaState)
-            sendKeyEvent(KeyEvent(event))
+            // If the event is not consumed, use the default implementation.
+            return focusOwner.dispatchKeyEvent(KeyEvent(event)) || super.dispatchKeyEvent(event)
         } else {
-            // This Owner has a focused child view, which is a view interop use case,
+            // This Owner has a focused child view, which is a view interoperability use case,
             // so we use the default ViewGroup behavior which will route tke key event to the
             // focused view.
-            super.dispatchKeyEvent(event)
+            return super.dispatchKeyEvent(event)
         }
+    }
+
+    override fun dispatchKeyEventPreIme(event: AndroidKeyEvent): Boolean {
+        return (isFocused && focusOwner.dispatchInterceptedSoftKeyboardEvent(KeyEvent(event))) ||
+            // If this view is not focused, and it received a key event, it means this is a view
+            // interoperability use case and we need to route the event to the embedded child view.
+            // Also, if this event wasn't consumed by the compose hierarchy, we need to send it back
+            // to the parent view. Both these cases are handles by the default view implementation.
+            super.dispatchKeyEventPreIme(event)
+    }
 
     override fun onAttach(node: LayoutNode) {
     }
@@ -1378,9 +1398,8 @@
         eventTime: Long,
         forceHover: Boolean = true
     ) {
-        val oldAction = motionEvent.actionMasked
         // don't send any events for pointers that are "up" unless they support hover
-        val upIndex = when (oldAction) {
+        val upIndex = when (motionEvent.actionMasked) {
             ACTION_UP -> if (action == ACTION_HOVER_ENTER || action == ACTION_HOVER_EXIT) -1 else 0
             ACTION_POINTER_UP -> motionEvent.actionIndex
             else -> -1
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 773cc2e..ee20288 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
@@ -52,7 +52,6 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.boundsInParent
-import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.LayoutNode
@@ -83,6 +82,7 @@
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.platform.URLSpanCache
 import androidx.compose.ui.text.platform.toAccessibilitySpannableString
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
@@ -190,6 +190,7 @@
     return null
 }
 
+@OptIn(InternalTextApi::class)
 internal class AndroidComposeViewAccessibilityDelegateCompat(val view: AndroidComposeView) :
     AccessibilityDelegateCompat() {
     companion object {
@@ -393,6 +394,8 @@
     private val EXTRA_DATA_TEST_TRAVERSALAFTER_VAL =
         "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALAFTER_VAL"
 
+    private val urlSpanCache = URLSpanCache()
+
     /**
      * A snapshot of the semantics node. The children here is fixed and are taken from the time
      * this node is constructed. While a SemanticsNode always contains the up-to-date children.
@@ -1290,7 +1293,6 @@
         }
     }
 
-    @OptIn(InternalTextApi::class)
     private fun setText(
         node: SemanticsNode,
         info: AccessibilityNodeInfoCompat,
@@ -1298,13 +1300,21 @@
         val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
         val editableTextToAssign = trimToSize(
             node.unmergedConfig.getTextForTextField()
-                ?.toAccessibilitySpannableString(density = view.density, fontFamilyResolver),
+                ?.toAccessibilitySpannableString(
+                    density = view.density,
+                    fontFamilyResolver,
+                    urlSpanCache
+                ),
             ParcelSafeTextLength
         )
 
         val textToAssign = trimToSize(
             node.unmergedConfig.getOrNull(SemanticsProperties.Text)?.firstOrNull()
-                ?.toAccessibilitySpannableString(density = view.density, fontFamilyResolver),
+                ?.toAccessibilitySpannableString(
+                    density = view.density,
+                    fontFamilyResolver,
+                    urlSpanCache
+                ),
             ParcelSafeTextLength
         )
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
index 9566714..04671af 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwner.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.focus
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.input.key.KeyEvent
@@ -80,6 +79,11 @@
     fun dispatchKeyEvent(keyEvent: KeyEvent): Boolean
 
     /**
+     * Dispatches an intercepted soft keyboard key event through the compose hierarchy.
+     */
+    fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean
+
+    /**
      * Dispatches a rotary scroll event through the compose hierarchy.
      */
     fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean
@@ -87,18 +91,15 @@
     /**
      * Schedule a FocusTarget node to be invalidated after onApplyChanges.
      */
-    @OptIn(ExperimentalComposeUiApi::class)
     fun scheduleInvalidation(node: FocusTargetModifierNode)
 
     /**
      * Schedule a FocusEvent node to be invalidated after onApplyChanges.
      */
-    @OptIn(ExperimentalComposeUiApi::class)
     fun scheduleInvalidation(node: FocusEventModifierNode)
 
     /**
      * Schedule a FocusProperties node to be invalidated after onApplyChanges.
      */
-    @OptIn(ExperimentalComposeUiApi::class)
     fun scheduleInvalidation(node: FocusPropertiesModifierNode)
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
index 7a08381..638594b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusOwnerImpl.kt
@@ -192,14 +192,25 @@
             onPreVisit = { if (it.onPreKeyEvent(keyEvent)) return true },
             onVisit = { if (it.onKeyEvent(keyEvent)) return true }
         )
+        return false
+    }
 
+    @OptIn(ExperimentalComposeUiApi::class)
+    override fun dispatchInterceptedSoftKeyboardEvent(keyEvent: KeyEvent): Boolean {
+        val focusedSoftKeyboardInterceptionNode = rootFocusNode.findActiveFocusNode()
+            ?.nearestAncestor(Nodes.SoftKeyboardKeyInput)
+
+        focusedSoftKeyboardInterceptionNode?.traverseAncestors(
+            type = Nodes.SoftKeyboardKeyInput,
+            onPreVisit = { if (it.onPreInterceptKeyBeforeSoftKeyboard(keyEvent)) return true },
+            onVisit = { if (it.onInterceptKeyBeforeSoftKeyboard(keyEvent)) return true }
+        )
         return false
     }
 
     /**
      * Dispatches a rotary scroll event through the compose hierarchy.
      */
-    @OptIn(ExperimentalComposeUiApi::class)
     override fun dispatchRotaryEvent(event: RotaryScrollEvent): Boolean {
         val focusedRotaryInputNode = rootFocusNode.findActiveFocusNode()
             ?.nearestAncestor(Nodes.RotaryInput)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
index 4d620d4..b823385 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifier.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.input.key
 
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
 
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -30,7 +32,7 @@
  */
 fun Modifier.onKeyEvent(
     onKeyEvent: (KeyEvent) -> Boolean
-): Modifier = this then OnKeyEventElement(onKeyEvent)
+): Modifier = this then KeyInputElement(onKeyEvent = onKeyEvent, onPreKeyEvent = null)
 
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -46,4 +48,35 @@
  */
 fun Modifier.onPreviewKeyEvent(
     onPreviewKeyEvent: (KeyEvent) -> Boolean
-): Modifier = this then OnPreviewKeyEvent(onPreviewKeyEvent)
\ No newline at end of file
+): Modifier = this then KeyInputElement(onKeyEvent = null, onPreKeyEvent = onPreviewKeyEvent)
+
+private data class KeyInputElement(
+    val onKeyEvent: ((KeyEvent) -> Boolean)?,
+    val onPreKeyEvent: ((KeyEvent) -> Boolean)?
+) : ModifierNodeElement<KeyInputModifierNodeImpl>() {
+    override fun create() = KeyInputModifierNodeImpl(onKeyEvent, onPreKeyEvent)
+
+    override fun update(node: KeyInputModifierNodeImpl) = node.apply {
+        onEvent = onKeyEvent
+        onPreEvent = onPreKeyEvent
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        onKeyEvent?.let {
+            name = "onKeyEvent"
+            properties["onKeyEvent"] = it
+        }
+        onPreKeyEvent?.let {
+            name = "onPreviewKeyEvent"
+            properties["onPreviewKeyEvent"] = it
+        }
+    }
+}
+
+private class KeyInputModifierNodeImpl(
+    var onEvent: ((KeyEvent) -> Boolean)?,
+    var onPreEvent: ((KeyEvent) -> Boolean)?
+) : KeyInputModifierNode, Modifier.Node() {
+    override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
+    override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
index 8521dec..550c5c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/KeyInputModifierNode.kt
@@ -18,8 +18,6 @@
 
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
 
 /**
  * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events.
@@ -47,48 +45,3 @@
      */
     fun onPreKeyEvent(event: KeyEvent): Boolean
 }
-
-internal data class OnKeyEventElement(
-    val onKeyEvent: (KeyEvent) -> Boolean
-) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
-    override fun create() = KeyInputInputModifierNodeImpl(
-        onEvent = onKeyEvent,
-        onPreEvent = null
-    )
-
-    override fun update(node: KeyInputInputModifierNodeImpl) = node.apply {
-        onEvent = onKeyEvent
-        onPreEvent = null
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "onKeyEvent"
-        properties["onKeyEvent"] = onKeyEvent
-    }
-}
-
-internal data class OnPreviewKeyEvent(
-    val onPreviewKeyEvent: (KeyEvent) -> Boolean
-) : ModifierNodeElement<KeyInputInputModifierNodeImpl>() {
-    override fun create(): KeyInputInputModifierNodeImpl {
-        return KeyInputInputModifierNodeImpl(onEvent = null, onPreEvent = onPreviewKeyEvent)
-    }
-
-    override fun update(node: KeyInputInputModifierNodeImpl) = node.apply {
-        onPreEvent = onPreviewKeyEvent
-        onEvent = null
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "onPreviewKeyEvent"
-        properties["onPreviewKeyEvent"] = onPreviewKeyEvent
-    }
-}
-
-internal class KeyInputInputModifierNodeImpl(
-    var onEvent: ((KeyEvent) -> Boolean)?,
-    var onPreEvent: ((KeyEvent) -> Boolean)?
-) : KeyInputModifierNode, Modifier.Node() {
-    override fun onKeyEvent(event: KeyEvent): Boolean = this.onEvent?.invoke(event) ?: false
-    override fun onPreKeyEvent(event: KeyEvent): Boolean = this.onPreEvent?.invoke(event) ?: false
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt
new file mode 100644
index 0000000..31eb488
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftKeyboardInterceptionModifierNode.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.key
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.DelegatableNode
+
+/**
+ * Implement this interface to create a [Modifier.Node] that can intercept hardware Key events
+ * before they are sent to the software keyboard.
+ *
+ * The event is routed to the focused item. Before reaching the focused item,
+ * [onPreInterceptKeyBeforeSoftKeyboard] is called for parents of the focused item.
+ * If the parents don't consume the event, [onPreSoftwareKeyboardKeyEvent]() is
+ * called for the focused item. If the event is still not consumed,
+ * [onInterceptKeyBeforeSoftKeyboard] is called on the focused item's parents.
+ */
+@ExperimentalComposeUiApi
+interface SoftKeyboardInterceptionModifierNode : DelegatableNode {
+    /**
+     * This function is called when a [KeyEvent] is received by this node during the upward
+     * pass. While implementing this callback, return true to stop propagation of this event.
+     * If you return false, the key event will be sent to this
+     * [SoftKeyboardInterceptionModifierNode]'s parent.
+     */
+    fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean
+
+    /**
+     * This function is called when a [KeyEvent] is received by this node during the
+     * downward pass. It gives ancestors of a focused component the chance to intercept an event.
+     * Return true to stop propagation of this event. If you return false, the event will be sent
+     * to this [SoftKeyboardInterceptionModifierNode]'s child. If none of the children consume
+     * the event, it will be sent back up to the root using the [onPreInterceptKeyBeforeSoftKeyboard]
+     * function.
+     */
+    fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt
new file mode 100644
index 0000000..88fc93a
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/key/SoftwareKeyboardInterceptionModifier.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.key
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
+
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events before they are sent to the software keyboard.
+ *
+ * @param onInterceptKeyBeforeSoftKeyboard This callback is invoked when the user interacts with
+ * the hardware keyboard. While implementing this callback, return true to stop propagation of this
+ * event. If you return false, the key event will be sent to this
+ * [SoftKeyboardInterceptionModifierNode]'s parent, and ultimately to the software keyboard.
+ *
+ * @sample androidx.compose.ui.samples.KeyEventSample
+ */
+@ExperimentalComposeUiApi
+fun Modifier.onInterceptKeyBeforeSoftKeyboard(
+    onInterceptKeyBeforeSoftKeyboard: (KeyEvent) -> Boolean
+): Modifier = this then SoftKeyboardInterceptionElement(
+    onKeyEvent = onInterceptKeyBeforeSoftKeyboard,
+    onPreKeyEvent = null
+)
+
+/**
+ * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
+ * allow it to intercept hardware key events before they are sent to the software keyboard. This
+ * modifier is similar to [onInterceptKeyBeforeSoftKeyboard], but allows a parent composable to
+ * intercept the hardware key event before any child.
+ *
+ * @param onPreInterceptKeyBeforeSoftKeyboard This callback is invoked when the user interacts
+ * with the hardware keyboard. It gives ancestors of a focused component the chance to intercept a
+ * [KeyEvent]. Return true to stop propagation of this event. If you return false, the key event
+ * will be sent to this [SoftKeyboardInterceptionModifierNode]'s child. If none of the children
+ * consume the event, it will be sent back up to the root [KeyInputModifierNode] using the
+ * onKeyEvent callback, and ultimately to the software keyboard.
+ *
+ * @sample androidx.compose.ui.samples.KeyEventSample
+ */
+@ExperimentalComposeUiApi
+fun Modifier.onPreInterceptKeyBeforeSoftKeyboard(
+    onPreInterceptKeyBeforeSoftKeyboard: (KeyEvent) -> Boolean,
+): Modifier = this then SoftKeyboardInterceptionElement(
+    onKeyEvent = null,
+    onPreKeyEvent = onPreInterceptKeyBeforeSoftKeyboard
+)
+
+private data class SoftKeyboardInterceptionElement(
+    val onKeyEvent: ((KeyEvent) -> Boolean)?,
+    val onPreKeyEvent: ((KeyEvent) -> Boolean)?
+) : ModifierNodeElement<InterceptedKeyInputModifierNodeImpl>() {
+    override fun create() = InterceptedKeyInputModifierNodeImpl(
+        onEvent = onKeyEvent,
+        onPreEvent = onPreKeyEvent
+    )
+
+    override fun update(node: InterceptedKeyInputModifierNodeImpl) = node.apply {
+        onEvent = onKeyEvent
+        onPreEvent = onPreKeyEvent
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        onKeyEvent?.let {
+            name = "onKeyToSoftKeyboardInterceptedEvent"
+            properties["onKeyToSoftKeyboardInterceptedEvent"] = it
+        }
+        onPreKeyEvent?.let {
+            name = "onPreKeyToSoftKeyboardInterceptedEvent"
+            properties["onPreKeyToSoftKeyboardInterceptedEvent"] = it
+        }
+    }
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private class InterceptedKeyInputModifierNodeImpl(
+    var onEvent: ((KeyEvent) -> Boolean)?,
+    var onPreEvent: ((KeyEvent) -> Boolean)?
+) : SoftKeyboardInterceptionModifierNode, Modifier.Node() {
+    override fun onInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean =
+        onEvent?.invoke(event) ?: false
+    override fun onPreInterceptKeyBeforeSoftKeyboard(event: KeyEvent): Boolean =
+        onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
index 076a965..6b2351c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifier.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.input.rotary
 
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
 
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -39,7 +41,10 @@
  */
 fun Modifier.onRotaryScrollEvent(
     onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = this then OnRotaryScrollEventElement(onRotaryScrollEvent)
+): Modifier = this then OnRotaryScrollEventElement(
+    onRotaryScrollEvent = onRotaryScrollEvent,
+    onPreRotaryScrollEvent = null
+)
 
 /**
  * Adding this [modifier][Modifier] to the [modifier][Modifier] parameter of a component will
@@ -64,4 +69,43 @@
  */
 fun Modifier.onPreRotaryScrollEvent(
     onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-): Modifier = this then OnPreRotaryScrollEventElement(onPreRotaryScrollEvent)
+): Modifier = this then OnRotaryScrollEventElement(
+    onRotaryScrollEvent = null,
+    onPreRotaryScrollEvent = onPreRotaryScrollEvent
+)
+
+private data class OnRotaryScrollEventElement(
+    val onRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?,
+    val onPreRotaryScrollEvent: ((RotaryScrollEvent) -> Boolean)?
+) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
+    override fun create() = RotaryInputModifierNodeImpl(
+        onEvent = onRotaryScrollEvent,
+        onPreEvent = onPreRotaryScrollEvent
+    )
+
+    override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
+        onEvent = onRotaryScrollEvent
+        onPreEvent = onPreRotaryScrollEvent
+    }
+
+    override fun InspectorInfo.inspectableProperties() {
+        onRotaryScrollEvent?.let {
+            name = "onRotaryScrollEvent"
+            properties["onRotaryScrollEvent"] = it
+        }
+        onPreRotaryScrollEvent?.let {
+            name = "onPreRotaryScrollEvent"
+            properties["onPreRotaryScrollEvent"] = it
+        }
+    }
+}
+
+private class RotaryInputModifierNodeImpl(
+    var onEvent: ((RotaryScrollEvent) -> Boolean)?,
+    var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
+) : RotaryInputModifierNode, Modifier.Node() {
+    override fun onRotaryScrollEvent(event: RotaryScrollEvent) =
+        onEvent?.invoke(event) ?: false
+    override fun onPreRotaryScrollEvent(event: RotaryScrollEvent) =
+        onPreEvent?.invoke(event) ?: false
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
index 05f8d64..d59566c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/rotary/RotaryInputModifierNode.kt
@@ -16,11 +16,8 @@
 
 package androidx.compose.ui.input.rotary
 
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.node.DelegatableNode
-import androidx.compose.ui.node.ModifierNodeElement
-import androidx.compose.ui.platform.InspectorInfo
 
 /**
  * Implement this interface to create a [Modifier.Node] that can intercept rotary scroll events.
@@ -30,7 +27,6 @@
  * consume the event, [onPreRotaryScrollEvent]() is called for the focused item. If the event is
  * still not consumed, [onRotaryScrollEvent]() is called on the focused item's parents.
  */
-@ExperimentalComposeUiApi
 interface RotaryInputModifierNode : DelegatableNode {
     /**
      * This function is called when a [RotaryScrollEvent] is received by this node during the upward
@@ -48,56 +44,3 @@
      */
     fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean
 }
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal data class OnRotaryScrollEventElement(
-    val onRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
-    override fun create() = RotaryInputModifierNodeImpl(
-        onEvent = onRotaryScrollEvent,
-        onPreEvent = null
-    )
-
-    override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
-        onEvent = onRotaryScrollEvent
-        onPreEvent = null
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "onRotaryScrollEvent"
-        properties["onRotaryScrollEvent"] = onRotaryScrollEvent
-    }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal data class OnPreRotaryScrollEventElement(
-    val onPreRotaryScrollEvent: (RotaryScrollEvent) -> Boolean
-) : ModifierNodeElement<RotaryInputModifierNodeImpl>() {
-    override fun create() = RotaryInputModifierNodeImpl(
-        onEvent = null,
-        onPreEvent = onPreRotaryScrollEvent
-    )
-
-    override fun update(node: RotaryInputModifierNodeImpl) = node.apply {
-        onPreEvent = onPreRotaryScrollEvent
-        onEvent = null
-    }
-
-    override fun InspectorInfo.inspectableProperties() {
-        name = "onPreRotaryScrollEvent"
-        properties["onPreRotaryScrollEvent"] = onPreRotaryScrollEvent
-    }
-}
-
-@OptIn(ExperimentalComposeUiApi::class)
-internal class RotaryInputModifierNodeImpl(
-    var onEvent: ((RotaryScrollEvent) -> Boolean)?,
-    var onPreEvent: ((RotaryScrollEvent) -> Boolean)?
-) : RotaryInputModifierNode, Modifier.Node() {
-    override fun onRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
-        return onEvent?.invoke(event) ?: false
-    }
-    override fun onPreRotaryScrollEvent(event: RotaryScrollEvent): Boolean {
-        return onPreEvent?.invoke(event) ?: false
-    }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
index b2e69f4..51e1fb6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerNodeCoordinator.kt
@@ -106,7 +106,7 @@
             measure(layoutNode.childMeasurables, constraints)
         }
         onMeasured()
-        return this
+        this
     }
 
     override fun minIntrinsicWidth(height: Int) =
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 cceff4d..89da2e1 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
@@ -281,12 +281,10 @@
 
     protected inline fun performingMeasure(
         constraints: Constraints,
-        block: () -> Placeable
+        crossinline block: () -> Placeable
     ): Placeable {
         measurementConstraints = constraints
-        val result = block()
-        layer?.resize(measuredSize)
-        return result
+        return block()
     }
 
     fun onMeasured() {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index b451d40..2d857bd 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.focus.FocusPropertiesModifierNode
 import androidx.compose.ui.focus.FocusTargetModifierNode
 import androidx.compose.ui.input.key.KeyInputModifierNode
+import androidx.compose.ui.input.key.SoftKeyboardInterceptionModifierNode
 import androidx.compose.ui.input.pointer.PointerInputModifier
 import androidx.compose.ui.input.pointer.SuspendPointerInputModifierNode
 import androidx.compose.ui.input.rotary.RotaryInputModifierNode
@@ -55,7 +56,6 @@
 // its own measureNode if the measureNode happens to implement LayoutAware. If the measureNode
 // implements any other node interfaces, such as draw, those should be visited by the coordinator
 // below them.
-@OptIn(ExperimentalComposeUiApi::class)
 internal val NodeKind<*>.includeSelfInTraversal: Boolean
     get() = mask and Nodes.LayoutAware.mask != 0
 
@@ -99,10 +99,12 @@
         get() = NodeKind<CompositionLocalConsumerModifierNode>(0b1 shl 15)
     @JvmStatic
     inline val SuspendPointerInput get() = NodeKind<SuspendPointerInputModifierNode>(0b1 shl 16)
+    @JvmStatic
+    inline val SoftKeyboardKeyInput
+        get() = NodeKind<SoftKeyboardInterceptionModifierNode>(0b1 shl 17)
     // ...
 }
 
-@OptIn(ExperimentalComposeUiApi::class)
 internal fun calculateNodeKindSetFrom(element: Modifier.Element): Int {
     var mask = Nodes.Any.mask
     if (element is LayoutModifier) {
@@ -195,7 +197,9 @@
     if (node is SuspendPointerInputModifierNode) {
         mask = mask or SuspendPointerInput
     }
-
+    if (node is SoftKeyboardInterceptionModifierNode) {
+        mask = mask or Nodes.SoftKeyboardKeyInput
+    }
     return mask
 }
 
@@ -203,16 +207,12 @@
 private const val Inserted = 1
 private const val Removed = 2
 
-@OptIn(ExperimentalComposeUiApi::class)
 internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)
 
-@OptIn(ExperimentalComposeUiApi::class)
 internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)
 
-@OptIn(ExperimentalComposeUiApi::class)
 internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
 
-@OptIn(ExperimentalComposeUiApi::class)
 private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
     check(node.isAttached)
     if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
diff --git a/core/core-ktx/build.gradle b/core/core-ktx/build.gradle
index 21bb1d2..b8c9576 100644
--- a/core/core-ktx/build.gradle
+++ b/core/core-ktx/build.gradle
@@ -7,7 +7,7 @@
 }
 
 dependencies {
-    // Atomic group
+    // Atomically versioned.
     constraints {
         implementation(project(":core:core"))
     }
diff --git a/core/core/build.gradle b/core/core/build.gradle
index b9743ef..ecf22c1 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -8,7 +8,7 @@
 }
 
 dependencies {
-    // Atomic group
+    // Atomically versioned.
     constraints {
         implementation(project(":core:core-ktx"))
     }
diff --git a/core/uwb/uwb-rxjava3/build.gradle b/core/uwb/uwb-rxjava3/build.gradle
index 9c1379f..95d0b38 100644
--- a/core/uwb/uwb-rxjava3/build.gradle
+++ b/core/uwb/uwb-rxjava3/build.gradle
@@ -36,7 +36,7 @@
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation('com.google.android.gms:play-services-base:18.0.1')
-    androidTestImplementation('com.google.android.gms:play-services-nearby:18.3.0', {
+    androidTestImplementation('com.google.android.gms:play-services-nearby:18.5.0', {
         exclude group: "androidx.core"
     })
     androidTestImplementation(libs.multidex)
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
index 2862833..522e730 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClient.kt
@@ -46,7 +46,7 @@
     private var startedRanging = false
     companion object {
         val rangingPosition = RangingPosition(
-            RangingMeasurement(1, 1.0F), null, null, 20)
+            RangingMeasurement(1, 1.0F), null, null, 20, -50)
     }
     override fun getApiKey(): ApiKey<zze> {
         TODO("Not yet implemented")
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
index a46f50e..6c2a4fb 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbClientSessionScope.kt
@@ -63,7 +63,6 @@
             .setSessionId(defaultRangingParameters.sessionId)
             .setUwbConfigId(configId)
             .setRangingUpdateRate(updateRate)
-            .setSessionKeyInfo(defaultRangingParameters.sessionKeyInfo)
         parametersBuilder.addPeerDevice(UwbDevice.createForAddress(uwbDevice.address.address))
         val callback =
             object : RangingSessionCallback {
diff --git a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
index 74d6b6e..d4769cc 100644
--- a/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
+++ b/core/uwb/uwb-rxjava3/src/androidTest/java/androidx/core/uwb/rxjava3/mock/TestUwbManager.kt
@@ -51,7 +51,8 @@
         val localAddress = com.google.android.gms.nearby.uwb.UwbAddress(DEVICE_ADDRESS)
 
         val rangingCapabilities =
-            com.google.android.gms.nearby.uwb.RangingCapabilities(true, false, false, 200)
+            com.google.android.gms.nearby.uwb.RangingCapabilities(true, false, false, 200,
+                listOf(9), listOf(1, 2, 3), 2F)
         val uwbClient = TestUwbClient(complexChannel, localAddress, rangingCapabilities, true)
         return if (isController) {
              TestUwbControllerSessionScope(
diff --git a/core/uwb/uwb/build.gradle b/core/uwb/uwb/build.gradle
index 1c781ab..8a23173 100644
--- a/core/uwb/uwb/build.gradle
+++ b/core/uwb/uwb/build.gradle
@@ -30,7 +30,7 @@
     implementation(libs.guavaAndroid)
     implementation('com.google.android.gms:play-services-base:18.0.1')
     implementation(libs.kotlinCoroutinesPlayServices)
-    implementation('com.google.android.gms:play-services-nearby:18.3.0', {
+    implementation('com.google.android.gms:play-services-nearby:18.5.0', {
         exclude group: "androidx.core"
     })
 
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
index ce0dab8..3290b14 100644
--- a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/common/TestCommons.kt
@@ -29,7 +29,8 @@
             .setChannel(10)
             .build()
         val LOCAL_ADDRESS = UwbAddress(byteArrayOf(0xB0.toByte()))
-        val RANGING_CAPABILITIES = RangingCapabilities(true, false, false, 200)
+        val RANGING_CAPABILITIES = RangingCapabilities(true, false, false,
+            200, listOf(9), listOf(1, 2, 3), 2F)
         val NEIGHBOR_1 = byteArrayOf(0xA1.toByte())
         val NEIGHBOR_2 = byteArrayOf(0xA5.toByte())
         val UWB_DEVICE = UwbDevice.createForAddress(NEIGHBOR_1)
diff --git a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
index c3f90ad..db0ff5b 100644
--- a/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
+++ b/core/uwb/uwb/src/androidTest/java/androidx/core/uwb/mock/TestUwbClient.kt
@@ -48,7 +48,7 @@
     private var startedRanging = false
     companion object {
         val rangingPosition = RangingPosition(
-            RangingMeasurement(1, 1.0F), null, null, 20)
+            RangingMeasurement(1, 1.0F), null, null, 20, -50)
     }
     override fun getApiKey(): ApiKey<zze> {
         TODO("Not yet implemented")
diff --git a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
index fa19f31..86c1559 100644
--- a/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
+++ b/core/uwb/uwb/src/main/java/androidx/core/uwb/impl/UwbClientSessionScopeImpl.kt
@@ -79,7 +79,6 @@
             .setSessionId(parameters.sessionId)
             .setUwbConfigId(configId)
             .setRangingUpdateRate(updateRate)
-            .setSessionKeyInfo(parameters.sessionKeyInfo)
             .setComplexChannel(
                 parameters.complexChannel?.let {
                     UwbComplexChannel.Builder()
@@ -87,6 +86,9 @@
                         .setPreambleIndex(it.preambleIndex)
                         .build()
                 })
+        if (parameters.sessionKeyInfo != null) {
+            parametersBuilder.setSessionKeyInfo(parameters.sessionKeyInfo)
+        }
         for (peer in parameters.peerDevices) {
             parametersBuilder.addPeerDevice(UwbDevice.createForAddress(peer.address.address))
         }
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index e099b3b..7978fe2 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -181,6 +181,7 @@
 WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//Stepper/\#kotlin\.Float\#kotlin\.Function[0-9]+\[kotlin\.Float,kotlin\.Unit\]\#kotlin\.Int\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#kotlin\.ranges\.ClosedFloatingPointRange\[kotlin\.Float\]\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.BoxScope,kotlin\.Unit\]/PointingToDeclaration/
 WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//Stepper/\#kotlin\.Int\#kotlin\.Function[0-9]+\[kotlin\.Int,kotlin\.Unit\]\#kotlin\.ranges\.IntProgression\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.BoxScope,kotlin\.Unit\]/PointingToDeclaration/
 WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material//TitleCard/\#kotlin\.Function[0-9]+\[kotlin\.Unit\]\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.RowScope,kotlin\.Unit\]\#androidx\.compose\.ui\.Modifier\#kotlin\.Boolean\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.RowScope,kotlin\.Unit\]\?\#androidx\.compose\.ui\.graphics\.painter\.Painter\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color\#kotlin\.Function[0-9]+\[androidx\.compose\.foundation\.layout\.ColumnScope,kotlin\.Unit\]/PointingToDeclaration/
+WARN: Missing @param tag for parameter `content` of function androidx\.wear\.compose\.material[0-9]+//MaterialTheme/\#androidx\.wear\.compose\.material[0-9]+\.ColorScheme\#androidx\.wear\.compose\.material[0-9]+\.Typography\#androidx\.wear\.compose\.material[0-9]+\.Shapes\#kotlin\.Function[0-9]+\[kotlin\.Unit\]/PointingToDeclaration/
 WARN: Unable to find what is referred to by
 # > Task :docs-tip-of-tree:dackkaDocs
 WARN\: Failed to resolve \`\@see SplitAttributes\.shouldExpandSecondaryContainer\`\!
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 67b8d16..3fb9fc1 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -30,24 +30,24 @@
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.2.0-beta01")
-    docs("androidx.benchmark:benchmark-common:1.2.0-alpha11")
-    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha11")
-    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha11")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha11")
+    docs("androidx.benchmark:benchmark-common:1.2.0-alpha12")
+    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha12")
+    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha12")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha12")
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
     docs("androidx.browser:browser:1.5.0")
-    docs("androidx.camera:camera-camera2:1.3.0-alpha04")
-    docs("androidx.camera:camera-core:1.3.0-alpha04")
-    docs("androidx.camera:camera-extensions:1.3.0-alpha04")
+    docs("androidx.camera:camera-camera2:1.3.0-alpha05")
+    docs("androidx.camera:camera-core:1.3.0-alpha05")
+    docs("androidx.camera:camera-extensions:1.3.0-alpha05")
     stubs(fileTree(dir: "../camera/camera-extensions-stub", include: ["camera-extensions-stub.jar"]))
-    docs("androidx.camera:camera-lifecycle:1.3.0-alpha04")
-    docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha04")
+    docs("androidx.camera:camera-lifecycle:1.3.0-alpha05")
+    docs("androidx.camera:camera-mlkit-vision:1.3.0-alpha05")
     docs("androidx.camera:camera-previewview:1.1.0-beta02")
-    docs("androidx.camera:camera-video:1.3.0-alpha04")
-    docs("androidx.camera:camera-view:1.3.0-alpha04")
-    docs("androidx.camera:camera-viewfinder:1.3.0-alpha04")
+    docs("androidx.camera:camera-video:1.3.0-alpha05")
+    docs("androidx.camera:camera-view:1.3.0-alpha05")
+    docs("androidx.camera:camera-viewfinder:1.3.0-alpha05")
     docs("androidx.car.app:app:1.4.0-alpha01")
     docs("androidx.car.app:app-automotive:1.4.0-alpha01")
     docs("androidx.car.app:app-projected:1.4.0-alpha01")
@@ -55,60 +55,60 @@
     docs("androidx.cardview:cardview:1.0.0")
     docs("androidx.collection:collection:1.3.0-alpha02")
     docs("androidx.collection:collection-ktx:1.3.0-alpha02")
-    docs("androidx.compose.animation:animation:1.4.0-rc01")
-    docs("androidx.compose.animation:animation-core:1.4.0-rc01")
-    docs("androidx.compose.animation:animation-graphics:1.4.0-rc01")
-    samples("androidx.compose.animation:animation-samples:1.4.0-rc01")
-    samples("androidx.compose.animation:animation-core-samples:1.4.0-rc01")
-    samples("androidx.compose.animation:animation-graphics-samples:1.4.0-rc01")
-    docs("androidx.compose.foundation:foundation:1.4.0-rc01")
-    docs("androidx.compose.foundation:foundation-layout:1.4.0-rc01")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.4.0-rc01")
-    samples("androidx.compose.foundation:foundation-samples:1.4.0-rc01")
-    docs("androidx.compose.material3:material3:1.1.0-alpha08")
-    samples("androidx.compose.material3:material3-samples:1.1.0-alpha08")
-    docs("androidx.compose.material3:material3-window-size-class:1.1.0-alpha08")
-    samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-alpha08")
-    docs("androidx.compose.material:material:1.4.0-rc01")
-    docs("androidx.compose.material:material-icons-core:1.4.0-rc01")
-    samples("androidx.compose.material:material-icons-core-samples:1.4.0-rc01")
-    docs("androidx.compose.material:material-ripple:1.4.0-rc01")
-    samples("androidx.compose.material:material-samples:1.4.0-rc01")
-    docs("androidx.compose.runtime:runtime:1.4.0-rc01")
-    docs("androidx.compose.runtime:runtime-livedata:1.4.0-rc01")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.4.0-rc01")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.4.0-rc01")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.4.0-rc01")
-    docs("androidx.compose.runtime:runtime-rxjava3:1.4.0-rc01")
-    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.4.0-rc01")
-    docs("androidx.compose.runtime:runtime-saveable:1.4.0-rc01")
-    samples("androidx.compose.runtime:runtime-saveable-samples:1.4.0-rc01")
-    samples("androidx.compose.runtime:runtime-samples:1.4.0-rc01")
+    docs("androidx.compose.animation:animation:1.5.0-alpha01")
+    docs("androidx.compose.animation:animation-core:1.5.0-alpha01")
+    docs("androidx.compose.animation:animation-graphics:1.5.0-alpha01")
+    samples("androidx.compose.animation:animation-samples:1.5.0-alpha01")
+    samples("androidx.compose.animation:animation-core-samples:1.5.0-alpha01")
+    samples("androidx.compose.animation:animation-graphics-samples:1.5.0-alpha01")
+    docs("androidx.compose.foundation:foundation:1.5.0-alpha01")
+    docs("androidx.compose.foundation:foundation-layout:1.5.0-alpha01")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.5.0-alpha01")
+    samples("androidx.compose.foundation:foundation-samples:1.5.0-alpha01")
+    docs("androidx.compose.material3:material3:1.1.0-beta01")
+    samples("androidx.compose.material3:material3-samples:1.1.0-beta01")
+    docs("androidx.compose.material3:material3-window-size-class:1.1.0-beta01")
+    samples("androidx.compose.material3:material3-window-size-class-samples:1.1.0-beta01")
+    docs("androidx.compose.material:material:1.5.0-alpha01")
+    docs("androidx.compose.material:material-icons-core:1.5.0-alpha01")
+    samples("androidx.compose.material:material-icons-core-samples:1.5.0-alpha01")
+    docs("androidx.compose.material:material-ripple:1.5.0-alpha01")
+    samples("androidx.compose.material:material-samples:1.5.0-alpha01")
+    docs("androidx.compose.runtime:runtime:1.5.0-alpha01")
+    docs("androidx.compose.runtime:runtime-livedata:1.5.0-alpha01")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.5.0-alpha01")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.5.0-alpha01")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.5.0-alpha01")
+    docs("androidx.compose.runtime:runtime-rxjava3:1.5.0-alpha01")
+    samples("androidx.compose.runtime:runtime-rxjava3-samples:1.5.0-alpha01")
+    docs("androidx.compose.runtime:runtime-saveable:1.5.0-alpha01")
+    samples("androidx.compose.runtime:runtime-saveable-samples:1.5.0-alpha01")
+    samples("androidx.compose.runtime:runtime-samples:1.5.0-alpha01")
     docs("androidx.compose.runtime:runtime-tracing:1.0.0-alpha03")
-    docs("androidx.compose.ui:ui:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-geometry:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-graphics:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-graphics-samples:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-test:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-test-junit4:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-test-samples:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-text:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-text-google-fonts:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-text-samples:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-tooling:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-tooling-data:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-tooling-preview:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-unit:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-unit-samples:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-util:1.4.0-rc01")
-    docs("androidx.compose.ui:ui-viewbinding:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.4.0-rc01")
-    samples("androidx.compose.ui:ui-samples:1.4.0-rc01")
+    docs("androidx.compose.ui:ui:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-geometry:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-graphics:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-graphics-samples:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-test:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-test-junit4:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-test-samples:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-text:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-text-google-fonts:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-text-samples:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-tooling:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-tooling-data:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-tooling-preview:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-unit:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-unit-samples:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-util:1.5.0-alpha01")
+    docs("androidx.compose.ui:ui-viewbinding:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.5.0-alpha01")
+    samples("androidx.compose.ui:ui-samples:1.5.0-alpha01")
     docs("androidx.concurrent:concurrent-futures:1.2.0-alpha01")
     docs("androidx.concurrent:concurrent-futures-ktx:1.2.0-alpha01")
-    docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha08")
-    docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha08")
-    docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha08")
+    docs("androidx.constraintlayout:constraintlayout:2.2.0-alpha09")
+    docs("androidx.constraintlayout:constraintlayout-compose:1.1.0-alpha09")
+    docs("androidx.constraintlayout:constraintlayout-core:1.1.0-alpha09")
     docs("androidx.contentpager:contentpager:1.0.0")
     docs("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
     docs("androidx.core.uwb:uwb:1.0.0-alpha04")
@@ -140,23 +140,23 @@
     docs("androidx.datastore:datastore-rxjava3:1.1.0-alpha01")
     docs("androidx.documentfile:documentfile:1.1.0-alpha01")
     docs("androidx.draganddrop:draganddrop:1.0.0")
-    docs("androidx.drawerlayout:drawerlayout:1.2.0-rc01")
+    docs("androidx.drawerlayout:drawerlayout:1.2.0")
     docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
     docs("androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03")
-    docs("androidx.emoji2:emoji2:1.3.0-rc01")
-    docs("androidx.emoji2:emoji2-bundled:1.3.0-rc01")
+    docs("androidx.emoji2:emoji2:1.4.0-alpha01")
+    docs("androidx.emoji2:emoji2-bundled:1.4.0-alpha01")
     docs("androidx.emoji2:emoji2-emojipicker:1.0.0-alpha03")
-    docs("androidx.emoji2:emoji2-views:1.3.0-rc01")
-    docs("androidx.emoji2:emoji2-views-helper:1.3.0-rc01")
+    docs("androidx.emoji2:emoji2-views:1.4.0-alpha01")
+    docs("androidx.emoji2:emoji2-views-helper:1.4.0-alpha01")
     docs("androidx.emoji:emoji:1.2.0-alpha03")
     docs("androidx.emoji:emoji-appcompat:1.2.0-alpha03")
     docs("androidx.emoji:emoji-bundled:1.2.0-alpha03")
     docs("androidx.enterprise:enterprise-feedback:1.1.0")
     docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
     docs("androidx.exifinterface:exifinterface:1.3.6")
-    docs("androidx.fragment:fragment:1.6.0-alpha07")
-    docs("androidx.fragment:fragment-ktx:1.6.0-alpha07")
-    docs("androidx.fragment:fragment-testing:1.6.0-alpha07")
+    docs("androidx.fragment:fragment:1.6.0-alpha08")
+    docs("androidx.fragment:fragment-ktx:1.6.0-alpha08")
+    docs("androidx.fragment:fragment-testing:1.6.0-alpha08")
     docs("androidx.glance:glance:1.0.0-alpha05")
     docs("androidx.glance:glance-appwidget:1.0.0-alpha05")
     docs("androidx.glance:glance-appwidget-preview:1.0.0-alpha05")
@@ -164,8 +164,8 @@
     docs("androidx.glance:glance-preview:1.0.0-alpha05")
     docs("androidx.glance:glance-wear-tiles:1.0.0-alpha05")
     docs("androidx.glance:glance-wear-tiles-preview:1.0.0-alpha05")
-    docs("androidx.graphics:graphics-core:1.0.0-alpha02")
-    docs("androidx.gridlayout:gridlayout:1.0.0")
+    docs("androidx.graphics:graphics-core:1.0.0-alpha03")
+    docs("androidx.gridlayout:gridlayout:1.1.0-alpha01")
     docs("androidx.health.connect:connect-client:1.0.0-alpha11")
     samples("androidx.health.connect:connect-client-samples:1.0.0-alpha11")
     docs("androidx.health:health-services-client:1.0.0-beta02")
@@ -176,7 +176,7 @@
     samples("androidx.hilt:hilt-navigation-compose-samples:1.1.0-alpha01")
     docs("androidx.hilt:hilt-navigation-fragment:1.0.0-beta01")
     docs("androidx.hilt:hilt-work:1.0.0-beta01")
-    docs("androidx.input:input-motionprediction:1.0.0-alpha02")
+    docs("androidx.input:input-motionprediction:1.0.0-beta01")
     docs("androidx.interpolator:interpolator:1.0.0")
     docs("androidx.javascriptengine:javascriptengine:1.0.0-alpha04")
     docs("androidx.leanback:leanback:1.2.0-alpha02")
@@ -238,19 +238,19 @@
     docs("androidx.mediarouter:mediarouter:1.6.0-alpha02")
     docs("androidx.mediarouter:mediarouter-testing:1.6.0-alpha02")
     docs("androidx.metrics:metrics-performance:1.0.0-alpha03")
-    docs("androidx.navigation:navigation-common:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-compose:2.6.0-alpha07")
-    samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-fragment:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-runtime:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-testing:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-ui:2.6.0-alpha07")
-    docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha07")
+    docs("androidx.navigation:navigation-common:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-common-ktx:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-compose:2.6.0-alpha08")
+    samples("androidx.navigation:navigation-compose-samples:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-dynamic-features-fragment:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-dynamic-features-runtime:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-fragment:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-fragment-ktx:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-runtime:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-runtime-ktx:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-testing:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-ui:2.6.0-alpha08")
+    docs("androidx.navigation:navigation-ui-ktx:2.6.0-alpha08")
     docs("androidx.paging:paging-common:3.2.0-alpha04")
     docs("androidx.paging:paging-common-ktx:3.2.0-alpha04")
     docs("androidx.paging:paging-compose:1.0.0-alpha18")
@@ -269,15 +269,15 @@
     docs("androidx.preference:preference:1.2.0")
     docs("androidx.preference:preference-ktx:1.2.0")
     docs("androidx.print:print:1.1.0-beta01")
-    docs("androidx.privacysandbox.ads:ads-adservices:1.0.0-beta01")
-    docs("androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta01")
+    docs("androidx.privacysandbox.ads:ads-adservices:1.0.0-beta02")
+    docs("androidx.privacysandbox.ads:ads-adservices-java:1.0.0-beta02")
     docs("androidx.privacysandbox.tools:tools:1.0.0-alpha03")
     docs("androidx.privacysandbox.tools:tools-apigenerator:1.0.0-alpha03")
     docs("androidx.privacysandbox.tools:tools-apipackager:1.0.0-alpha03")
     docs("androidx.privacysandbox.tools:tools-core:1.0.0-alpha03")
-    docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha01")
-    docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha01")
-    docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha01")
+    docs("androidx.privacysandbox.ui:ui-client:1.0.0-alpha02")
+    docs("androidx.privacysandbox.ui:ui-core:1.0.0-alpha02")
+    docs("androidx.privacysandbox.ui:ui-provider:1.0.0-alpha02")
     docs("androidx.profileinstaller:profileinstaller:1.3.0")
     docs("androidx.recommendation:recommendation:1.0.0")
     docs("androidx.recyclerview:recyclerview:1.3.0")
@@ -285,18 +285,18 @@
     docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
     docs("androidx.resourceinspection:resourceinspection-annotation:1.0.1")
     docs("androidx.resourceinspection:resourceinspection-processor:1.0.1")
-    docs("androidx.room:room-common:2.5.0")
-    docs("androidx.room:room-guava:2.5.0")
-    docs("androidx.room:room-ktx:2.5.0")
-    docs("androidx.room:room-migration:2.5.0")
-    docs("androidx.room:room-paging:2.5.0")
-    docs("androidx.room:room-paging-guava:2.5.0")
-    docs("androidx.room:room-paging-rxjava2:2.5.0")
-    docs("androidx.room:room-paging-rxjava3:2.5.0")
-    docs("androidx.room:room-runtime:2.5.0")
-    docs("androidx.room:room-rxjava2:2.5.0")
-    docs("androidx.room:room-rxjava3:2.5.0")
-    docs("androidx.room:room-testing:2.5.0")
+    docs("androidx.room:room-common:2.6.0-alpha01")
+    docs("androidx.room:room-guava:2.6.0-alpha01")
+    docs("androidx.room:room-ktx:2.6.0-alpha01")
+    docs("androidx.room:room-migration:2.6.0-alpha01")
+    docs("androidx.room:room-paging:2.6.0-alpha01")
+    docs("androidx.room:room-paging-guava:2.6.0-alpha01")
+    docs("androidx.room:room-paging-rxjava2:2.6.0-alpha01")
+    docs("androidx.room:room-paging-rxjava3:2.6.0-alpha01")
+    docs("androidx.room:room-runtime:2.6.0-alpha01")
+    docs("androidx.room:room-rxjava2:2.6.0-alpha01")
+    docs("androidx.room:room-rxjava3:2.6.0-alpha01")
+    docs("androidx.room:room-testing:2.6.0-alpha01")
     docs("androidx.savedstate:savedstate:1.2.1")
     docs("androidx.savedstate:savedstate-ktx:1.2.1")
     docs("androidx.security:security-app-authenticator:1.0.0-alpha02")
@@ -310,9 +310,9 @@
     docs("androidx.slice:slice-core:1.1.0-alpha02")
     docs("androidx.slice:slice-view:1.1.0-alpha02")
     docs("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
-    docs("androidx.sqlite:sqlite:2.3.0")
-    docs("androidx.sqlite:sqlite-framework:2.3.0")
-    docs("androidx.sqlite:sqlite-ktx:2.3.0")
+    docs("androidx.sqlite:sqlite:2.4.0-alpha01")
+    docs("androidx.sqlite:sqlite-framework:2.4.0-alpha01")
+    docs("androidx.sqlite:sqlite-ktx:2.4.0-alpha01")
     docs("androidx.startup:startup-runtime:1.2.0-alpha02")
     docs("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
     docs("androidx.test:core:1.5.0")
@@ -336,15 +336,15 @@
     docs("androidx.test.services:storage:1.4.2")
     docs("androidx.test.uiautomator:uiautomator:2.3.0-alpha02")
     docs("androidx.textclassifier:textclassifier:1.0.0-alpha04")
-    docs("androidx.tracing:tracing:1.2.0-beta01")
-    docs("androidx.tracing:tracing-ktx:1.2.0-beta01")
-    docs("androidx.tracing:tracing-perfetto:1.0.0-alpha12")
-    docs("androidx.tracing:tracing-perfetto-common:1.0.0-alpha12")
+    docs("androidx.tracing:tracing:1.2.0-beta02")
+    docs("androidx.tracing:tracing-ktx:1.2.0-beta02")
+    docs("androidx.tracing:tracing-perfetto:1.0.0-alpha13")
+    docs("androidx.tracing:tracing-perfetto-common:1.0.0-alpha13")
     docs("androidx.transition:transition:1.4.1")
     docs("androidx.transition:transition-ktx:1.4.1")
-    docs("androidx.tv:tv-foundation:1.0.0-alpha04")
-    docs("androidx.tv:tv-material:1.0.0-alpha04")
-    samples("androidx.tv:tv-samples:1.0.0-alpha04")
+    docs("androidx.tv:tv-foundation:1.0.0-alpha05")
+    docs("androidx.tv:tv-material:1.0.0-alpha05")
+    samples("androidx.tv:tv-samples:1.0.0-alpha05")
     docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
     docs("androidx.vectordrawable:vectordrawable:1.2.0-beta01")
     docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -352,25 +352,27 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-beta01")
     docs("androidx.viewpager:viewpager:1.1.0-alpha01")
-    docs("androidx.wear.compose:compose-foundation:1.2.0-alpha06")
-    samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha06")
-    docs("androidx.wear.compose:compose-material:1.2.0-alpha06")
-    docs("androidx.wear.compose:compose-material-core:1.2.0-alpha06")
-    samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha06")
+    docs("androidx.wear.compose:compose-foundation:1.2.0-alpha07")
+    samples("androidx.wear.compose:compose-foundation-samples:1.2.0-alpha07")
+    docs("androidx.wear.compose:compose-material:1.2.0-alpha07")
+    docs("androidx.wear.compose:compose-material-core:1.2.0-alpha07")
+    samples("androidx.wear.compose:compose-material-samples:1.2.0-alpha07")
     docs("androidx.wear.compose:compose-material3:1.0.0-alpha01")
-    samples("androidx.wear.compose:compose-material3-samples:1.2.0-alpha06")
-    docs("androidx.wear.compose:compose-navigation:1.2.0-alpha06")
-    samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha06")
-    docs("androidx.wear.protolayout:protolayout:1.0.0-alpha05")
-    docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha05")
-    docs("androidx.wear.protolayout:protolayout-material:1.0.0-alpha05")
-    docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha05")
-    docs("androidx.wear.protolayout:protolayout-renderer:1.0.0-alpha05")
-    docs("androidx.wear.tiles:tiles:1.2.0-alpha01")
-    docs("androidx.wear.tiles:tiles-material:1.2.0-alpha01")
-    docs("androidx.wear.tiles:tiles-proto:1.2.0-alpha01")
-    docs("androidx.wear.tiles:tiles-renderer:1.2.0-alpha01")
-    docs("androidx.wear.tiles:tiles-testing:1.2.0-alpha01")
+    samples("androidx.wear.compose:compose-material3-samples:1.2.0-alpha07")
+    docs("androidx.wear.compose:compose-navigation:1.2.0-alpha07")
+    samples("androidx.wear.compose:compose-navigation-samples:1.2.0-alpha07")
+    docs("androidx.wear.compose:compose-ui-tooling:1.2.0-alpha07")
+    docs("androidx.wear.protolayout:protolayout:1.0.0-alpha06")
+    docs("androidx.wear.protolayout:protolayout-expression:1.0.0-alpha06")
+    docs("androidx.wear.protolayout:protolayout-material:1.0.0-alpha06")
+    docs("androidx.wear.protolayout:protolayout-proto:1.0.0-alpha06")
+    docs("androidx.wear.protolayout:protolayout-renderer:1.0.0-alpha06")
+    docs("androidx.wear.tiles:tiles:1.2.0-alpha02")
+    docs("androidx.wear.tiles:tiles-material:1.2.0-alpha02")
+    docs("androidx.wear.tiles:tiles-proto:1.2.0-alpha02")
+    docs("androidx.wear.tiles:tiles-renderer:1.2.0-alpha02")
+    docs("androidx.wear.tiles:tiles-testing:1.2.0-alpha02")
+    docs("androidx.wear.tiles:tiles-tooling:1.2.0-alpha02")
     docs("androidx.wear.watchface:watchface:1.2.0-alpha07")
     docs("androidx.wear.watchface:watchface-client:1.2.0-alpha07")
     docs("androidx.wear.watchface:watchface-client-guava:1.2.0-alpha07")
@@ -395,7 +397,7 @@
     docs("androidx.wear:wear-input:1.2.0-alpha02")
     samples("androidx.wear:wear-input-samples:1.2.0-alpha01")
     docs("androidx.wear:wear-input-testing:1.2.0-alpha02")
-    docs("androidx.webkit:webkit:1.7.0-alpha03")
+    docs("androidx.webkit:webkit:1.7.0-beta01")
     docs("androidx.window.extensions.core:core:1.0.0-beta01")
     docs("androidx.window:window:1.1.0-beta01")
     stubs(fileTree(dir: "../window/stubs/", include: ["window-sidecar-release-0.1.0-alpha01.aar"]))
@@ -406,11 +408,11 @@
     docs("androidx.window:window-rxjava3:1.1.0-beta01")
     samples("androidx.window:window-samples:1.1.0-beta01")
     docs("androidx.window:window-testing:1.1.0-beta01")
-    docs("androidx.work:work-gcm:2.8.0")
-    docs("androidx.work:work-multiprocess:2.8.0")
-    docs("androidx.work:work-runtime:2.8.0")
-    docs("androidx.work:work-runtime-ktx:2.8.0")
-    docs("androidx.work:work-rxjava2:2.8.0")
-    docs("androidx.work:work-rxjava3:2.8.0")
-    docs("androidx.work:work-testing:2.8.0")
+    docs("androidx.work:work-gcm:2.8.1")
+    docs("androidx.work:work-multiprocess:2.8.1")
+    docs("androidx.work:work-runtime:2.8.1")
+    docs("androidx.work:work-runtime-ktx:2.8.1")
+    docs("androidx.work:work-rxjava2:2.8.1")
+    docs("androidx.work:work-rxjava3:2.8.1")
+    docs("androidx.work:work-testing:2.8.1")
 }
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
index 5e1993d..4dac647 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
@@ -58,10 +58,10 @@
             R.id.emoji_picker_header_icon
         ).apply {
             setImageDrawable(context.getDrawable(emojiPickerItems.getHeaderIconId(i)))
-            setOnClickListener { onHeaderIconClicked(i) }
             isSelected = isItemSelected
             contentDescription = emojiPickerItems.getHeaderIconDescription(i)
         }
+        viewHolder.itemView.setOnClickListener { onHeaderIconClicked(i) }
         if (isItemSelected) {
             headerIcon.post {
                 headerIcon.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
index 2989592003..8a75049 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/TextTranslator.kt
@@ -26,6 +26,7 @@
 import android.text.style.StrikethroughSpan
 import android.text.style.StyleSpan
 import android.text.style.TextAppearanceSpan
+import android.text.style.TypefaceSpan
 import android.text.style.UnderlineSpan
 import android.util.Log
 import android.util.TypedValue
@@ -115,6 +116,9 @@
         }
         spans.add(TextAppearanceSpan(translationContext.context, textAppearance))
     }
+    style.fontFamily?.let { family ->
+        spans.add(TypefaceSpan(family.family))
+    }
     style.textAlign?.let { align ->
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             TextTranslatorApi31Impl.setTextViewGravity(
@@ -140,6 +144,7 @@
                 setTextColor(resId, colorProvider.getColor(translationContext.context).toArgb())
             }
         }
+
         is DayNightColorProvider -> {
             if (Build.VERSION.SDK_INT >= 31) {
                 setTextViewTextColor(
@@ -151,6 +156,7 @@
                 setTextColor(resId, colorProvider.getColor(translationContext.context).toArgb())
             }
         }
+
         else -> Log.w(GlanceAppWidgetTag, "Unexpected text color: $colorProvider")
     }
 }
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
index b877f04..1b4731c 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/translators/TextTranslatorTest.kt
@@ -26,6 +26,7 @@
 import android.text.style.StrikethroughSpan
 import android.text.style.StyleSpan
 import android.text.style.TextAppearanceSpan
+import android.text.style.TypefaceSpan
 import android.text.style.UnderlineSpan
 import android.view.Gravity
 import android.widget.LinearLayout
@@ -47,6 +48,7 @@
 import androidx.glance.layout.fillMaxWidth
 import androidx.glance.semantics.contentDescription
 import androidx.glance.semantics.semantics
+import androidx.glance.text.FontFamily
 import androidx.glance.text.FontStyle
 import androidx.glance.text.FontWeight
 import androidx.glance.text.Text
@@ -56,6 +58,7 @@
 import androidx.glance.unit.ColorProvider
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runTest
@@ -64,7 +67,6 @@
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
-import kotlin.test.assertIs
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
@@ -119,6 +121,96 @@
     }
 
     @Test
+    fun canTranslateText_withMonoFontFamily() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                "test",
+                style = TextStyle(fontFamily = FontFamily.Monospace),
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        val content = view.text as SpannedString
+        assertThat(content.toString()).isEqualTo("test")
+        content.checkSingleSpan<TypefaceSpan> { span ->
+            assertThat(span.family).isEqualTo("monospace")
+        }
+    }
+
+    @Test
+    fun canTranslateText_withMonoSerifFamily() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                "test",
+                style = TextStyle(fontFamily = FontFamily.Serif),
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        val content = view.text as SpannedString
+        assertThat(content.toString()).isEqualTo("test")
+        content.checkSingleSpan<TypefaceSpan> { span ->
+            assertThat(span.family).isEqualTo("serif")
+        }
+    }
+
+    @Test
+    fun canTranslateText_withSansFontFamily() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                "test",
+                style = TextStyle(fontFamily = FontFamily.SansSerif),
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        val content = view.text as SpannedString
+        assertThat(content.toString()).isEqualTo("test")
+        content.checkSingleSpan<TypefaceSpan> { span ->
+            assertThat(span.family).isEqualTo("sans-serif")
+        }
+    }
+
+    @Test
+    fun canTranslateText_withCursiveFontFamily() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                "test",
+                style = TextStyle(fontFamily = FontFamily.Cursive),
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        val content = view.text as SpannedString
+        assertThat(content.toString()).isEqualTo("test")
+        content.checkSingleSpan<TypefaceSpan> { span ->
+            assertThat(span.family).isEqualTo("cursive")
+        }
+    }
+
+    @Test
+    fun canTranslateText_withCustomFontFamily() = fakeCoroutineScope.runTest {
+        val rv = context.runAndTranslate {
+            Text(
+                "test",
+                style = TextStyle(fontFamily = FontFamily("casual")),
+            )
+        }
+        val view = context.applyRemoteViews(rv)
+
+        assertIs<TextView>(view)
+        val content = view.text as SpannedString
+        assertThat(content.toString()).isEqualTo("test")
+        content.checkSingleSpan<TypefaceSpan> { span ->
+            assertThat(span.family).isEqualTo("casual")
+        }
+    }
+
+    @Test
     fun canTranslateText_withStyleStrikeThrough() = fakeCoroutineScope.runTest {
         val rv = context.runAndTranslate {
             Text("test", style = TextStyle(textDecoration = TextDecoration.LineThrough))
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -666,6 +666,24 @@
 
 package androidx.glance.text {
 
+  public final class FontFamily {
+    ctor public FontFamily(String family);
+    method public String getFamily();
+    property public final String family;
+    field public static final androidx.glance.text.FontFamily.Companion Companion;
+  }
+
+  public static final class FontFamily.Companion {
+    method public androidx.glance.text.FontFamily getCursive();
+    method public androidx.glance.text.FontFamily getMonospace();
+    method public androidx.glance.text.FontFamily getSansSerif();
+    method public androidx.glance.text.FontFamily getSerif();
+    property public final androidx.glance.text.FontFamily Cursive;
+    property public final androidx.glance.text.FontFamily Monospace;
+    property public final androidx.glance.text.FontFamily SansSerif;
+    property public final androidx.glance.text.FontFamily Serif;
+  }
+
   @kotlin.jvm.JvmInline public final value class FontStyle {
     field public static final androidx.glance.text.FontStyle.Companion Companion;
   }
@@ -739,15 +757,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class TextStyle {
-    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
-    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
     method public androidx.glance.unit.ColorProvider getColor();
+    method public androidx.glance.text.FontFamily? getFontFamily();
     method public androidx.compose.ui.unit.TextUnit? getFontSize();
     method public androidx.glance.text.FontStyle? getFontStyle();
     method public androidx.glance.text.FontWeight? getFontWeight();
     method public androidx.glance.text.TextAlign? getTextAlign();
     method public androidx.glance.text.TextDecoration? getTextDecoration();
     property public final androidx.glance.unit.ColorProvider color;
+    property public final androidx.glance.text.FontFamily? fontFamily;
     property public final androidx.compose.ui.unit.TextUnit? fontSize;
     property public final androidx.glance.text.FontStyle? fontStyle;
     property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -666,6 +666,24 @@
 
 package androidx.glance.text {
 
+  public final class FontFamily {
+    ctor public FontFamily(String family);
+    method public String getFamily();
+    property public final String family;
+    field public static final androidx.glance.text.FontFamily.Companion Companion;
+  }
+
+  public static final class FontFamily.Companion {
+    method public androidx.glance.text.FontFamily getCursive();
+    method public androidx.glance.text.FontFamily getMonospace();
+    method public androidx.glance.text.FontFamily getSansSerif();
+    method public androidx.glance.text.FontFamily getSerif();
+    property public final androidx.glance.text.FontFamily Cursive;
+    property public final androidx.glance.text.FontFamily Monospace;
+    property public final androidx.glance.text.FontFamily SansSerif;
+    property public final androidx.glance.text.FontFamily Serif;
+  }
+
   @kotlin.jvm.JvmInline public final value class FontStyle {
     field public static final androidx.glance.text.FontStyle.Companion Companion;
   }
@@ -739,15 +757,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class TextStyle {
-    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
-    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
     method public androidx.glance.unit.ColorProvider getColor();
+    method public androidx.glance.text.FontFamily? getFontFamily();
     method public androidx.compose.ui.unit.TextUnit? getFontSize();
     method public androidx.glance.text.FontStyle? getFontStyle();
     method public androidx.glance.text.FontWeight? getFontWeight();
     method public androidx.glance.text.TextAlign? getTextAlign();
     method public androidx.glance.text.TextDecoration? getTextDecoration();
     property public final androidx.glance.unit.ColorProvider color;
+    property public final androidx.glance.text.FontFamily? fontFamily;
     property public final androidx.compose.ui.unit.TextUnit? fontSize;
     property public final androidx.glance.text.FontStyle? fontStyle;
     property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index a14ea96..f01603c 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -666,6 +666,24 @@
 
 package androidx.glance.text {
 
+  public final class FontFamily {
+    ctor public FontFamily(String family);
+    method public String getFamily();
+    property public final String family;
+    field public static final androidx.glance.text.FontFamily.Companion Companion;
+  }
+
+  public static final class FontFamily.Companion {
+    method public androidx.glance.text.FontFamily getCursive();
+    method public androidx.glance.text.FontFamily getMonospace();
+    method public androidx.glance.text.FontFamily getSansSerif();
+    method public androidx.glance.text.FontFamily getSerif();
+    property public final androidx.glance.text.FontFamily Cursive;
+    property public final androidx.glance.text.FontFamily Monospace;
+    property public final androidx.glance.text.FontFamily SansSerif;
+    property public final androidx.glance.text.FontFamily Serif;
+  }
+
   @kotlin.jvm.JvmInline public final value class FontStyle {
     field public static final androidx.glance.text.FontStyle.Companion Companion;
   }
@@ -739,15 +757,17 @@
   }
 
   @androidx.compose.runtime.Immutable public final class TextStyle {
-    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
-    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration);
+    ctor public TextStyle(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
+    method public androidx.glance.text.TextStyle copy(optional androidx.glance.unit.ColorProvider color, optional androidx.compose.ui.unit.TextUnit? fontSize, optional androidx.glance.text.FontWeight? fontWeight, optional androidx.glance.text.FontStyle? fontStyle, optional androidx.glance.text.TextAlign? textAlign, optional androidx.glance.text.TextDecoration? textDecoration, optional androidx.glance.text.FontFamily? fontFamily);
     method public androidx.glance.unit.ColorProvider getColor();
+    method public androidx.glance.text.FontFamily? getFontFamily();
     method public androidx.compose.ui.unit.TextUnit? getFontSize();
     method public androidx.glance.text.FontStyle? getFontStyle();
     method public androidx.glance.text.FontWeight? getFontWeight();
     method public androidx.glance.text.TextAlign? getTextAlign();
     method public androidx.glance.text.TextDecoration? getTextDecoration();
     property public final androidx.glance.unit.ColorProvider color;
+    property public final androidx.glance.text.FontFamily? fontFamily;
     property public final androidx.compose.ui.unit.TextUnit? fontSize;
     property public final androidx.glance.text.FontStyle? fontStyle;
     property public final androidx.glance.text.FontWeight? fontWeight;
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt
new file mode 100644
index 0000000..39b8e52
--- /dev/null
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/FontFamily.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.glance.text
+
+/**
+ * Describes the family of the font.
+ * Defaults are provided, but it is also possible to supply a custom family. If this is found on
+ * the system it will be used, otherwise it will fallback to a system default.
+ */
+class FontFamily constructor(val family: String) {
+    companion object {
+        /**
+         * The formal text style for scripts.
+         */
+        val Serif = FontFamily("serif")
+
+        /**
+         * Font family with low contrast and plain stroke endings.
+         */
+        val SansSerif = FontFamily("sans-serif")
+
+        /**
+         * Font family where glyphs have the same fixed width.
+         */
+        val Monospace = FontFamily("monospace")
+
+        /**
+         * Cursive, hand-written like font family.
+         */
+        val Cursive = FontFamily("cursive")
+    }
+
+    override fun toString(): String {
+        return family
+    }
+}
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
index f8c01f9..1c1ad1f 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/text/TextStyle.kt
@@ -31,6 +31,7 @@
     val fontStyle: FontStyle? = null,
     val textAlign: TextAlign? = null,
     val textDecoration: TextDecoration? = null,
+    val fontFamily: FontFamily? = null,
 ) {
     fun copy(
         color: ColorProvider = this.color,
@@ -38,14 +39,16 @@
         fontWeight: FontWeight? = this.fontWeight,
         fontStyle: FontStyle? = this.fontStyle,
         textAlign: TextAlign? = this.textAlign,
-        textDecoration: TextDecoration? = this.textDecoration
+        textDecoration: TextDecoration? = this.textDecoration,
+        fontFamily: FontFamily? = this.fontFamily,
     ) = TextStyle(
         color = color,
         fontSize = fontSize,
         fontWeight = fontWeight,
         fontStyle = fontStyle,
         textAlign = textAlign,
-        textDecoration = textDecoration
+        textDecoration = textDecoration,
+        fontFamily = fontFamily,
     )
 
     override fun equals(other: Any?): Boolean {
@@ -57,6 +60,7 @@
         if (fontStyle != other.fontStyle) return false
         if (textDecoration != other.textDecoration) return false
         if (textAlign != other.textAlign) return false
+        if (fontFamily != other.fontFamily) return false
         return true
     }
 
@@ -67,10 +71,12 @@
         result = 31 * result + fontStyle.hashCode()
         result = 31 * result + textDecoration.hashCode()
         result = 31 * result + textAlign.hashCode()
+        result = 31 * result + fontFamily.hashCode()
         return result
     }
 
     override fun toString() =
         "TextStyle(color=$color, fontSize=$fontSize, fontWeight=$fontWeight, " +
-            "fontStyle=$fontStyle, textDecoration=$textDecoration, textAlign=$textAlign)"
+            "fontStyle=$fontStyle, textDecoration=$textDecoration, textAlign=$textAlign, " +
+            "fontFamily=$fontFamily)"
 }
diff --git a/libraryversions.toml b/libraryversions.toml
index 3d658fb..c8b1b2e 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -8,7 +8,7 @@
 APPSEARCH = "1.1.0-alpha03"
 ARCH_CORE = "2.3.0-alpha01"
 ASYNCLAYOUTINFLATER = "1.1.0-alpha02"
-AUTOFILL = "1.2.0-beta02"
+AUTOFILL = "1.3.0-alpha01"
 BENCHMARK = "1.2.0-alpha12"
 BIOMETRIC = "1.2.0-alpha06"
 BLUETOOTH = "1.0.0-alpha01"
diff --git a/palette/palette-ktx/build.gradle b/palette/palette-ktx/build.gradle
index 20b494b..4eef22b 100644
--- a/palette/palette-ktx/build.gradle
+++ b/palette/palette-ktx/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":palette:palette"))
-    }
-
     api(project(":palette:palette"))
     api(libs.kotlinStdlib)
     androidTestImplementation(libs.junit)
diff --git a/palette/palette/build.gradle b/palette/palette/build.gradle
index bb524c8..90bdee7 100644
--- a/palette/palette/build.gradle
+++ b/palette/palette/build.gradle
@@ -6,11 +6,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":palette:palette-ktx"))
-    }
-
     api("androidx.core:core:1.1.0")
     implementation("androidx.collection:collection:1.1.0")
 
diff --git a/preference/preference-ktx/build.gradle b/preference/preference-ktx/build.gradle
index 19e8159..ebef733 100644
--- a/preference/preference-ktx/build.gradle
+++ b/preference/preference-ktx/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":preference:preference"))
-    }
-
     api(project(":preference:preference"))
     api("androidx.core:core-ktx:1.1.0") {
         because "Mirror preference dependency graph for -ktx artifacts"
diff --git a/preference/preference/build.gradle b/preference/preference/build.gradle
index 475c906..366ccdc 100644
--- a/preference/preference/build.gradle
+++ b/preference/preference/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":preference:preference-ktx"))
-    }
-
     api("androidx.annotation:annotation:1.2.0")
     api("androidx.appcompat:appcompat:1.1.0")
     // Use the latest version of core library for verifying insets visibility
diff --git a/savedstate/savedstate-ktx/build.gradle b/savedstate/savedstate-ktx/build.gradle
index da99fd8..0f3f346 100644
--- a/savedstate/savedstate-ktx/build.gradle
+++ b/savedstate/savedstate-ktx/build.gradle
@@ -23,11 +23,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":savedstate:savedstate"))
-    }
-
     api(project(":savedstate:savedstate"))
     api(libs.kotlinStdlib)
 
diff --git a/savedstate/savedstate/build.gradle b/savedstate/savedstate/build.gradle
index 23eb488..a102643 100644
--- a/savedstate/savedstate/build.gradle
+++ b/savedstate/savedstate/build.gradle
@@ -14,11 +14,6 @@
 }
 
 dependencies {
-    // Atomic group
-    constraints {
-        implementation(project(":savedstate:savedstate-ktx"))
-    }
-
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.arch.core:core-common:2.1.0")
     implementation("androidx.lifecycle:lifecycle-common:2.6.1")
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/current.txt b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
index e36b8b2..50ef0aa 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/current.txt
@@ -7,8 +7,8 @@
   }
 
   public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
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 e36b8b2..50ef0aa 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
@@ -7,8 +7,8 @@
   }
 
   public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
diff --git a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
index e32ed48..0b89ad0 100644
--- a/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression-pipeline/api/restricted_current.txt
@@ -7,8 +7,8 @@
   }
 
   public class DynamicTypeEvaluator implements java.lang.AutoCloseable {
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore);
-    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
+    ctor public DynamicTypeEvaluator(boolean, androidx.wear.protolayout.expression.pipeline.ObservableStateStore, androidx.wear.protolayout.expression.pipeline.QuotaManager, androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway?);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString, android.icu.util.ULocale, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.String!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Integer!>);
     method public androidx.wear.protolayout.expression.pipeline.BoundDynamicType bind(androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat, java.util.concurrent.Executor, androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver<java.lang.Float!>);
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 cfe4d48..fd2b9f9 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
@@ -141,15 +141,15 @@
      */
     public DynamicTypeEvaluator(
             boolean platformDataSourcesInitiallyEnabled,
-            @Nullable SensorGateway sensorGateway,
-            @NonNull ObservableStateStore stateStore) {
+            @NonNull ObservableStateStore stateStore,
+            @Nullable SensorGateway sensorGateway) {
         // Build pipeline with quota that doesn't allow any animations.
         this(
                 platformDataSourcesInitiallyEnabled,
-                sensorGateway,
                 stateStore,
                 /* enableAnimations= */ false,
-                DISABLED_ANIMATIONS_QUOTA_MANAGER);
+                DISABLED_ANIMATIONS_QUOTA_MANAGER,
+                sensorGateway);
     }
 
     /**
@@ -170,15 +170,15 @@
      */
     public DynamicTypeEvaluator(
             boolean platformDataSourcesInitiallyEnabled,
-            @Nullable SensorGateway sensorGateway,
             @NonNull ObservableStateStore stateStore,
-            @NonNull QuotaManager animationQuotaManager) {
+            @NonNull QuotaManager animationQuotaManager,
+            @Nullable SensorGateway sensorGateway) {
         this(
                 platformDataSourcesInitiallyEnabled,
-                sensorGateway,
                 stateStore,
                 /* enableAnimations= */ true,
-                animationQuotaManager);
+                animationQuotaManager,
+                sensorGateway);
     }
 
     /**
@@ -196,10 +196,10 @@
      */
     private DynamicTypeEvaluator(
             boolean platformDataSourcesInitiallyEnabled,
-            @Nullable SensorGateway sensorGateway,
             @NonNull ObservableStateStore stateStore,
             boolean enableAnimations,
-            @NonNull QuotaManager animationQuotaManager) {
+            @NonNull QuotaManager animationQuotaManager,
+            @Nullable SensorGateway sensorGateway) {
         this.mSensorGateway = sensorGateway;
         Handler uiHandler = new Handler(Looper.getMainLooper());
         MainThreadExecutor uiExecutor = new MainThreadExecutor(uiHandler);
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
index 0bcba4e..630218e 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/test/java/androidx/wear/protolayout/expression/pipeline/DynamicTypeEvaluatorTest.java
@@ -273,9 +273,9 @@
         DynamicTypeEvaluator evaluator =
                 new DynamicTypeEvaluator(
                         /* platformDataSourcesInitiallyEnabled= */ true,
-                        /* sensorGateway= */ null,
                         stateStore,
-                        new FixedQuotaManagerImpl(MAX_VALUE));
+                        new FixedQuotaManagerImpl(MAX_VALUE),
+                        /* sensorGateway= */ null);
 
         mTestCase.runTest(evaluator);
     }
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index 3421707..536215b 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -35,7 +35,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation("androidx.core:core:1.7.0")
     androidTestImplementation(project(":test:screenshot:screenshot"))
-    androidTestImplementation(project(":wear:tiles:tiles-renderer"))
+    androidTestImplementation(project(":wear:protolayout:protolayout-renderer"))
     androidTestRuntimeOnly(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     androidTestImplementation(libs.protobuf)
 
@@ -56,8 +56,7 @@
     }
 
     sourceSets {
-        // TODO(b/268312818): Update to protolayout location and copy images.
-        androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-tiles-material"
+        androidTest.assets.srcDirs += project.rootDir.absolutePath + "/../../golden/wear/wear-protolayout-material"
     }
 
     defaultConfig {
diff --git a/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml b/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..fc13b63
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+    <application
+        android:label="Golden Tests"
+        android:supportsRtl="true"
+        android:theme="@android:style/Theme.DeviceDefault"
+        android:taskAffinity="">
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="androidx.wear.protolayout.material.testapp.GoldenTestActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
new file mode 100644
index 0000000..13d9252
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenTest {
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+    public MaterialGoldenTest(String expected, LayoutElement layoutElement) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        float scale = displayMetrics.density;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // Not important for components.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        Map<String, LayoutElement> testCases = generateTestCases(context, deviceParameters, "");
+
+        return testCases.entrySet().stream()
+                .map(test -> new Object[] {test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @SdkSuppress(maxSdkVersion = 32) // b/271486183
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
new file mode 100644
index 0000000..7e03861
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/MaterialGoldenXLTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.protolayout.material.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class MaterialGoldenXLTest {
+    /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+    tests together, first all parametrization (data()) methods are called, and then individual
+    tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+    each test. */
+    private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+    private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+    private static final float FONT_SCALE_XXXL = 1.24f;
+
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+    public MaterialGoldenXLTest(String expected, LayoutElement layoutElement) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        currentDisplayMetrics.setTo(displayMetrics);
+        displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+        float scale = displayMetrics.density;
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // Not important for components.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        Map<String, LayoutElement> testCases =
+                generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+        // Restore state before this method, so other test have correct context.
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+
+        return testCases.entrySet().stream()
+                .map(test -> new Object[] {test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @Parameterized.BeforeParam
+    public static void restoreBefore() {
+        OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+        getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+    }
+
+    @Parameterized.AfterParam
+    public static void restoreAfter() {
+        getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+    }
+
+    @SdkSuppress(maxSdkVersion = 32) // b/271486183
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
new file mode 100644
index 0000000..1033504
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/RunnerUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.test.screenshot.matchers.MSSIMMatcher;
+import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.material.testapp.GoldenTestActivity;
+
+public class RunnerUtils {
+    // This isn't totally ideal right now. The screenshot tests run on a phone, so emulate some
+    // watch dimensions here.
+    public static final int SCREEN_WIDTH = 390;
+    public static final int SCREEN_HEIGHT = 390;
+
+    private RunnerUtils() {}
+
+    public static void runSingleScreenshotTest(
+            @NonNull AndroidXScreenshotTestRule rule,
+            @NonNull LayoutElementBuilders.LayoutElement layoutElement,
+            @NonNull String expected) {
+        LayoutElementBuilders.Layout layout =
+                LayoutElementBuilders.Layout.fromLayoutElement(layoutElement);
+        byte[] layoutPayload = layout.toByteArray();
+
+        Intent startIntent =
+                new Intent(
+                        InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                        GoldenTestActivity.class);
+        startIntent.putExtra("layout", layoutPayload);
+
+        ActivityScenario<GoldenTestActivity> scenario = ActivityScenario.launch(startIntent);
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+
+        try {
+            // Wait 1s after launching the activity. This allows for the old white layout in the
+            // bootstrap activity to fully go away before proceeding.
+            Thread.sleep(1000);
+        } catch (Exception ex) {
+            if (ex instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            Log.e("MaterialGoldenTest", "Error sleeping", ex);
+        }
+
+        Bitmap bitmap =
+                Bitmap.createBitmap(
+                        InstrumentationRegistry.getInstrumentation()
+                                .getUiAutomation()
+                                .takeScreenshot(),
+                        0,
+                        0,
+                        SCREEN_WIDTH,
+                        SCREEN_HEIGHT);
+        rule.assertBitmapAgainstGolden(bitmap, expected, new MSSIMMatcher());
+
+        // There's a weird bug (related to b/159805732) where, when calling .close() on
+        // ActivityScenario or calling finish() and immediately exiting the test, the test can hang
+        // on a white screen for 45s. Closing the activity here and waiting for 1s seems to fix
+        // this.
+        scenario.onActivity(Activity::finish);
+
+        try {
+            Thread.sleep(1000);
+        } catch (Exception ex) {
+            if (ex instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
+            Log.e("MaterialGoldenTest", "Error sleeping", ex);
+        }
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
new file mode 100644
index 0000000..cb43b9b
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/TestCasesGenerator.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_CENTER;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_END;
+import static androidx.wear.protolayout.LayoutElementBuilders.HORIZONTAL_ALIGN_START;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.NonNull;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Row;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestCasesGenerator {
+    private TestCasesGenerator() {}
+
+    public static final String NORMAL_SCALE_SUFFIX = "";
+    public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+    private static final String ICON_ID = "tile_icon";
+    private static final String AVATAR = "avatar_image";
+
+    /**
+     * This function will append goldenSuffix on the name of the golden images that should be
+     * different for different user font sizes. Note that some of the golden will have the same name
+     * as it should point on the same size independent image.
+     */
+    @NonNull
+    static Map<String, LayoutElement> generateTestCases(
+            @NonNull Context context,
+            @NonNull DeviceParametersBuilders.DeviceParameters deviceParameters,
+            @NonNull String goldenSuffix) {
+        Clickable clickable =
+                new Clickable.Builder()
+                        .setOnClick(new LaunchAction.Builder().build())
+                        .setId("action_id")
+                        .build();
+        String mainText = "Primary label";
+        String labelText = "Secondary label";
+        String largeChipText = "Action";
+        HashMap<String, LayoutElement> testCases = new HashMap<>();
+
+        testCases.put(
+                "default_icon_button_golden" + NORMAL_SCALE_SUFFIX,
+                new Button.Builder(context, clickable).setIconContent(ICON_ID).build());
+        testCases.put(
+                "extralarge_secondary_icon_after_button_golden" + NORMAL_SCALE_SUFFIX,
+                new Button.Builder(context, clickable)
+                        .setButtonColors(ButtonDefaults.SECONDARY_COLORS)
+                        .setIconContent(ICON_ID)
+                        .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+                        .build());
+        testCases.put(
+                "large_secondary_icon_40size_button_golden" + NORMAL_SCALE_SUFFIX,
+                new Button.Builder(context, clickable)
+                        .setSize(ButtonDefaults.LARGE_SIZE)
+                        .setButtonColors(ButtonDefaults.SECONDARY_COLORS)
+                        .setIconContent(ICON_ID, dp(40))
+                        .build());
+        testCases.put(
+                "extralarge_custom_text_custom_sizefont_button_golden" + goldenSuffix,
+                new Button.Builder(context, clickable)
+                        .setButtonColors(new ButtonColors(Color.YELLOW, Color.GREEN))
+                        .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+                        .setCustomContent(
+                                new Text.Builder(context, "ABC")
+                                        .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+                                        .setItalic(true)
+                                        .setColor(argb(Color.GREEN))
+                                        .build())
+                        .build());
+        testCases.put(
+                "default_text_button_golden" + goldenSuffix,
+                new Button.Builder(context, clickable).setTextContent("ABC").build());
+        testCases.put(
+                "default_image_button_golden" + NORMAL_SCALE_SUFFIX,
+                new Button.Builder(context, clickable).setImageContent(AVATAR).build());
+
+        testCases.put(
+                "default_chip_maintext_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryLabelContent(mainText)
+                        .build());
+        testCases.put(
+                "default_chip_maintextlabeltext_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryLabelContent(mainText)
+                        .setSecondaryLabelContent(labelText)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .build());
+        testCases.put(
+                "default_chip_maintexticon_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryLabelContent(mainText)
+                        .setIconContent(ICON_ID)
+                        .build());
+        testCases.put(
+                "secondary_chip_maintext_centered_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setChipColors(ChipDefaults.SECONDARY_COLORS)
+                        .setPrimaryLabelContent(mainText)
+                        .build());
+        testCases.put(
+                "custom_chip_all_overflows_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setWidth(130)
+                        .setPrimaryLabelContent(mainText)
+                        .setSecondaryLabelContent(labelText)
+                        .setIconContent(ICON_ID)
+                        .setChipColors(
+                                new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+                        .build());
+        testCases.put(
+                "default_chip_all_centered_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setPrimaryLabelContent(mainText)
+                        .setSecondaryLabelContent(labelText)
+                        .setIconContent(ICON_ID)
+                        .build());
+        testCases.put(
+                "default_chip_all_rigthalign_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_END)
+                        .setPrimaryLabelContent(mainText)
+                        .setSecondaryLabelContent(labelText)
+                        .setIconContent(ICON_ID)
+                        .build());
+        testCases.put(
+                "custom_chip_icon_primary_overflows_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryLabelContent(mainText)
+                        .setIconContent(ICON_ID)
+                        .setWidth(150)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setChipColors(
+                                new ChipColors(Color.YELLOW, Color.GREEN, Color.BLACK, Color.GRAY))
+                        .build());
+        testCases.put(
+                "chip_custom_content_centered_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setCustomContent(
+                                new Box.Builder()
+                                        .addContent(
+                                                new Text.Builder(context, "random text")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE3)
+                                                        .setItalic(true)
+                                                        .build())
+                                        .build())
+                        .build());
+        testCases.put(
+                "chip_custom_content_leftaligned_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setChipColors(ChipDefaults.SECONDARY_COLORS)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setCustomContent(
+                                new Row.Builder()
+                                        .addContent(
+                                                new Text.Builder(context, "text1")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE3)
+                                                        .setItalic(true)
+                                                        .setColor(argb(Color.WHITE))
+                                                        .build())
+                                        .addContent(
+                                                new Text.Builder(context, "text2")
+                                                        .setTypography(Typography.TYPOGRAPHY_TITLE2)
+                                                        .setColor(argb(Color.YELLOW))
+                                                        .build())
+                                        .build())
+                        .setWidth(150)
+                        .build());
+        testCases.put(
+                "chip_2lines_primary_overflows_golden" + goldenSuffix,
+                new Chip.Builder(context, clickable, deviceParameters)
+                        .setPrimaryLabelContent("abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde")
+                        .build());
+
+        // Different text lengths to test expanding the width based on the size of text. If it's
+        // more than 9, the rest will be deleted.
+        testCases.put(
+                "compactchip_default_len2_golden" + goldenSuffix,
+                new CompactChip.Builder(context, "Ab", clickable, deviceParameters).build());
+        testCases.put(
+                "compactchip_default_len5_golden" + goldenSuffix,
+                new CompactChip.Builder(context, "Abcde", clickable, deviceParameters).build());
+        testCases.put(
+                "compactchip_default_len9_golden" + goldenSuffix,
+                new CompactChip.Builder(context, "Abcdefghi", clickable, deviceParameters).build());
+        testCases.put(
+                "compactchip_default_toolong_golden" + goldenSuffix,
+                new CompactChip.Builder(
+                                context, "AbcdefghiEXTRAEXTRAEXTRA", clickable, deviceParameters)
+                        .build());
+        testCases.put(
+                "compactchip_custom_default_golden" + goldenSuffix,
+                new CompactChip.Builder(context, "Action", clickable, deviceParameters)
+                        .setChipColors(new ChipColors(Color.YELLOW, Color.BLACK))
+                        .build());
+
+        testCases.put(
+                "titlechip_default_golden" + goldenSuffix,
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters).build());
+        testCases.put(
+                "titlechip_default_texttoolong_golden" + goldenSuffix,
+                new TitleChip.Builder(context, "abcdeabcdeabcdeEXTRA", clickable, deviceParameters)
+                        .build());
+        testCases.put(
+                "titlechip_leftalign_secondary_default_golden" + goldenSuffix,
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_START)
+                        .setChipColors(ChipDefaults.TITLE_SECONDARY_COLORS)
+                        .build());
+        testCases.put(
+                "titlechip_centered_custom_150_secondary_default_golden" + goldenSuffix,
+                new TitleChip.Builder(context, largeChipText, clickable, deviceParameters)
+                        .setHorizontalAlignment(HORIZONTAL_ALIGN_CENTER)
+                        .setChipColors(new ChipColors(Color.YELLOW, Color.BLUE))
+                        .setWidth(150)
+                        .build());
+
+        testCases.put(
+                "default_full_circularprogressindicator",
+                new CircularProgressIndicator.Builder().build());
+        testCases.put(
+                "default_gap_circularprogressindicator",
+                new CircularProgressIndicator.Builder()
+                        .setStartAngle(ProgressIndicatorDefaults.GAP_START_ANGLE)
+                        .setEndAngle(ProgressIndicatorDefaults.GAP_END_ANGLE)
+                        .build());
+        testCases.put(
+                "default_full_90_circularprogressindicator",
+                new CircularProgressIndicator.Builder().setProgress(0.25f).build());
+        testCases.put(
+                "default_gap_90_circularprogressindicator",
+                new CircularProgressIndicator.Builder()
+                        .setProgress(0.25f)
+                        .setStartAngle(ProgressIndicatorDefaults.GAP_START_ANGLE)
+                        .setEndAngle(ProgressIndicatorDefaults.GAP_END_ANGLE)
+                        .build());
+        testCases.put(
+                "custom_gap_45_circularprogressindicator",
+                new CircularProgressIndicator.Builder()
+                        .setStartAngle(45)
+                        .setEndAngle(270)
+                        .setProgress(0.2f)
+                        .setStrokeWidth(12)
+                        .setCircularProgressIndicatorColors(
+                                new ProgressIndicatorColors(Color.BLUE, Color.YELLOW))
+                        .build());
+
+        testCases.put(
+                "default_text_golden" + goldenSuffix, new Text.Builder(context, "Testing").build());
+        testCases.put(
+                "custom_text_golden" + goldenSuffix,
+                new Text.Builder(context, "Testing text.")
+                        .setItalic(true)
+                        .setColor(argb(Color.YELLOW))
+                        .setWeight(LayoutElementBuilders.FONT_WEIGHT_BOLD)
+                        .setTypography(Typography.TYPOGRAPHY_BODY2)
+                        .build());
+        testCases.put(
+                "overflow_text_golden" + goldenSuffix,
+                new Text.Builder(context, "abcdeabcdeabcde").build());
+
+        return testCases;
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
new file mode 100644
index 0000000..e39d6fa
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenTest.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class LayoutsGoldenTest {
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+    public LayoutsGoldenTest(String expected, LayoutElement layoutElement) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        float scale = displayMetrics.density;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // TODO(b/231543947): Add test cases for round screen.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        Map<String, LayoutElement> testCases = generateTestCases(context, deviceParameters, "");
+
+        return testCases.entrySet().stream()
+                .map(test -> new Object[] {test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @SdkSuppress(maxSdkVersion = 32) // b/271486183
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
new file mode 100644
index 0000000..0089ecd
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/LayoutsGoldenXLTest.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+import static androidx.wear.protolayout.material.RunnerUtils.runSingleScreenshotTest;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.XXXL_SCALE_SUFFIX;
+import static androidx.wear.protolayout.material.layouts.TestCasesGenerator.generateTestCases;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+
+import androidx.annotation.Dimension;
+import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.screenshot.AndroidXScreenshotTestRule;
+import androidx.wear.protolayout.DeviceParametersBuilders;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@RunWith(Parameterized.class)
+@LargeTest
+public class LayoutsGoldenXLTest {
+    /* We set DisplayMetrics in the data() method for creating test cases. However, when running all
+    tests together, first all parametrization (data()) methods are called, and then individual
+    tests, causing that actual DisplayMetrics will be different. So we need to restore it before
+    each test. */
+    private static final DisplayMetrics DISPLAY_METRICS_FOR_TEST = new DisplayMetrics();
+    private static final DisplayMetrics OLD_DISPLAY_METRICS = new DisplayMetrics();
+
+    private static final float FONT_SCALE_XXXL = 1.24f;
+
+    private final LayoutElement mLayoutElement;
+    private final String mExpected;
+
+    @Rule
+    public AndroidXScreenshotTestRule mScreenshotRule =
+            new AndroidXScreenshotTestRule("wear/wear-protolayout-material");
+
+    public LayoutsGoldenXLTest(String expected, LayoutElement layoutElement) {
+        mLayoutElement = layoutElement;
+        mExpected = expected;
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection<Object[]> data() {
+        Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        DisplayMetrics currentDisplayMetrics = new DisplayMetrics();
+        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+        currentDisplayMetrics.setTo(displayMetrics);
+        displayMetrics.scaledDensity *= FONT_SCALE_XXXL;
+
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(displayMetrics);
+
+        DISPLAY_METRICS_FOR_TEST.setTo(displayMetrics);
+
+        float scale = displayMetrics.density;
+        DeviceParameters deviceParameters =
+                new DeviceParameters.Builder()
+                        .setScreenWidthDp(pxToDp(SCREEN_WIDTH, scale))
+                        .setScreenHeightDp(pxToDp(SCREEN_HEIGHT, scale))
+                        .setScreenDensity(displayMetrics.density)
+                        // TODO(b/231543947): Add test cases for round screen.
+                        .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                        .build();
+
+        Map<String, LayoutElement> testCases =
+                generateTestCases(context, deviceParameters, XXXL_SCALE_SUFFIX);
+
+        // Restore state before this method, so other test have correct context. This is needed here
+        // too, besides in restoreBefore and restoreAfter as the test cases builder uses the context
+        // to apply font scaling, so we need that display metrics passed in. However, after
+        // generating cases we need to restore the state as other data() methods in this package can
+        // work correctly with the default state, as when the tests are run, first all data() static
+        // methods are called, and then parameterized test cases.
+        InstrumentationRegistry.getInstrumentation()
+                .getContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+        InstrumentationRegistry.getInstrumentation()
+                .getTargetContext()
+                .getResources()
+                .getDisplayMetrics()
+                .setTo(currentDisplayMetrics);
+
+        return testCases.entrySet().stream()
+                .map(test -> new Object[] {test.getKey(), test.getValue()})
+                .collect(Collectors.toList());
+    }
+
+    @Parameterized.BeforeParam
+    public static void restoreBefore() {
+        // Set the state as it was in data() method when we generated test cases. This was
+        // overridden by other static data() methods, so we need to restore it.
+        OLD_DISPLAY_METRICS.setTo(getApplicationContext().getResources().getDisplayMetrics());
+        getApplicationContext().getResources().getDisplayMetrics().setTo(DISPLAY_METRICS_FOR_TEST);
+    }
+
+    @Parameterized.AfterParam
+    public static void restoreAfter() {
+        // Restore the state to default, so the other tests and emulator have the correct starter
+        // state.
+        getApplicationContext().getResources().getDisplayMetrics().setTo(OLD_DISPLAY_METRICS);
+    }
+
+    @SdkSuppress(maxSdkVersion = 32) // b/271486183
+    @Test
+    public void test() {
+        runSingleScreenshotTest(mScreenshotRule, mLayoutElement, mExpected);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java
new file mode 100644
index 0000000..e95c9da
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/layouts/TestCasesGenerator.java
@@ -0,0 +1,644 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.layouts;
+
+import static androidx.wear.protolayout.ColorBuilders.argb;
+import static androidx.wear.protolayout.DimensionBuilders.dp;
+import static androidx.wear.protolayout.DimensionBuilders.expand;
+import static androidx.wear.protolayout.DimensionBuilders.wrap;
+
+import android.content.Context;
+import android.graphics.Color;
+
+import androidx.annotation.Dimension;
+import androidx.annotation.NonNull;
+import androidx.wear.protolayout.ActionBuilders.LaunchAction;
+import androidx.wear.protolayout.DeviceParametersBuilders.DeviceParameters;
+import androidx.wear.protolayout.LayoutElementBuilders.Box;
+import androidx.wear.protolayout.LayoutElementBuilders.Column;
+import androidx.wear.protolayout.LayoutElementBuilders.LayoutElement;
+import androidx.wear.protolayout.LayoutElementBuilders.Spacer;
+import androidx.wear.protolayout.ModifiersBuilders.Background;
+import androidx.wear.protolayout.ModifiersBuilders.Clickable;
+import androidx.wear.protolayout.ModifiersBuilders.Modifiers;
+import androidx.wear.protolayout.material.Button;
+import androidx.wear.protolayout.material.ButtonDefaults;
+import androidx.wear.protolayout.material.Chip;
+import androidx.wear.protolayout.material.ChipColors;
+import androidx.wear.protolayout.material.CircularProgressIndicator;
+import androidx.wear.protolayout.material.Colors;
+import androidx.wear.protolayout.material.CompactChip;
+import androidx.wear.protolayout.material.ProgressIndicatorColors;
+import androidx.wear.protolayout.material.ProgressIndicatorDefaults;
+import androidx.wear.protolayout.material.Text;
+import androidx.wear.protolayout.material.TitleChip;
+import androidx.wear.protolayout.material.Typography;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestCasesGenerator {
+    private TestCasesGenerator() {}
+
+    public static final String NORMAL_SCALE_SUFFIX = "";
+    public static final String XXXL_SCALE_SUFFIX = "_xxxl";
+
+    /**
+     * This function will append goldenSuffix on the name of the golden images that should be
+     * different for different user font sizes. Note that some of the golden will have the same name
+     * as it should point on the same size independent image.
+     */
+    @NonNull
+    static Map<String, LayoutElement> generateTestCases(
+            @NonNull Context context,
+            @NonNull DeviceParameters deviceParameters,
+            @NonNull String goldenSuffix) {
+        Clickable clickable =
+                new Clickable.Builder()
+                        .setOnClick(new LaunchAction.Builder().build())
+                        .setId("action_id")
+                        .build();
+        HashMap<String, LayoutElement> testCases = new HashMap<>();
+
+        TitleChip content =
+                new TitleChip.Builder(context, "Action", clickable, deviceParameters).build();
+        CompactChip.Builder primaryChipBuilder =
+                new CompactChip.Builder(context, "Action", clickable, deviceParameters);
+
+        testCases.put(
+                "default_empty_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "default_longtext_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(
+                                new CompactChip.Builder(
+                                                context,
+                                                "Too_long_textToo_long_textToo_long_text",
+                                                clickable,
+                                                deviceParameters)
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_primarylabel_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(buildColoredBoxPLL(Color.YELLOW))
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_primarylabel_secondarylabel_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(buildColoredBoxPLL(Color.YELLOW))
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_secondarylabel_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(buildColoredBoxPLL(Color.YELLOW))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+
+        testCases.put(
+                "custom_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(content)
+                        .setPrimaryChipContent(
+                                primaryChipBuilder
+                                        .setChipColors(new ChipColors(Color.YELLOW, Color.GREEN))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(buildColoredBoxPLL(Color.YELLOW))
+                        .build());
+        testCases.put(
+                "two_chips_content_primarychiplayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new Column.Builder()
+                                        .setWidth(expand())
+                                        .setHeight(wrap())
+                                        .addContent(
+                                                new Chip.Builder(
+                                                                context,
+                                                                clickable,
+                                                                deviceParameters)
+                                                        .setPrimaryLabelContent("First chip")
+                                                        .setWidth(expand())
+                                                        .build())
+                                        .addContent(new Spacer.Builder().setHeight(dp(4)).build())
+                                        .addContent(
+                                                new Chip.Builder(
+                                                                context,
+                                                                clickable,
+                                                                deviceParameters)
+                                                        .setPrimaryLabelContent("Second chip")
+                                                        .setWidth(expand())
+                                                        .build())
+                                        .build())
+                        .build());
+
+        primaryChipBuilder =
+                new CompactChip.Builder(context, "Action", clickable, deviceParameters);
+        testCases.put(
+                "coloredbox_1_chip_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_2_chip_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_3_chip_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_2_chip_primary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_2_chip_primary_secondary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_2_columnslayout_golden" + NORMAL_SCALE_SUFFIX,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_2_primary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_2_primary_secondary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_3_chip_primary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_3_chip_primary_secondary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_3_columnslayout_golden" + NORMAL_SCALE_SUFFIX,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_3_primary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "coloredbox_3_primary_secondary_columnslayout_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .build());
+        testCases.put(
+                "custom_spacer_coloredbox_3_chip_primary_secondary_columnslayout_golden"
+                        + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setContent(
+                                new MultiSlotLayout.Builder()
+                                        .addSlotContent(buildColoredBoxMSL(Color.YELLOW))
+                                        .addSlotContent(buildColoredBoxMSL(Color.BLUE))
+                                        .addSlotContent(buildColoredBoxMSL(Color.MAGENTA))
+                                        .setHorizontalSpacerWidth(2)
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setSecondaryLabelTextContent(buildTextLabel(context, "Secondary label"))
+                        .setVerticalSpacerHeight(1)
+                        .build());
+
+        CircularProgressIndicator.Builder progressIndicatorBuilder =
+                new CircularProgressIndicator.Builder().setProgress(0.3f);
+        Text textContent =
+                new Text.Builder(context, "Text")
+                        .setColor(argb(Color.WHITE))
+                        .setTypography(Typography.TYPOGRAPHY_DISPLAY1)
+                        .build();
+        testCases.put(
+                "default_text_progressindicatorlayout_golden" + goldenSuffix,
+                new EdgeContentLayout.Builder(deviceParameters)
+                        .setEdgeContent(progressIndicatorBuilder.build())
+                        .setPrimaryLabelTextContent(
+                                new Text.Builder(context, "Primary label")
+                                        .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+                                        .setColor(argb(Colors.PRIMARY))
+                                        .build())
+                        .setContent(textContent)
+                        .setSecondaryLabelTextContent(
+                                new Text.Builder(context, "Secondary label")
+                                        .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+                                        .setColor(argb(Colors.ON_SURFACE))
+                                        .build())
+                        .build());
+        testCases.put(
+                "default_empty_progressindicatorlayout_golden" + NORMAL_SCALE_SUFFIX,
+                new EdgeContentLayout.Builder(deviceParameters)
+                        .setEdgeContent(progressIndicatorBuilder.build())
+                        .build());
+        testCases.put(
+                "custom_progressindicatorlayout_golden" + goldenSuffix,
+                new EdgeContentLayout.Builder(deviceParameters)
+                        .setContent(textContent)
+                        .setEdgeContent(
+                                progressIndicatorBuilder
+                                        .setCircularProgressIndicatorColors(
+                                                new ProgressIndicatorColors(
+                                                        Color.YELLOW, Color.GREEN))
+                                        .build())
+                        .build());
+        testCases.put(
+                "coloredbox_progressindicatorlayout_golden" + NORMAL_SCALE_SUFFIX,
+                new EdgeContentLayout.Builder(deviceParameters)
+                        .setEdgeContent(
+                                progressIndicatorBuilder
+                                        .setCircularProgressIndicatorColors(
+                                                ProgressIndicatorDefaults.DEFAULT_COLORS)
+                                        .build())
+                        .setContent(
+                                new Box.Builder()
+                                        .setWidth(dp(500))
+                                        .setHeight(dp(500))
+                                        .setModifiers(
+                                                new Modifiers.Builder()
+                                                        .setBackground(
+                                                                new Background.Builder()
+                                                                        .setColor(
+                                                                                argb(Color.YELLOW))
+                                                                        .build())
+                                                        .build())
+                                        .build())
+                        .build());
+
+        Button button1 = new Button.Builder(context, clickable).setTextContent("1").build();
+        Button button2 = new Button.Builder(context, clickable).setTextContent("2").build();
+        Button button3 = new Button.Builder(context, clickable).setTextContent("3").build();
+        Button button4 = new Button.Builder(context, clickable).setTextContent("4").build();
+        Button button5 = new Button.Builder(context, clickable).setTextContent("5").build();
+        Button button6 = new Button.Builder(context, clickable).setTextContent("6").build();
+        Button button7 = new Button.Builder(context, clickable).setTextContent("7").build();
+        Button largeButton1 =
+                new Button.Builder(context, clickable)
+                        .setTextContent("1")
+                        .setSize(ButtonDefaults.LARGE_SIZE)
+                        .build();
+        Button largeButton2 =
+                new Button.Builder(context, clickable)
+                        .setTextContent("2")
+                        .setSize(ButtonDefaults.LARGE_SIZE)
+                        .build();
+        Button extraLargeButton =
+                new Button.Builder(context, clickable)
+                        .setTextContent("1")
+                        .setSize(ButtonDefaults.EXTRA_LARGE_SIZE)
+                        .build();
+        testCases.put(
+                "multibutton_layout_1button_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(extraLargeButton)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_1button_chip_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(extraLargeButton)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_2button_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(largeButton1)
+                                        .addButtonContent(largeButton2)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_2button_chip_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(largeButton1)
+                                        .addButtonContent(largeButton2)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_2button_primarylabel_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(largeButton1)
+                                        .addButtonContent(largeButton2)
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "multibutton_layout_2button_chip_primarylabel_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(largeButton1)
+                                        .addButtonContent(largeButton2)
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_3button_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_3button_chip_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_3button_primarylabel_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "multibutton_layout_3button_chip_primarylabel_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .setPrimaryLabelTextContent(buildTextLabel(context, "Primary label"))
+                        .build());
+        testCases.put(
+                "multibutton_layout_4button_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_4button_chip_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_5button_top_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .addButtonContent(button5)
+                                        .setFiveButtonDistribution(
+                                                MultiButtonLayout
+                                                        .FIVE_BUTTON_DISTRIBUTION_TOP_HEAVY)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_5button_bottom_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .addButtonContent(button5)
+                                        .setFiveButtonDistribution(
+                                                MultiButtonLayout
+                                                        .FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_5button_bottom_chip_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .addButtonContent(button5)
+                                        .setFiveButtonDistribution(
+                                                MultiButtonLayout
+                                                        .FIVE_BUTTON_DISTRIBUTION_BOTTOM_HEAVY)
+                                        .build())
+                        .setPrimaryChipContent(primaryChipBuilder.build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_6button_golden" + goldenSuffix,
+                new PrimaryLayout.Builder(deviceParameters)
+                        .setContent(
+                                new MultiButtonLayout.Builder()
+                                        .addButtonContent(button1)
+                                        .addButtonContent(button2)
+                                        .addButtonContent(button3)
+                                        .addButtonContent(button4)
+                                        .addButtonContent(button5)
+                                        .addButtonContent(button6)
+                                        .build())
+                        .build());
+        testCases.put(
+                "multibutton_layout_7button_golden" + goldenSuffix,
+                new MultiButtonLayout.Builder()
+                        .addButtonContent(button1)
+                        .addButtonContent(button2)
+                        .addButtonContent(button3)
+                        .addButtonContent(button4)
+                        .addButtonContent(button5)
+                        .addButtonContent(button6)
+                        .addButtonContent(button7)
+                        .build());
+
+        return testCases;
+    }
+
+    @NonNull
+    private static Text buildTextLabel(@NonNull Context context, @NonNull String text) {
+        return new Text.Builder(context, text)
+                .setTypography(Typography.TYPOGRAPHY_CAPTION1)
+                .setColor(argb(Color.WHITE))
+                .build();
+    }
+
+    @NonNull
+    private static Box buildColoredBoxMSL(int color) {
+        return new Box.Builder()
+                .setWidth(dp(60))
+                .setHeight(dp(60))
+                .setModifiers(
+                        new Modifiers.Builder()
+                                .setBackground(
+                                        new Background.Builder().setColor(argb(color)).build())
+                                .build())
+                .build();
+    }
+
+    @NonNull
+    private static Box buildColoredBoxPLL(int color) {
+        return new Box.Builder()
+                .setWidth(expand())
+                .setHeight(dp(60))
+                .setModifiers(
+                        new Modifiers.Builder()
+                                .setBackground(
+                                        new Background.Builder().setColor(argb(color)).build())
+                                .build())
+                .build();
+    }
+
+    @Dimension(unit = Dimension.DP)
+    static int pxToDp(int px, float scale) {
+        return (int) ((px - 0.5f) / scale);
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java
new file mode 100644
index 0000000..53fc7d0
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/androidTest/java/androidx/wear/protolayout/material/testapp/GoldenTestActivity.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.material.testapp;
+
+import static androidx.wear.protolayout.material.Helper.checkNotNull;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_HEIGHT;
+import static androidx.wear.protolayout.material.RunnerUtils.SCREEN_WIDTH;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Color;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.FrameLayout.LayoutParams;
+
+import androidx.annotation.Nullable;
+import androidx.wear.protolayout.LayoutElementBuilders.Layout;
+import androidx.wear.protolayout.ResourceBuilders.AndroidImageResourceByResId;
+import androidx.wear.protolayout.ResourceBuilders.ImageResource;
+import androidx.wear.protolayout.ResourceBuilders.Resources;
+import androidx.wear.protolayout.material.R;
+import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+@SuppressWarnings("deprecation")
+public class GoldenTestActivity extends Activity {
+    private static final String ICON_ID = "icon";
+    private static final String AVATAR = "avatar_image";
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        byte[] layoutPayload = getIntent().getExtras().getByteArray("layout");
+        Layout layout = Layout.fromByteArray(layoutPayload);
+
+        Context appContext = getApplicationContext();
+        FrameLayout root = new FrameLayout(appContext);
+        root.setBackgroundColor(Color.BLACK);
+        root.setLayoutParams(new LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+
+        ListeningExecutorService mainExecutor = MoreExecutors.newDirectExecutorService();
+        Resources resources = generateResources();
+
+        ProtoLayoutViewInstance instance =
+                new ProtoLayoutViewInstance(
+                        new ProtoLayoutViewInstance.Config.Builder(
+                                appContext,
+                                mainExecutor,
+                                mainExecutor,
+                                "androidx.wear.tiles.extra.CLICKABLE_ID")
+                                .setIsViewFullyVisible(true)
+                        .build());
+
+        instance.renderAndAttach(checkNotNull(layout).toProto(), resources.toProto(), root);
+
+        View firstChild = root.getChildAt(0);
+
+        // Simulate what the thing outside the renderer should do. Center the contents.
+        LayoutParams layoutParams = (LayoutParams) firstChild.getLayoutParams();
+        layoutParams.gravity = Gravity.CENTER;
+
+        // Set the activity to be full screen so when we crop the Bitmap we don't get time bar etc.
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        getWindow()
+                .setFlags(
+                        WindowManager.LayoutParams.FLAG_FULLSCREEN,
+                        WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+        setContentView(root, new ViewGroup.LayoutParams(SCREEN_WIDTH, SCREEN_HEIGHT));
+        super.onCreate(savedInstanceState);
+    }
+
+    private static Resources generateResources() {
+        return new Resources.Builder()
+                .addIdToImageMapping(
+                        ICON_ID,
+                        new ImageResource.Builder()
+                                .setAndroidResourceByResId(
+                                        new AndroidImageResourceByResId.Builder()
+                                                .setResourceId(R.drawable.icon)
+                                                .build())
+                                .build())
+                .addIdToImageMapping(
+                        AVATAR,
+                        new ImageResource.Builder()
+                                .setAndroidResourceByResId(
+                                        new AndroidImageResourceByResId.Builder()
+                                                .setResourceId(R.drawable.avatar)
+                                                .build())
+                                .build())
+                .build();
+    }
+}
diff --git a/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png b/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png
new file mode 100644
index 0000000..a6da988
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/main/res/drawable/avatar.png
Binary files differ
diff --git a/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml b/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml
new file mode 100644
index 0000000..21eb853
--- /dev/null
+++ b/wear/protolayout/protolayout-material/src/main/res/drawable/icon.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:fillColor="#000"
+      android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+</vector>
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/types.proto b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
index c9ddacc..5d06b7c 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/types.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/types.proto
@@ -20,8 +20,11 @@
 
 // A string type.
 message StringProp {
-  // The value.
-  string value = 1;
+
+  oneof optional_value {
+    // The value.
+    string value = 1;
+  }
 
   // The dynamic value.
   androidx.wear.protolayout.expression.proto.DynamicString dynamic_value = 2;
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
index 3a01156..11ff17f 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/dynamicdata/ProtoLayoutDynamicDataPipeline.java
@@ -141,10 +141,10 @@
         if (enableAnimations) {
             this.mEvaluator =
                     new DynamicTypeEvaluator(
-                            canUpdateGateways, sensorGateway, stateStore, animationQuotaManager);
+                            canUpdateGateways, stateStore, animationQuotaManager, sensorGateway);
         } else {
             this.mEvaluator =
-                    new DynamicTypeEvaluator(canUpdateGateways, sensorGateway, stateStore);
+                    new DynamicTypeEvaluator(canUpdateGateways, stateStore, sensorGateway);
         }
     }
 
diff --git a/wear/protolayout/protolayout/api/current.txt b/wear/protolayout/protolayout/api/current.txt
index d4dbbd8..2a5ba9d 100644
--- a/wear/protolayout/protolayout/api/current.txt
+++ b/wear/protolayout/protolayout/api/current.txt
@@ -1095,13 +1095,27 @@
     method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
   }
 
+  public static final class TypeBuilders.StringLayoutConstraint {
+    method public int getAlignment();
+    method public String getPatternForLayout();
+  }
+
+  public static final class TypeBuilders.StringLayoutConstraint.Builder {
+    ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+  }
+
   public static final class TypeBuilders.StringProp {
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
     method public String getValue();
   }
 
   public static final class TypeBuilders.StringProp.Builder {
-    ctor public TypeBuilders.StringProp.Builder();
+    ctor @Deprecated public TypeBuilders.StringProp.Builder();
+    ctor public TypeBuilders.StringProp.Builder(String);
     method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
   }
 
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index 41ff5b2..6de2652 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -1243,13 +1243,27 @@
     method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
   }
 
+  public static final class TypeBuilders.StringLayoutConstraint {
+    method public int getAlignment();
+    method public String getPatternForLayout();
+  }
+
+  public static final class TypeBuilders.StringLayoutConstraint.Builder {
+    ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+  }
+
   public static final class TypeBuilders.StringProp {
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
     method public String getValue();
   }
 
   public static final class TypeBuilders.StringProp.Builder {
-    ctor public TypeBuilders.StringProp.Builder();
+    ctor @Deprecated public TypeBuilders.StringProp.Builder();
+    ctor public TypeBuilders.StringProp.Builder(String);
     method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
   }
 
diff --git a/wear/protolayout/protolayout/api/restricted_current.txt b/wear/protolayout/protolayout/api/restricted_current.txt
index d4dbbd8..2a5ba9d 100644
--- a/wear/protolayout/protolayout/api/restricted_current.txt
+++ b/wear/protolayout/protolayout/api/restricted_current.txt
@@ -1095,13 +1095,27 @@
     method public androidx.wear.protolayout.TypeBuilders.Int32Prop.Builder setValue(int);
   }
 
+  public static final class TypeBuilders.StringLayoutConstraint {
+    method public int getAlignment();
+    method public String getPatternForLayout();
+  }
+
+  public static final class TypeBuilders.StringLayoutConstraint.Builder {
+    ctor public TypeBuilders.StringLayoutConstraint.Builder(String);
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint build();
+    method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint.Builder setAlignment(int);
+  }
+
   public static final class TypeBuilders.StringProp {
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicString? getDynamicValue();
     method public String getValue();
   }
 
   public static final class TypeBuilders.StringProp.Builder {
-    ctor public TypeBuilders.StringProp.Builder();
+    ctor @Deprecated public TypeBuilders.StringProp.Builder();
+    ctor public TypeBuilders.StringProp.Builder(String);
     method public androidx.wear.protolayout.TypeBuilders.StringProp build();
+    method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setDynamicValue(androidx.wear.protolayout.expression.DynamicBuilders.DynamicString);
     method public androidx.wear.protolayout.TypeBuilders.StringProp.Builder setValue(String);
   }
 
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
index 0bcbc72..fd275b0 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/TypeBuilders.java
@@ -16,13 +16,17 @@
 
 package androidx.wear.protolayout;
 
+import static androidx.wear.protolayout.expression.Preconditions.checkNotNull;
+
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.Fingerprint;
+import androidx.wear.protolayout.proto.AlignmentProto;
 import androidx.wear.protolayout.proto.TypesProto;
 
 /** Builders for extensible primitive types used by layout elements. */
@@ -111,7 +115,7 @@
         }
 
         /**
-         * Gets the value.
+         * Gets the static value.
          *
          * @since 1.0
          */
@@ -120,6 +124,20 @@
             return mImpl.getValue();
         }
 
+        /**
+         * Gets the dynamic value.
+         *
+         * @since 1.2
+         */
+        @Nullable
+        public DynamicBuilders.DynamicString getDynamicValue() {
+            if (mImpl.hasDynamicValue()) {
+                return DynamicBuilders.dynamicStringFromProto(mImpl.getDynamicValue());
+            } else {
+                return null;
+            }
+        }
+
         /** Get the fingerprint for this object, or null if unknown. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Nullable
@@ -142,10 +160,26 @@
             private final TypesProto.StringProp.Builder mImpl = TypesProto.StringProp.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(327834307);
 
+            /**
+             * Creates an instance of {@link Builder}.
+             *
+             * @deprecated use {@link Builder(String)}
+             */
+            @Deprecated
             public Builder() {}
 
             /**
-             * Sets the value.
+             * Creates an instance of {@link Builder}.
+             *
+             * @param staticValue the static value.
+             */
+            public Builder(@NonNull String staticValue) {
+                setValue(staticValue);
+            }
+
+            /**
+             * Sets the static value. If a dynamic value is also set and the renderer supports
+             * dynamic values for the corresponding field, this static value will be ignored.
              *
              * @since 1.0
              */
@@ -156,15 +190,137 @@
                 return this;
             }
 
+            /**
+             * Sets the dynamic value. Note that when setting this value, the static value is still
+             * required to be set to support older renderers that only read the static value.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            public Builder setDynamicValue(@NonNull DynamicBuilders.DynamicString dynamicValue) {
+                mImpl.setDynamicValue(dynamicValue.toDynamicStringProto());
+                mFingerprint.recordPropertyUpdate(
+                        2, checkNotNull(dynamicValue.getFingerprint()).aggregateValueAsInt());
+                return this;
+            }
+
             /** Builds an instance from accumulated values. */
             @NonNull
             public StringProp build() {
+                if (mImpl.hasDynamicValue() && !mImpl.hasValue()) {
+                    throw new IllegalStateException("Static value is missing.");
+                }
                 return new StringProp(mImpl.build(), mFingerprint);
             }
         }
     }
 
     /**
+     * A type for specifying layout constraints when using {@link StringProp} on a data bindable
+     * layout element.
+     *
+     * @since 1.2
+     */
+    public static final class StringLayoutConstraint {
+        private final TypesProto.StringProp mImpl;
+        @Nullable private final Fingerprint mFingerprint;
+
+        StringLayoutConstraint(TypesProto.StringProp impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        /**
+         * Gets the text string to use as the pattern for the largest text that can be laid out.
+         * Used to ensure that the layout is of a known size during the layout pass. If
+         * not set defaults to the static value of the associated {@link StringProp} field.
+         *
+         * @since 1.2
+         */
+        @NonNull
+        public String getPatternForLayout() {
+            return mImpl.getValueForLayout();
+        }
+
+        /**
+         * Gets angular alignment of the actual content within the space reserved by value.
+         *
+         * @since 1.2
+         */
+        @LayoutElementBuilders.TextAlignment
+        public int getAlignment() {
+            return mImpl.getTextAlignmentForLayoutValue();
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Nullable
+        public Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public TypesProto.StringProp toProto() {
+            return mImpl;
+        }
+
+        @NonNull
+        static StringLayoutConstraint fromProto(@NonNull TypesProto.StringProp proto) {
+            return new StringLayoutConstraint(proto, null);
+        }
+
+        /** Builder for {@link StringLayoutConstraint}. */
+        public static final class Builder {
+            private final TypesProto.StringProp.Builder mImpl = TypesProto.StringProp.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(-1927567664);
+
+            /**
+             * Creates a new builder for {@link StringLayoutConstraint}.
+             *
+             * @param patternForLayout Sets the text string to use as the pattern for the largest
+             *                        text that can be laid out. Used to ensure that the layout
+             *                        is of a known size during the layout pass.
+             * @since 1.2
+             */
+            public Builder(@NonNull String patternForLayout) {
+                setValue(patternForLayout);
+            }
+
+            /**
+             * Sets the text string to use as the pattern for the largest text that can be laid out.
+             * Used to ensure that the layout is of a known size during the layout pass.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            private Builder setValue(@NonNull String patternForLayout) {
+                mImpl.setValueForLayout(patternForLayout);
+                mFingerprint.recordPropertyUpdate(3, patternForLayout.hashCode());
+                return this;
+            }
+
+            /**
+             * Sets alignment of the actual text within the space reserved by patternForMeasurement.
+             * If not specified, defaults to center alignment.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            public Builder setAlignment(@LayoutElementBuilders.TextAlignment int alignment) {
+                mImpl.setTextAlignmentForLayout(AlignmentProto.TextAlignment.forNumber(alignment));
+                mFingerprint.recordPropertyUpdate(4, alignment);
+                return this;
+            }
+
+            /** Builds an instance of {@link StringLayoutConstraint}. */
+            @NonNull
+            public StringLayoutConstraint build() {
+                return new StringLayoutConstraint(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
+    /**
      * A float type.
      *
      * @since 1.0
diff --git a/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java
new file mode 100644
index 0000000..21cac51
--- /dev/null
+++ b/wear/protolayout/protolayout/src/test/java/androidx/wear/protolayout/TypeBuildersTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.wear.protolayout.expression.DynamicBuilders;
+import androidx.wear.protolayout.proto.TypesProto;
+
+import org.junit.Test;
+
+public class TypeBuildersTest {
+    private static final String STATE_KEY = "state-key";
+    private static final TypeBuilders.StringProp STRING_PROP =
+            new TypeBuilders.StringProp.Builder("string")
+                    .setDynamicValue(DynamicBuilders.DynamicString.fromState(STATE_KEY))
+                    .build();
+
+    @SuppressWarnings("deprecation")
+    private static final TypeBuilders.StringProp.Builder STRING_PROP_BUILDER_WITHOUT_STATIC_VALUE =
+            new TypeBuilders.StringProp.Builder()
+                    .setDynamicValue(DynamicBuilders.DynamicString.fromState(STATE_KEY));
+
+    @Test
+    public void stringPropSupportsDynamicString() {
+        TypesProto.StringProp stringPropProto = STRING_PROP.toProto();
+
+        assertThat(stringPropProto.getValue()).isEqualTo(STRING_PROP.getValue());
+        assertThat(stringPropProto.getDynamicValue().getStateSource().getSourceKey())
+                .isEqualTo(STATE_KEY);
+    }
+
+    @Test
+    public void stringProp_withoutStaticValue_throws() {
+        assertThrows(IllegalStateException.class, STRING_PROP_BUILDER_WITHOUT_STATIC_VALUE::build);
+    }
+}
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index 7270552..1d8a312 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -36,6 +36,7 @@
     implementation "androidx.wear:wear:1.2.0"
 
     implementation(project(":wear:protolayout:protolayout"))
+    implementation(project(":wear:protolayout:protolayout-expression-pipeline"))
     implementation(project(":wear:protolayout:protolayout-renderer"))
     implementation(project(":wear:tiles:tiles"))
     implementation(project(":wear:tiles:tiles-proto"))
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 1432c9e..0cc77dc 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
@@ -26,6 +26,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StyleRes;
 import androidx.wear.protolayout.LayoutElementBuilders;
+import androidx.wear.protolayout.expression.pipeline.ObservableStateStore;
 import androidx.wear.protolayout.proto.LayoutElementProto;
 import androidx.wear.protolayout.proto.ResourceProto;
 import androidx.wear.protolayout.proto.StateProto;
@@ -34,6 +35,7 @@
 import androidx.wear.tiles.StateBuilders;
 import androidx.wear.tiles.TileService;
 
+import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.MoreExecutors;
 
@@ -115,6 +117,9 @@
         ProtoLayoutViewInstance.Config.Builder config =
                 new ProtoLayoutViewInstance.Config.Builder(uiContext, mUiExecutor, mUiExecutor,
                         TileService.EXTRA_CLICKABLE_ID)
+                        .setAnimationEnabled(true)
+                        .setIsViewFullyVisible(true)
+                        .setStateStore(new ObservableStateStore(ImmutableMap.of()))
                         .setLoadActionListener(instanceListener);
         this.mInstance = new ProtoLayoutViewInstance(config.build());
     }
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index fbe14a8..12d6cc7 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -47,8 +47,8 @@
 import androidx.wear.watchface.complications.datasource.ComplicationDataSourceService.ComplicationRequestListener
 import java.util.concurrent.CountDownLatch
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 
@@ -274,7 +274,7 @@
  */
 public abstract class ComplicationDataSourceService : Service() {
     private var wrapper: IComplicationProviderWrapper? = null
-    private var lastExpressionEvaluator: ComplicationDataExpressionEvaluator? = null
+    private var lastExpressionEvaluationJob: Job? = null
     internal val mainThreadHandler by lazy { createMainThreadHandler() }
     internal val mainThreadCoroutineScope by lazy {
         CoroutineScope(mainThreadHandler.asCoroutineDispatcher())
@@ -307,7 +307,7 @@
 
     override fun onDestroy() {
         super.onDestroy()
-        lastExpressionEvaluator?.close()
+        lastExpressionEvaluationJob?.cancel()
     }
 
     /**
@@ -554,7 +554,8 @@
                         }
 
                         private fun WireComplicationData?.evaluateAndUpdateManager() {
-                            lastExpressionEvaluator?.close() // Cancelling any previous evaluation.
+                            // Cancelling any previous evaluation.
+                            lastExpressionEvaluationJob?.cancel()
                             if (
                                 // Will be evaluated by the platform.
                                 // TODO(b/257422920): Set this to the exact platform version.
@@ -568,11 +569,14 @@
                                 )
                                 return
                             }
-                            lastExpressionEvaluator =
-                                ComplicationDataExpressionEvaluator(this).apply {
-                                    listenAndUpdateManager(
-                                        iComplicationManager,
+                            lastExpressionEvaluationJob =
+                                mainThreadCoroutineScope.launch {
+                                    iComplicationManager.updateComplicationData(
                                         complicationInstanceId,
+                                        // Doing one-off evaluation, the service will be re-invoked.
+                                        ComplicationDataExpressionEvaluator()
+                                            .evaluate(this@evaluateAndUpdateManager)
+                                            .first(),
                                     )
                                 }
                         }
@@ -581,22 +585,6 @@
             }
         }
 
-        private fun ComplicationDataExpressionEvaluator.listenAndUpdateManager(
-            iComplicationManager: IComplicationManager,
-            complicationInstanceId: Int,
-        ) {
-            mainThreadCoroutineScope.launch {
-                use {
-                    init()
-                    // Doing one-off evaluation, the service will be re-invoked.
-                    iComplicationManager.updateComplicationData(
-                        complicationInstanceId,
-                        data.filterNotNull().first()
-                    )
-                }
-            }
-        }
-
         @SuppressLint("SyntheticAccessor")
         override fun onComplicationDeactivated(complicationInstanceId: Int) {
             mainThreadHandler.post {
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 f496e85..f4d51da 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
@@ -20,7 +20,6 @@
 import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import androidx.annotation.RestrictTo
-import androidx.core.util.Consumer
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.protolayout.expression.pipeline.BoundDynamicType
 import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
@@ -29,16 +28,15 @@
 import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway
 import java.util.concurrent.Executor
 import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emitAll
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.mapNotNull
 import kotlinx.coroutines.flow.update
 
 /**
@@ -49,211 +47,104 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 class ComplicationDataExpressionEvaluator(
-    val unevaluatedData: WireComplicationData,
     private val stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
     private val sensorGateway: SensorGateway? = null,
     private val keepExpression: Boolean = false,
-) : AutoCloseable {
+) {
     /**
-     * Java compatibility class for [ComplicationDataExpressionEvaluator].
+     * Returns a [Flow] that provides the evaluated [WireComplicationData].
      *
-     * Unlike [data], [listener] is not invoked until there is a value (until [data] is non-null).
+     * The expression is evaluated _separately_ on each flow collection.
      */
-    class Compat
-    internal constructor(
-        val unevaluatedData: WireComplicationData,
-        private val listener: Consumer<WireComplicationData>,
-        stateStore: ObservableStateStore,
-        sensorGateway: SensorGateway?,
-        keepExpression: Boolean,
-    ) : AutoCloseable {
-        private val evaluator =
-            ComplicationDataExpressionEvaluator(
-                unevaluatedData,
-                stateStore,
-                sensorGateway,
-                keepExpression,
+    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.preUpdateCount == 0 -> it.data
+                                // Skipping states that are not ready for be emitted.
+                                else -> null
+                            }
+                        }
+                        .distinctUntilChanged()
+                emitAll(evaluatedData)
+            }
+        }
+
+    private suspend fun WireComplicationData.buildState() =
+        MutableStateFlow(State(this)).apply {
+            if (hasRangedValueExpression()) {
+                addReceiver(
+                    rangedValueExpression,
+                    expressionTrimmer = { setRangedValueExpression(null) },
+                    setter = { setRangedValue(it) },
+                )
+            }
+            if (hasLongText()) addReceiver(longText) { setLongText(it) }
+            if (hasLongTitle()) addReceiver(longTitle) { setLongTitle(it) }
+            if (hasShortText()) addReceiver(shortText) { setShortText(it) }
+            if (hasShortTitle()) addReceiver(shortTitle) { setShortTitle(it) }
+            if (hasContentDescription()) {
+                addReceiver(contentDescription) { setContentDescription(it) }
+            }
+            // Add all the receivers before we start binding them because binding can synchronously
+            // trigger the receiver, which would update the data before all the fields are
+            // evaluated.
+            value.initEvaluation()
+        }
+
+    private suspend fun MutableStateFlow<State>.addReceiver(
+        expression: DynamicFloat?,
+        expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+        setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
+    ) {
+        expression ?: return
+        val executor = currentCoroutineContext().asExecutor()
+        update { state ->
+            state.withPendingReceiver(
+                ComplicationEvaluationResultReceiver<Float>(
+                    this,
+                    setter = { value ->
+                        if (!keepExpression) expressionTrimmer(this)
+                        setter(this, value)
+                    },
+                    binder = { receiver -> value.evaluator.bind(expression, executor, receiver) },
+                )
             )
-
-        /** Builder for [ComplicationDataExpressionEvaluator.Compat]. */
-        class Builder(
-            private val unevaluatedData: WireComplicationData,
-            private val listener: Consumer<WireComplicationData>,
-        ) {
-            private var stateStore: ObservableStateStore = ObservableStateStore(emptyMap())
-            private var sensorGateway: SensorGateway? = null
-            private var keepExpression: Boolean = false
-
-            fun setStateStore(value: ObservableStateStore) = apply { stateStore = value }
-            fun setSensorGateway(value: SensorGateway?) = apply { sensorGateway = value }
-            fun setKeepExpression(value: Boolean) = apply { keepExpression = value }
-
-            fun build() =
-                Compat(unevaluatedData, listener, stateStore, sensorGateway, keepExpression)
-        }
-
-        /**
-         * @see ComplicationDataExpressionEvaluator.init, [executor] is used in place of
-         *   `coroutineScope`, and defaults to an immediate main-thread [Executor].
-         */
-        @JvmOverloads
-        fun init(executor: Executor = CoroutineScope(Dispatchers.Main.immediate).asExecutor()) {
-            evaluator.init(CoroutineScope(executor.asCoroutineDispatcher()))
-            evaluator.data
-                .filterNotNull()
-                .onEach(listener::accept)
-                .launchIn(CoroutineScope(executor.asCoroutineDispatcher()))
-        }
-
-        /** @see ComplicationDataExpressionEvaluator.close */
-        override fun close() {
-            evaluator.close()
         }
     }
 
-    private val _data = MutableStateFlow<WireComplicationData?>(null)
-
-    /**
-     * The evaluated data, or `null` if it wasn't evaluated yet, or [NoDataComplicationData] if it
-     * wasn't possible to evaluate the [unevaluatedData].
-     */
-    val data: StateFlow<WireComplicationData?> = _data.asStateFlow()
-
-    @Volatile // In case init() and close() are called on different threads.
-    private lateinit var evaluator: DynamicTypeEvaluator
-    private val state = MutableStateFlow(State(unevaluatedData))
-
-    /**
-     * Parses the expression and starts blocking evaluation.
-     *
-     * This needs to be called exactly once.
-     *
-     * @param coroutineScope used for background evaluation, must be single threaded
-     */
-    fun init(coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)) {
-        // Add all the receivers before we start binding them because binding can synchronously
-        // trigger the receiver, which would update the data before all the fields are evaluated.
-        Initializer(coroutineScope).init()
-    }
-
-    /**
-     * Stops evaluation.
-     *
-     * [data] will not change after this is called.
-     */
-    override fun close() {
-        for (receiver in state.value.all) receiver.close()
-        if (this::evaluator.isInitialized) evaluator.close()
-    }
-
-    private inner class Initializer(val coroutineScope: CoroutineScope) {
-        fun init() {
-            initStateReceivers()
-            initEvaluator()
-            monitorState()
-        }
-
-        /** Adds [ComplicationEvaluationResultReceiver]s to [state]. */
-        private fun initStateReceivers() {
-            val receivers = mutableSetOf<ComplicationEvaluationResultReceiver<out Any>>()
-
-            if (unevaluatedData.hasRangedValueExpression()) {
-                unevaluatedData.rangedValueExpression
-                    ?.buildReceiver(
-                        expressionTrimmer = { setRangedValueExpression(null) },
-                        setter = { setRangedValue(it) },
-                    )
-                    ?.let { receivers += it }
-            }
-            if (unevaluatedData.hasLongText()) {
-                unevaluatedData.longText?.buildReceiver { setLongText(it) }?.let { receivers += it }
-            }
-            if (unevaluatedData.hasLongTitle()) {
-                unevaluatedData.longTitle
-                    ?.buildReceiver { setLongTitle(it) }
-                    ?.let { receivers += it }
-            }
-            if (unevaluatedData.hasShortText()) {
-                unevaluatedData.shortText
-                    ?.buildReceiver { setShortText(it) }
-                    ?.let { receivers += it }
-            }
-            if (unevaluatedData.hasShortTitle()) {
-                unevaluatedData.shortTitle
-                    ?.buildReceiver { setShortTitle(it) }
-                    ?.let { receivers += it }
-            }
-            if (unevaluatedData.hasContentDescription()) {
-                unevaluatedData.contentDescription
-                    ?.buildReceiver { setContentDescription(it) }
-                    ?.let { receivers += it }
-            }
-
-            state.value = State(unevaluatedData, receivers)
-        }
-
-        private fun DynamicFloat.buildReceiver(
-            expressionTrimmer: WireComplicationData.Builder.() -> WireComplicationData.Builder,
-            setter: WireComplicationData.Builder.(Float) -> WireComplicationData.Builder,
-        ) =
-            ComplicationEvaluationResultReceiver(
-                setter = {
-                    if (!keepExpression) expressionTrimmer(this)
-                    setter(this, it)
-                },
-                binder = { receiver ->
-                    evaluator.bind(this@buildReceiver, coroutineScope.asExecutor(), receiver)
-                },
-            )
-
-        private fun WireComplicationText.buildReceiver(
-            setter:
-                WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
-        ) =
-            expression?.let { expression ->
+    private suspend fun MutableStateFlow<State>.addReceiver(
+        text: WireComplicationText?,
+        setter: WireComplicationData.Builder.(WireComplicationText) -> WireComplicationData.Builder,
+    ) {
+        val expression = text?.expression ?: return
+        val executor = currentCoroutineContext().asExecutor()
+        update {
+            it.withPendingReceiver(
                 ComplicationEvaluationResultReceiver<String>(
-                    setter = {
+                    this,
+                    setter = { value ->
                         setter(
                             if (keepExpression) {
-                                WireComplicationText(it, expression)
+                                WireComplicationText(value, expression)
                             } else {
-                                WireComplicationText(it)
+                                WireComplicationText(value)
                             }
                         )
                     },
                     binder = { receiver ->
-                        evaluator.bind(
-                            expression,
-                            ULocale.getDefault(),
-                            coroutineScope.asExecutor(),
-                            receiver
-                        )
+                        value.evaluator.bind(expression, ULocale.getDefault(), executor, receiver)
                     },
                 )
-            }
-
-        /** Initializes the internal [DynamicTypeEvaluator] if there are pending receivers. */
-        private fun initEvaluator() {
-            if (state.value.pending.isEmpty()) return
-            evaluator =
-                DynamicTypeEvaluator(
-                    /* platformDataSourcesInitiallyEnabled = */ true,
-                    sensorGateway,
-                    stateStore,
-                )
-            for (receiver in state.value.pending) receiver.bind()
-            for (receiver in state.value.pending) receiver.startEvaluation()
-            evaluator.enablePlatformDataSources()
-        }
-
-        /** Monitors [state] changes and updates [data]. */
-        private fun monitorState() {
-            state
-                .onEach {
-                    if (it.invalid.isNotEmpty()) _data.value = INVALID_DATA
-                    else if (it.pending.isEmpty() && it.preUpdateCount == 0) _data.value = it.data
-                }
-                .launchIn(coroutineScope)
+            )
         }
     }
 
@@ -261,14 +152,14 @@
      * Holds the state of the continuously evaluated [WireComplicationData] and the various
      * [ComplicationEvaluationResultReceiver] that are evaluating it.
      */
-    private class State(
+    private inner class State(
         val data: WireComplicationData,
-        val pending: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
-        val invalid: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
-        val complete: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+        val pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+        val invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
+        val completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> = setOf(),
         val preUpdateCount: Int = 0,
-    ) {
-        val all = pending + invalid + complete
+    ) : AutoCloseable {
+        lateinit var evaluator: DynamicTypeEvaluator
 
         init {
             require(preUpdateCount >= 0) {
@@ -276,38 +167,78 @@
             }
         }
 
-        fun withPreUpdate() =
-            State(
-                data,
-                pending = pending,
-                invalid = invalid,
-                complete = complete,
-                preUpdateCount + 1,
+        fun withPendingReceiver(receiver: ComplicationEvaluationResultReceiver<out Any>) =
+            copy(pendingReceivers = pendingReceivers + receiver)
+
+        fun withPreUpdate() = copy(preUpdateCount = preUpdateCount + 1)
+
+        fun withInvalidReceiver(receiver: ComplicationEvaluationResultReceiver<out Any>) =
+            copy(
+                pendingReceivers = pendingReceivers - receiver,
+                invalidReceivers = invalidReceivers + receiver,
+                completeReceivers = completeReceivers - receiver,
+                preUpdateCount = preUpdateCount - 1,
             )
 
-        fun withInvalid(receiver: ComplicationEvaluationResultReceiver<out Any>) =
-            State(
-                data,
-                pending = pending - receiver,
-                invalid = invalid + receiver,
-                complete = complete - receiver,
-                preUpdateCount - 1,
-            )
-
-        fun withUpdate(
+        fun withUpdatedData(
             data: WireComplicationData,
             receiver: ComplicationEvaluationResultReceiver<out Any>,
         ) =
-            State(
+            copy(
                 data,
-                pending = pending - receiver,
-                invalid = invalid - receiver,
-                complete = complete + receiver,
-                preUpdateCount - 1,
+                pendingReceivers = pendingReceivers - receiver,
+                invalidReceivers = invalidReceivers - receiver,
+                completeReceivers = completeReceivers + receiver,
+                preUpdateCount = preUpdateCount - 1,
+            )
+
+        /**
+         * Initializes the internal [DynamicTypeEvaluator] if there are pending receivers.
+         *
+         * Should be called after all receivers were added.
+         */
+        fun initEvaluation() {
+            if (pendingReceivers.isEmpty()) return
+            require(!this::evaluator.isInitialized) { "initEvaluator must be called exactly once." }
+            evaluator =
+                DynamicTypeEvaluator(
+                    /* platformDataSourcesInitiallyEnabled = */ true,
+                    stateStore,
+                    sensorGateway,
+                )
+            for (receiver in pendingReceivers) receiver.bind()
+            for (receiver in pendingReceivers) receiver.startEvaluation()
+            evaluator.enablePlatformDataSources()
+        }
+
+        override fun close() {
+            for (receiver in pendingReceivers + invalidReceivers + completeReceivers) {
+                receiver.close()
+            }
+            if (this::evaluator.isInitialized) evaluator.close()
+        }
+
+        private fun copy(
+            data: WireComplicationData = this.data,
+            pendingReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+                this.pendingReceivers,
+            invalidReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+                this.invalidReceivers,
+            completeReceivers: Set<ComplicationEvaluationResultReceiver<out Any>> =
+                this.completeReceivers,
+            preUpdateCount: Int = this.preUpdateCount,
+        ) =
+            State(
+                data = data,
+                pendingReceivers = pendingReceivers,
+                invalidReceivers = invalidReceivers,
+                completeReceivers = completeReceivers,
+                preUpdateCount = preUpdateCount,
             )
     }
 
     private inner class ComplicationEvaluationResultReceiver<T : Any>(
+        private val state: MutableStateFlow<State>,
         private val setter: WireComplicationData.Builder.(T) -> WireComplicationData.Builder,
         private val binder: (ComplicationEvaluationResultReceiver<T>) -> BoundDynamicType,
     ) : DynamicTypeValueReceiver<T>, AutoCloseable {
@@ -332,12 +263,15 @@
 
         override fun onData(newData: T) {
             state.update {
-                it.withUpdate(setter(WireComplicationData.Builder(it.data), newData).build(), this)
+                it.withUpdatedData(
+                    setter(WireComplicationData.Builder(it.data), newData).build(),
+                    this
+                )
             }
         }
 
         override fun onInvalidated() {
-            state.update { it.withInvalid(this) }
+            state.update { it.withInvalidReceiver(this) }
         }
     }
 
@@ -360,10 +294,10 @@
  * Replacement for CoroutineDispatcher.asExecutor extension due to
  * https://github.com/Kotlin/kotlinx.coroutines/pull/3683.
  */
-internal fun CoroutineScope.asExecutor() = Executor { runnable ->
-    val dispatcher = coroutineContext[ContinuationInterceptor] as CoroutineDispatcher
-    if (dispatcher.isDispatchNeeded(coroutineContext)) {
-        dispatcher.dispatch(coroutineContext, runnable)
+internal fun CoroutineContext.asExecutor() = Executor { runnable ->
+    val dispatcher = this[ContinuationInterceptor] as CoroutineDispatcher
+    if (dispatcher.isDispatchNeeded(this)) {
+        dispatcher.dispatch(this, runnable)
     } else {
         runnable.run()
     }
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 1ee050f..dbb6d56 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
@@ -16,12 +16,9 @@
 
 package androidx.wear.watchface.complications.data
 
-import android.os.Handler
-import android.os.Looper
 import android.support.wearable.complications.ComplicationData as WireComplicationData
 import android.support.wearable.complications.ComplicationText as WireComplicationText
 import android.util.Log
-import androidx.core.util.Consumer
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
 import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue
@@ -30,48 +27,33 @@
 import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
 import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
-import java.util.concurrent.Executor
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.runBlocking
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.kotlin.any
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.times
-import org.mockito.kotlin.verify
 import org.robolectric.shadows.ShadowLog
 
 @RunWith(SharedRobolectricTestRunner::class)
 class ComplicationDataExpressionEvaluatorTest {
     @get:Rule val expect = Expect.create()
 
-    private val listener = mock<Consumer<WireComplicationData>>()
-
     @Before
     fun setup() {
         ShadowLog.setLoggable("ComplicationData", Log.DEBUG)
     }
 
     @Test
-    fun data_notInitialized_setToNull() {
-        ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
-            assertThat(evaluator.data.value).isNull()
-        }
-    }
+    fun evaluate_noExpression_returnsUnevaluated() = runBlocking {
+        val evaluator = ComplicationDataExpressionEvaluator()
 
-    @Test
-    fun data_noExpression_setToUnevaluated() {
-        ComplicationDataExpressionEvaluator(DATA_WITH_NO_EXPRESSION).use { evaluator ->
-            evaluator.init()
-
-            assertThat(evaluator.data.value).isEqualTo(DATA_WITH_NO_EXPRESSION)
-        }
+        assertThat(evaluator.evaluate(DATA_WITH_NO_EXPRESSION).firstOrNull())
+            .isEqualTo(DATA_WITH_NO_EXPRESSION)
     }
 
     /**
@@ -208,47 +190,34 @@
     }
 
     @Test
-    fun data_withExpression_setToEvaluated() {
+    fun evaluate_withExpression_returnsEvaluated() = runBlocking {
         for (scenario in DataWithExpressionScenario.values()) {
             // Defensive copy due to in-place evaluation.
             val expressed = WireComplicationData.Builder(scenario.expressed).build()
             val stateStore = ObservableStateStore(mapOf())
-            ComplicationDataExpressionEvaluator(expressed, stateStore).use { evaluator ->
-                val allEvaluations =
-                    evaluator.data
-                        .filterNotNull()
-                        .shareIn(
-                            CoroutineScope(Dispatchers.Main.immediate),
-                            SharingStarted.Eagerly,
-                            replay = 10,
-                        )
-                evaluator.init()
+            val evaluator = ComplicationDataExpressionEvaluator(stateStore)
+            val allEvaluations =
+                evaluator
+                    .evaluate(expressed)
+                    .shareIn(
+                        CoroutineScope(Dispatchers.Main.immediate),
+                        SharingStarted.Eagerly,
+                        replay = 10,
+                    )
 
-                for (state in scenario.states) {
-                    stateStore.setStateEntryValues(state)
-                }
-
-                expect
-                    .withMessage(scenario.name)
-                    .that(allEvaluations.replayCache)
-                    .isEqualTo(scenario.evaluated)
+            for (state in scenario.states) {
+                stateStore.setStateEntryValues(state)
             }
+
+            expect
+                .withMessage(scenario.name)
+                .that(allEvaluations.replayCache)
+                .isEqualTo(scenario.evaluated)
         }
     }
 
     @Test
-    fun data_keepExpression_doesNotTrimUnevaluatedExpression() {
-        class MainExecutor : Executor {
-            private val handler = Handler(Looper.getMainLooper())
-
-            override fun execute(runnable: Runnable) {
-                if (handler.looper == Looper.myLooper()) {
-                    runnable.run()
-                } else {
-                    handler.post(runnable)
-                }
-            }
-        }
+    fun evaluate_keepExpression_doesNotTrimUnevaluatedExpression() = runBlocking {
         val expressed =
             WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
                 .setRangedValueExpression(DynamicFloat.constant(1f))
@@ -258,38 +227,30 @@
                 .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
                 .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
                 .build()
-        ComplicationDataExpressionEvaluator(expressed, keepExpression = true).use { evaluator ->
-            evaluator.init()
+        val evaluator = ComplicationDataExpressionEvaluator(keepExpression = true)
 
-            assertThat(evaluator.data.value)
-                .isEqualTo(
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
-                        .setRangedValue(1f)
-                        .setRangedValueExpression(DynamicFloat.constant(1f))
-                        .setLongText(
-                            WireComplicationText("Long Text", DynamicString.constant("Long Text"))
-                        )
-                        .setLongTitle(
-                            WireComplicationText("Long Title", DynamicString.constant("Long Title"))
-                        )
-                        .setShortText(
-                            WireComplicationText("Short Text", DynamicString.constant("Short Text"))
-                        )
-                        .setShortTitle(
-                            WireComplicationText(
-                                "Short Title",
-                                DynamicString.constant("Short Title")
-                            )
-                        )
-                        .setContentDescription(
-                            WireComplicationText(
-                                "Description",
-                                DynamicString.constant("Description")
-                            )
-                        )
-                        .build()
-                )
-        }
+        assertThat(evaluator.evaluate(expressed).firstOrNull())
+            .isEqualTo(
+                WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+                    .setRangedValue(1f)
+                    .setRangedValueExpression(DynamicFloat.constant(1f))
+                    .setLongText(
+                        WireComplicationText("Long Text", DynamicString.constant("Long Text"))
+                    )
+                    .setLongTitle(
+                        WireComplicationText("Long Title", DynamicString.constant("Long Title"))
+                    )
+                    .setShortText(
+                        WireComplicationText("Short Text", DynamicString.constant("Short Text"))
+                    )
+                    .setShortTitle(
+                        WireComplicationText("Short Title", DynamicString.constant("Short Title"))
+                    )
+                    .setContentDescription(
+                        WireComplicationText("Description", DynamicString.constant("Description"))
+                    )
+                    .build()
+            )
     }
 
     enum class HasExpressionDataWithExpressionScenario(val data: WireComplicationData) {
@@ -337,43 +298,6 @@
         assertThat(hasExpression(DATA_WITH_NO_EXPRESSION)).isFalse()
     }
 
-    @Test
-    fun compat_notInitialized_listenerNotInvoked() {
-        ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
-            .build()
-            .use { verify(listener, never()).accept(any()) }
-    }
-
-    @Test
-    fun compat_noExpression_listenerInvokedWithData() {
-        ComplicationDataExpressionEvaluator.Compat.Builder(DATA_WITH_NO_EXPRESSION, listener)
-            .build()
-            .use { evaluator ->
-                evaluator.init()
-
-                verify(listener, times(1)).accept(DATA_WITH_NO_EXPRESSION)
-            }
-    }
-
-    @Test
-    fun compat_expression_listenerInvokedWithEvaluatedData() {
-        val data =
-            WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
-                .setRangedValueExpression(DynamicFloat.constant(1f))
-                .build()
-        ComplicationDataExpressionEvaluator.Compat.Builder(data, listener).build().use { evaluator
-            ->
-            evaluator.init()
-
-            verify(listener, times(1))
-                .accept(
-                    WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
-                        .setRangedValue(1f)
-                        .build()
-                )
-        }
-    }
-
     private companion object {
         val DATA_WITH_NO_EXPRESSION =
             WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index a196c02..4122a7d 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -1078,6 +1078,9 @@
 
             iWatchFaceService = IWatchFaceService.Stub.asInterface(binder)
 
+            // A ParameterlessEngine doesn't exist in WSL flow.
+            InteractiveInstanceManager.setParameterlessEngine(null)
+
             try {
                 // Note if the implementation doesn't support getVersion this will return zero
                 // rather than throwing an exception.
@@ -1346,7 +1349,7 @@
                     return
                 }
 
-                val pendingWallpaperInstance =
+                var pendingWallpaperInstance =
                     InteractiveInstanceManager.takePendingWallpaperInteractiveWatchFaceInstance()
 
                 // In a direct boot scenario attempt to load the previously serialized parameters.
@@ -1376,16 +1379,32 @@
                                 ?.let {
                                     Log.e(
                                         TAG,
-                                        "takePendingWallpaperInteractiveWatchFaceInstance failed"
+                                        "takePendingWallpaperInteractiveWatchFaceInstance failed",
+                                        e
                                     )
                                     it.callback.onInteractiveWatchFaceCrashed(CrashInfoParcel(e))
                                 }
                         } finally {
                             asyncTraceEvent.close()
                         }
+
+                        return
                     }
                 }
 
+                if (pendingWallpaperInstance == null) {
+                    // In this case we don't have any watchface parameters, probably because a WSL
+                    // watchface has been upgraded to an AndroidX one. The system has either just
+                    // racily attempted to connect (in which case we should carry on normally) or it
+                    // probably will connect at a later time. In the latter case we should
+                    // register a parameterless engine to allow the subsequent connection to
+                    // succeed.
+                    pendingWallpaperInstance = InteractiveInstanceManager
+                        .setParameterlessEngineOrTakePendingWallpaperInteractiveWatchFaceInstance(
+                            this
+                        )
+                }
+
                 // If there's a pending WallpaperInteractiveWatchFaceInstance then create it.
                 if (pendingWallpaperInstance != null) {
                     val asyncTraceEvent =
@@ -1402,7 +1421,7 @@
                             )
                             instance
                         } catch (e: Exception) {
-                            Log.e(TAG, "createInteractiveInstance failed")
+                            Log.e(TAG, "createInteractiveInstance failed", e)
                             pendingWallpaperInstance.callback.onInteractiveWatchFaceCrashed(
                                 CrashInfoParcel(e)
                             )
@@ -1434,6 +1453,29 @@
                 }
             }
 
+        /** Attaches to a parameterlessEngine if we're completely uninitialized. */
+        @SuppressWarnings("NewApi")
+        internal fun attachToParameterlessEngine(
+            pendingWallpaperInstance:
+            InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance
+        ) {
+            uiThreadCoroutineScope.launch {
+                try {
+                    pendingWallpaperInstance.callback.onInteractiveWatchFaceCreated(
+                        createInteractiveInstance(
+                            pendingWallpaperInstance.params,
+                            "attachToParameterlessEngine"
+                        )
+                    )
+                } catch (e: Exception) {
+                    Log.e(TAG, "attachToParameterlessEngine failed", e)
+                    pendingWallpaperInstance.callback.onInteractiveWatchFaceCrashed(
+                        CrashInfoParcel(e)
+                    )
+                }
+            }
+        }
+
         @UiThread
         internal fun ambientTickUpdate(): Unit =
             TraceEvent("EngineWrapper.ambientTickUpdate").use {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
index bcbafd1..1dbbc9a 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/InteractiveInstanceManager.kt
@@ -20,6 +20,7 @@
 import androidx.annotation.UiThread
 import androidx.annotation.VisibleForTesting
 import androidx.wear.watchface.IndentingPrintWriter
+import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.control.data.WallpaperInteractiveWatchFaceInstanceParams
 import androidx.wear.watchface.utility.TraceEvent
 
@@ -53,6 +54,7 @@
         private var pendingWallpaperInteractiveWatchFaceInstance:
             PendingWallpaperInteractiveWatchFaceInstance? =
             null
+        private var parameterlessEngine: WatchFaceService.EngineWrapper? = null
 
         @VisibleForTesting
         fun getInstances() =
@@ -70,6 +72,54 @@
             }
         }
 
+        /**
+         * We either return the pendingWallpaperInteractiveWatchFaceInstance if there is one or
+         * set parameterlessEngine. A parameterless engine, is one that's been created without any
+         * start up params. Typically this can only happen if a WSL watchface is upgraded to an
+         * androidx one, so WallpaperManager knows about it but WearServices/WSL does not.
+         */
+        @SuppressLint("SyntheticAccessor")
+        fun setParameterlessEngineOrTakePendingWallpaperInteractiveWatchFaceInstance(
+            parameterlessEngine: WatchFaceService.EngineWrapper?
+        ): PendingWallpaperInteractiveWatchFaceInstance? {
+            synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+                require(this.parameterlessEngine == null || parameterlessEngine == null) {
+                    "Already have a parameterlessEngine registered"
+                }
+
+                if (pendingWallpaperInteractiveWatchFaceInstance == null) {
+                    this.parameterlessEngine = parameterlessEngine
+                    return null
+                } else {
+                    val returnValue = pendingWallpaperInteractiveWatchFaceInstance
+                    pendingWallpaperInteractiveWatchFaceInstance = null
+                    return returnValue
+                }
+            }
+        }
+
+        /**
+         * A parameterless engine, is one that's been created without any start up params. Typically
+         * this can only happen if a WSL watchface is upgraded to an androidx one, so
+         * WallpaperManager knows about it but WearServices/WSL does not.
+         */
+        @SuppressLint("SyntheticAccessor")
+        fun setParameterlessEngine(parameterlessEngine: WatchFaceService.EngineWrapper?) {
+            synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+                require(this.parameterlessEngine == null || parameterlessEngine == null) {
+                    "Already have a parameterlessEngine registered"
+                }
+                this.parameterlessEngine = parameterlessEngine
+            }
+        }
+
+        @SuppressLint("SyntheticAccessor")
+        fun getParameterlessEngine(): WatchFaceService.EngineWrapper? {
+            synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
+                return parameterlessEngine
+            }
+        }
+
         @SuppressLint("SyntheticAccessor")
         fun getAndRetainInstance(instanceId: String): InteractiveWatchFaceImpl? {
             synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
@@ -121,12 +171,22 @@
             val impl =
                 synchronized(pendingWallpaperInteractiveWatchFaceInstanceLock) {
                     val instance = instances[value.params.instanceId]
+
                     if (instance == null) {
+                        parameterlessEngine?.let {
+                            parameterlessEngine = null
+                            it.attachToParameterlessEngine(value)
+                            return null
+                        }
+
                         TraceEvent("Set pendingWallpaperInteractiveWatchFaceInstance").use {
                             pendingWallpaperInteractiveWatchFaceInstance = value
                         }
                         return null
                     }
+                    if (instance.impl.engine == parameterlessEngine) {
+                        parameterlessEngine = null
+                    }
                     instance.impl
                 }
 
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
index a48ba9c..01c3acb 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/AsyncWatchFaceInitTest.kt
@@ -183,6 +183,7 @@
 
     @After
     fun tearDown() {
+        assertThat(InteractiveInstanceManager.getParameterlessEngine()).isNull()
         InteractiveInstanceManager.releaseInstance(initParams.instanceId)
         assertThat(InteractiveInstanceManager.getInstances()).isEmpty()
     }
@@ -228,6 +229,9 @@
         runPostedTasksFor(0)
 
         assertThat(pendingException.message).startsWith("WatchFace already exists!")
+
+        // Tidy up.
+        InteractiveInstanceManager.setParameterlessEngine(null)
     }
 
     @Test
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 140549e..d6870d7 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -751,6 +751,7 @@
         }
 
         assertThat(InteractiveInstanceManager.getInstances()).isEmpty()
+        assertThat(InteractiveInstanceManager.getParameterlessEngine()).isNull()
         validateMockitoUsage()
     }
 
@@ -3756,6 +3757,21 @@
     @Config(sdk = [Build.VERSION_CODES.R])
     public fun directBoot() {
         val instanceId = "DirectBootInstance"
+        val params = WallpaperInteractiveWatchFaceInstanceParams(
+            instanceId,
+            DeviceConfig(false, false, 0, 0),
+            WatchUiState(false, 0),
+            UserStyle(
+                hashMapOf(
+                    colorStyleSetting to blueStyleOption,
+                    watchHandStyleSetting to gothicStyleOption
+                )
+            )
+                .toWireFormat(),
+            null,
+            null,
+            null
+        )
         testWatchFaceService =
             TestWatchFaceService(
                 WatchFaceType.ANALOG,
@@ -3772,21 +3788,7 @@
                 watchState,
                 handler,
                 null,
-                WallpaperInteractiveWatchFaceInstanceParams(
-                    instanceId,
-                    DeviceConfig(false, false, 0, 0),
-                    WatchUiState(false, 0),
-                    UserStyle(
-                            hashMapOf(
-                                colorStyleSetting to blueStyleOption,
-                                watchHandStyleSetting to gothicStyleOption
-                            )
-                        )
-                        .toWireFormat(),
-                    null,
-                    null,
-                    null
-                ),
+                params,
                 choreographer
             )
 
@@ -3794,15 +3796,34 @@
         engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
 
         // This increments refcount to 2
-        val instance = InteractiveInstanceManager.getAndRetainInstance(instanceId)
+        val instance =
+            InteractiveInstanceManager
+                .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
+                    InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
+                        params,
+                        object : IPendingInteractiveWatchFace.Stub() {
+                            override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+                            override fun onInteractiveWatchFaceCreated(
+                                iInteractiveWatchFace: IInteractiveWatchFace
+                            ) {
+                                fail("Shouldn't get called")
+                            }
+
+                            override fun onInteractiveWatchFaceCrashed(
+                                exception: CrashInfoParcel?
+                            ) {
+                                fail("WatchFace crashed: $exception")
+                            }
+                        }
+                    )
+                )
         assertThat(instance).isNotNull()
         watchFaceImpl = engineWrapper.getWatchFaceImplOrNull()!!
         val userStyle = watchFaceImpl.currentUserStyleRepository.userStyle.value
         assertThat(userStyle[colorStyleSetting]).isEqualTo(blueStyleOption)
         assertThat(userStyle[watchHandStyleSetting]).isEqualTo(gothicStyleOption)
         instance!!.release()
-
-        InteractiveInstanceManager.releaseInstance(instanceId)
     }
 
     @Test
@@ -6523,6 +6544,78 @@
         assertThat(HeadlessWatchFaceImpl.headlessInstances).isEmpty()
     }
 
+    @Test
+    public fun attachToExistingParameterlessEngine() {
+        // Construct a parameterless engine.
+        testWatchFaceService =
+            TestWatchFaceService(
+                WatchFaceType.DIGITAL,
+                emptyList(),
+                { _, currentUserStyleRepository, watchState ->
+                    renderer =
+                        TestRenderer(
+                            surfaceHolder,
+                            currentUserStyleRepository,
+                            watchState,
+                            INTERACTIVE_UPDATE_RATE_MS
+                        )
+                    renderer
+                },
+                UserStyleSchema(emptyList()),
+                watchState,
+                handler,
+                null,
+                null,
+                choreographer,
+                mockSystemTimeMillis = looperTimeMillis,
+                complicationCache = null
+            )
+
+        engineWrapper = testWatchFaceService.onCreateEngine() as WatchFaceService.EngineWrapper
+        engineWrapper.onCreate(surfaceHolder)
+        engineWrapper.onSurfaceChanged(surfaceHolder, 0, 100, 100)
+        engineWrapper.onVisibilityChanged(true)
+
+        val callback = object : IPendingInteractiveWatchFace.Stub() {
+            override fun getApiVersion() = IPendingInteractiveWatchFace.API_VERSION
+
+            override fun onInteractiveWatchFaceCreated(
+                iInteractiveWatchFace: IInteractiveWatchFace
+            ) {
+                interactiveWatchFaceInstance = iInteractiveWatchFace
+            }
+
+            override fun onInteractiveWatchFaceCrashed(exception: CrashInfoParcel?) {
+                fail("WatchFace crashed: $exception")
+            }
+        }
+
+        InteractiveInstanceManager
+            .getExistingInstanceOrSetPendingWallpaperInteractiveWatchFaceInstance(
+                InteractiveInstanceManager.PendingWallpaperInteractiveWatchFaceInstance(
+                    WallpaperInteractiveWatchFaceInstanceParams(
+                        INTERACTIVE_INSTANCE_ID,
+                        DeviceConfig(false, false, 0, 0),
+                        WatchUiState(false, 0),
+                        UserStyle(emptyMap()).toWireFormat(),
+                        null,
+                        null,
+                        null
+                    ),
+                    callback
+                )
+             )
+
+        runBlocking {
+            watchFaceImpl = engineWrapper.deferredWatchFaceImpl.awaitWithTimeout()
+            engineWrapper.deferredValidation.awaitWithTimeout()
+        }
+
+        assertThat(this::interactiveWatchFaceInstance.isInitialized).isTrue()
+        assertThat(watchFaceImpl.renderer.watchState.watchFaceInstanceId.value)
+            .isEqualTo(INTERACTIVE_INSTANCE_ID)
+    }
+
     private fun getLeftShortTextComplicationDataText(): CharSequence {
         val complication =
             complicationSlotsManager[LEFT_COMPLICATION_ID]!!.complicationData.value