Merge "Polish the documentation for BasicTextField2." into androidx-main
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
index 1a917ef..34b6889 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutor.kt
@@ -16,6 +16,9 @@
 
 package androidx.appactions.interaction.capabilities.core
 
+import androidx.annotation.RestrictTo
+import androidx.concurrent.futures.await
+
 /**
  * An interface of executing the action.
  *
@@ -23,6 +26,10 @@
  * For a Future-based solution, see ActionExecutorAsync.
  */
 fun interface ActionExecutor<ArgumentsT, OutputT> {
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    val uiHandle: Any
+        get() = this
+
     /**
      * Calls to execute the action.
      *
@@ -31,3 +38,10 @@
      */
     suspend fun onExecute(arguments: ArgumentsT): ExecutionResult<OutputT>
 }
+
+internal fun <ArgumentsT, OutputT> ActionExecutorAsync<ArgumentsT, OutputT>.toActionExecutor():
+    ActionExecutor<ArgumentsT, OutputT> = object : ActionExecutor<ArgumentsT, OutputT> {
+    override val uiHandle = this@toActionExecutor
+    override suspend fun onExecute(arguments: ArgumentsT): ExecutionResult<OutputT> =
+        [email protected](arguments).await()
+}
\ No newline at end of file
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt
index 7958e11..d7181f6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/ActionExecutorAsync.kt
@@ -16,16 +16,10 @@
 
 package androidx.appactions.interaction.capabilities.core
 
-import androidx.annotation.RestrictTo
-import androidx.appactions.interaction.capabilities.core.impl.concurrent.convertToListenableFuture
 import com.google.common.util.concurrent.ListenableFuture
 
 /** An ListenableFuture-based interface of executing an action. */
 fun interface ActionExecutorAsync<ArgumentsT, OutputT> {
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
-    val uiHandle: Any
-        get() = this
-
     /**
      * Calls to execute the action.
      *
@@ -33,18 +27,4 @@
      * @return A ListenableFuture containing the ExecutionResult
      */
     fun onExecute(arguments: ArgumentsT): ListenableFuture<ExecutionResult<OutputT>>
-
-    companion object {
-        fun <ArgumentsT, OutputT> ActionExecutor<ArgumentsT, OutputT>.toActionExecutorAsync():
-            ActionExecutorAsync<ArgumentsT, OutputT> =
-            object : ActionExecutorAsync<ArgumentsT, OutputT> {
-                override val uiHandle = this@toActionExecutorAsync
-                override fun onExecute(
-                    arguments: ArgumentsT,
-                ): ListenableFuture<ExecutionResult<OutputT>> =
-                    convertToListenableFuture("ActionExecutor#execute") {
-                        [email protected](arguments)
-                    }
-            }
-    }
 }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
index fd3a716..2075267 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/Capability.kt
@@ -17,7 +17,6 @@
 package androidx.appactions.interaction.capabilities.core
 
 import androidx.annotation.RestrictTo
-import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync.Companion.toActionExecutorAsync
 import androidx.appactions.interaction.capabilities.core.impl.CapabilitySession
 import androidx.appactions.interaction.capabilities.core.impl.SingleTurnCapabilityImpl
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
@@ -66,7 +65,8 @@
             ArgumentsT,
             OutputT,
             ConfirmationT,
-            ExecutionSessionT,>,
+            ExecutionSessionT,
+            >,
         PropertyT,
         ArgumentsT,
         OutputT,
@@ -77,7 +77,7 @@
     ) {
         private var id: String? = null
         private var property: PropertyT? = null
-        private var actionExecutorAsync: ActionExecutorAsync<ArgumentsT, OutputT>? = null
+        private var actionExecutor: ActionExecutor<ArgumentsT, OutputT>? = null
         private var sessionFactory: ExecutionSessionFactory<ExecutionSessionT>? = null
 
         /**
@@ -120,7 +120,7 @@
          * which accepts the ActionExecutorAsync instead.
          */
         fun setExecutor(actionExecutor: ActionExecutor<ArgumentsT, OutputT>) = asBuilder().apply {
-            this.actionExecutorAsync = actionExecutor.toActionExecutorAsync()
+            this.actionExecutor = actionExecutor
         }
 
         /**
@@ -134,7 +134,7 @@
         fun setExecutor(
             actionExecutorAsync: ActionExecutorAsync<ArgumentsT, OutputT>,
         ) = asBuilder().apply {
-            this.actionExecutorAsync = actionExecutorAsync
+            this.actionExecutor = actionExecutorAsync.toActionExecutor()
         }
 
         /**
@@ -154,12 +154,12 @@
         open fun build(): Capability {
             val checkedId = requireNotNull(id) { "setId must be called before build" }
             val checkedProperty = requireNotNull(property) { "property must not be null." }
-            if (actionExecutorAsync != null) {
+            if (actionExecutor != null) {
                 return SingleTurnCapabilityImpl(
                     checkedId,
                     actionSpec,
                     checkedProperty,
-                    actionExecutorAsync!!,
+                    actionExecutor!!,
                 )
             } else {
                 val checkedSessionFactory = requireNotNull(sessionFactory) {
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
index 0fed7e9..826b7da 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilityImpl.kt
@@ -17,8 +17,8 @@
 package androidx.appactions.interaction.capabilities.core.impl
 
 import androidx.annotation.RestrictTo
+import androidx.appactions.interaction.capabilities.core.ActionExecutor
 import androidx.appactions.interaction.capabilities.core.Capability
-import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
 import androidx.appactions.interaction.capabilities.core.HostProperties
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
 import androidx.appactions.interaction.proto.AppActionsContext.AppAction
@@ -35,7 +35,7 @@
     id: String,
     val actionSpec: ActionSpec<PropertyT, ArgumentsT, OutputT>,
     val property: PropertyT,
-    val actionExecutorAsync: ActionExecutorAsync<ArgumentsT, OutputT>,
+    val actionExecutor: ActionExecutor<ArgumentsT, OutputT>,
 ) : Capability(id) {
     private val mutex = Mutex()
 
@@ -52,7 +52,7 @@
         return SingleTurnCapabilitySession(
             sessionId,
             actionSpec,
-            actionExecutorAsync,
+            actionExecutor,
             mutex,
         )
     }
diff --git a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
index 40081e8..72c2c03 100644
--- a/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
+++ b/appactions/interaction/interaction-capabilities-core/src/main/java/androidx/appactions/interaction/capabilities/core/impl/SingleTurnCapabilitySession.kt
@@ -17,13 +17,12 @@
 package androidx.appactions.interaction.capabilities.core.impl
 
 import androidx.annotation.RestrictTo
-import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
+import androidx.appactions.interaction.capabilities.core.ActionExecutor
 import androidx.appactions.interaction.capabilities.core.ExecutionResult
 import androidx.appactions.interaction.capabilities.core.impl.spec.ActionSpec
 import androidx.appactions.interaction.proto.AppActionsContext.AppDialogState
 import androidx.appactions.interaction.proto.FulfillmentResponse
 import androidx.appactions.interaction.proto.ParamValue
-import androidx.concurrent.futures.await
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.Dispatchers
@@ -39,10 +38,10 @@
 internal class SingleTurnCapabilitySession<
     ArgumentsT,
     OutputT,
->(
+    >(
     override val sessionId: String,
     private val actionSpec: ActionSpec<*, ArgumentsT, OutputT>,
-    private val actionExecutorAsync: ActionExecutorAsync<ArgumentsT, OutputT>,
+    private val actionExecutor: ActionExecutor<ArgumentsT, OutputT>,
     private val mutex: Mutex,
     private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
 ) : CapabilitySession {
@@ -55,7 +54,7 @@
             throw UnsupportedOperationException()
         }
 
-    override val uiHandle: Any = actionExecutorAsync.uiHandle
+    override val uiHandle: Any = actionExecutor.uiHandle
 
     override fun destroy() {}
 
@@ -75,7 +74,7 @@
             try {
                 mutex.lock(owner = this@SingleTurnCapabilitySession)
                 UiHandleRegistry.registerUiHandle(uiHandle, sessionId)
-                val output = actionExecutorAsync.onExecute(arguments).await()
+                val output = actionExecutor.onExecute(arguments)
                 callback.onSuccess(convertToFulfillmentResponse(output))
             } catch (t: Throwable) {
                 callback.onError(ErrorStatusInternal.CANCELLED)
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 de130535..26b6158 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
@@ -19,7 +19,7 @@
 import android.util.SizeF
 import androidx.appactions.interaction.capabilities.core.ActionExecutor
 import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync
-import androidx.appactions.interaction.capabilities.core.ActionExecutorAsync.Companion.toActionExecutorAsync
+import androidx.appactions.interaction.capabilities.core.toActionExecutor
 import androidx.appactions.interaction.capabilities.core.ExecutionResult
 import androidx.appactions.interaction.capabilities.core.HostProperties
 import androidx.appactions.interaction.capabilities.core.impl.concurrent.Futures
@@ -70,12 +70,12 @@
                 actionSpec = ACTION_SPEC,
                 property =
                 Properties.newBuilder()
-                        .setRequiredEntityField(
-                            Property.Builder<Entity>().build(),
-                        )
-                        .setOptionalStringField(Property.prohibited())
-                        .build(),
-                actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
+                    .setRequiredEntityField(
+                        Property.Builder<Entity>().build(),
+                    )
+                    .setOptionalStringField(Property.prohibited())
+                    .build(),
+                actionExecutor = actionExecutor,
             )
 
         val capabilitySession = capability.createSession(fakeSessionId, hostProperties)
@@ -124,12 +124,12 @@
                 actionSpec = ACTION_SPEC,
                 property =
                 Properties.newBuilder()
-                        .setRequiredEntityField(
-                            Property.Builder<Entity>().build(),
-                        )
-                        .setOptionalStringField(Property.prohibited())
-                        .build(),
-                actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
+                    .setRequiredEntityField(
+                        Property.Builder<Entity>().build(),
+                    )
+                    .setOptionalStringField(Property.prohibited())
+                    .build(),
+                actionExecutor = actionExecutor,
             )
 
         val capabilitySession = capability.createSession(fakeSessionId, hostProperties)
@@ -163,7 +163,7 @@
                         Property.Builder<Entity>().build(),
                     )
                     .build(),
-                actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
+                actionExecutor = actionExecutor,
             )
         val session = capability.createSession(fakeSessionId, hostProperties)
         assertThat(session.uiHandle).isSameInstanceAs(actionExecutor)
@@ -185,7 +185,7 @@
                         Property.Builder<Entity>().build(),
                     )
                     .build(),
-                actionExecutorAsync = actionExecutorAsync,
+                actionExecutor = actionExecutorAsync.toActionExecutor(),
             )
         val session = capability.createSession(fakeSessionId, hostProperties)
         assertThat(session.uiHandle).isSameInstanceAs(actionExecutorAsync)
@@ -207,7 +207,7 @@
             property = Properties.newBuilder().setRequiredEntityField(
                 Property.Builder<Entity>().build(),
             ).build(),
-            actionExecutorAsync = actionExecutor.toActionExecutorAsync(),
+            actionExecutor = actionExecutor,
         )
         val session1 = capability.createSession("session1", hostProperties)
         val session2 = capability.createSession("session2", hostProperties)
diff --git a/bluetooth/integration-tests/testapp/build.gradle b/bluetooth/integration-tests/testapp/build.gradle
index 441d909..860d815 100644
--- a/bluetooth/integration-tests/testapp/build.gradle
+++ b/bluetooth/integration-tests/testapp/build.gradle
@@ -48,17 +48,17 @@
     implementation(project(":bluetooth:bluetooth"))
 
     implementation("androidx.core:core-ktx:1.9.0")
-    implementation("androidx.appcompat:appcompat:1.6.0")
+    implementation("androidx.appcompat:appcompat:1.6.1")
 
     implementation(libs.material)
 
-    implementation("androidx.activity:activity-ktx:1.6.1")
-    implementation("androidx.fragment:fragment-ktx:1.5.5")
+    implementation("androidx.activity:activity-ktx:1.7.0")
+    implementation("androidx.fragment:fragment-ktx:1.5.6")
 
     implementation(libs.constraintLayout)
 
-    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
-    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
+    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
 
     implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
     implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
index 634a638..aa29ddb 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
@@ -25,6 +25,8 @@
 import androidx.navigation.findNavController
 import androidx.navigation.ui.AppBarConfiguration
 import androidx.navigation.ui.setupActionBarWithNavController
+import androidx.navigation.ui.setupWithNavController
+import com.google.android.material.bottomnavigation.BottomNavigationView
 
 class MainActivity : AppCompatActivity() {
 
@@ -47,11 +49,14 @@
         binding = ActivityMainBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
+        val navView: BottomNavigationView = binding.navView
+
         val navController = findNavController(R.id.nav_host_fragment_activity_main)
         val appBarConfiguration = AppBarConfiguration(
-            setOf(R.id.navigation_home)
+            setOf(R.id.navigation_home, R.id.navigation_scanner, R.id.navigation_advertiser)
         )
         setupActionBarWithNavController(navController, appBarConfiguration)
+        navView.setupWithNavController(navController)
     }
 
     override fun onResume() {
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
new file mode 100644
index 0000000..3f12108
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.bluetooth.integration.testapp.ui.advertiser
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.bluetooth.integration.testapp.databinding.FragmentAdvertiserBinding
+import androidx.fragment.app.Fragment
+
+class AdvertiserFragment : Fragment() {
+
+    companion object {
+        private const val TAG = "AdvertiserFragment"
+    }
+
+    private var _binding: FragmentAdvertiserBinding? = null
+
+    // This property is only valid between onCreateView and onDestroyView.
+    private val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        Log.d(
+            TAG, "onCreateView() called with: inflater = $inflater, " +
+                "container = $container, savedInstanceState = $savedInstanceState"
+        )
+
+        _binding = FragmentAdvertiserBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
new file mode 100644
index 0000000..ec8e6d9
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.bluetooth.integration.testapp.ui.scanner
+
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.bluetooth.integration.testapp.databinding.FragmentScannerBinding
+import androidx.fragment.app.Fragment
+
+class ScannerFragment : Fragment() {
+
+    companion object {
+        private const val TAG = "ScannerFragment"
+    }
+
+    private var _binding: FragmentScannerBinding? = null
+
+    // This property is only valid between onCreateView and onDestroyView.
+    private val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        Log.d(
+            TAG, "onCreateView() called with: inflater = $inflater, " +
+                "container = $container, savedInstanceState = $savedInstanceState"
+        )
+
+        _binding = FragmentScannerBinding.inflate(inflater, container, false)
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_bluetooth_searching_24.xml b/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_bluetooth_searching_24.xml
new file mode 100644
index 0000000..0e722c2
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_bluetooth_searching_24.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+  -->
+
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#000000" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M14.24,12.01l2.32,2.32c0.28,-0.72 0.44,-1.51 0.44,-2.33 0,-0.82 -0.16,-1.59 -0.43,-2.31l-2.33,2.32zM19.53,6.71l-1.26,1.26c0.63,1.21 0.98,2.57 0.98,4.02s-0.36,2.82 -0.98,4.02l1.2,1.2c0.97,-1.54 1.54,-3.36 1.54,-5.31 -0.01,-1.89 -0.55,-3.67 -1.48,-5.19zM15.71,7.71L10,2L9,2v7.59L4.41,5 3,6.41 8.59,12 3,17.59 4.41,19 9,14.41L9,22h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM11,5.83l1.88,1.88L11,9.59L11,5.83zM12.88,16.29L11,18.17v-3.76l1.88,1.88z"/>
+</vector>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_wb_iridescent_24.xml b/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_wb_iridescent_24.xml
new file mode 100644
index 0000000..37762ce
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/drawable/baseline_wb_iridescent_24.xml
@@ -0,0 +1,21 @@
+<!--
+  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.
+  -->
+
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M5,14.5h14v-6L5,8.5v6zM11,0.55L11,3.5h2L13,0.55h-2zM19.04,3.05l-1.79,1.79 1.41,1.41 1.8,-1.79 -1.42,-1.41zM13,22.45L13,19.5h-2v2.95h2zM20.45,18.54l-1.8,-1.79 -1.41,1.41 1.79,1.8 1.42,-1.42zM3.55,4.46l1.79,1.79 1.41,-1.41 -1.79,-1.79 -1.41,1.41zM4.96,19.95l1.79,-1.8 -1.41,-1.41 -1.79,1.79 1.41,1.42z"/>
+</vector>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
index ad75688..4c9b86fb 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -23,6 +23,18 @@
     android:layout_height="match_parent"
     tools:context=".MainActivity">
 
+    <com.google.android.material.bottomnavigation.BottomNavigationView
+        android:id="@+id/nav_view"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="0dp"
+        android:layout_marginEnd="0dp"
+        android:background="?android:attr/windowBackground"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:menu="@menu/bottom_nav_menu" />
+
     <fragment
         android:id="@+id/nav_host_fragment_activity_main"
         android:name="androidx.navigation.fragment.NavHostFragment"
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
new file mode 100644
index 0000000..14e7b3e
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_advertiser.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_scanner.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_scanner.xml
new file mode 100644
index 0000000..14e7b3e
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_scanner.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml b/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
index 87e60bd..68f6353 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/menu/bottom_nav_menu.xml
@@ -21,4 +21,14 @@
         android:icon="@drawable/ic_bluetooth_24"
         android:title="@string/title_home" />
 
+    <item
+        android:id="@+id/navigation_scanner"
+        android:icon="@drawable/baseline_bluetooth_searching_24"
+        android:title="@string/title_scanner" />
+
+    <item
+        android:id="@+id/navigation_advertiser"
+        android:icon="@drawable/baseline_wb_iridescent_24"
+        android:title="@string/title_advertiser" />
+
 </menu>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
index 2713795..b3c74a1 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
@@ -26,4 +26,16 @@
         android:label="@string/title_home"
         tools:layout="@layout/fragment_home" />
 
+    <fragment
+        android:id="@+id/navigation_scanner"
+        android:name="androidx.bluetooth.integration.testapp.ui.scanner.ScannerFragment"
+        android:label="@string/title_scanner"
+        tools:layout="@layout/fragment_home" />
+
+    <fragment
+        android:id="@+id/navigation_advertiser"
+        android:name="androidx.bluetooth.integration.testapp.ui.advertiser.AdvertiserFragment"
+        android:label="@string/title_advertiser"
+        tools:layout="@layout/fragment_home" />
+
 </navigation>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 4ec5c99..d60cf5c 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -18,6 +18,8 @@
     <string name="app_name">AndroidX Bluetooth Test App</string>
 
     <string name="title_home">AndroidX Bluetooth</string>
+    <string name="title_scanner">Scanner</string>
+    <string name="title_advertiser">Advertiser</string>
 
     <string name="scan_using_androidx_bluetooth">Scan using AndroidX Bluetooth APIs</string>
     <string name="scan_start_message">Scan started. Results are in Logcat</string>
diff --git a/browser/browser/api/current.txt b/browser/browser/api/current.txt
index 1cb34f0..42d5a3b 100644
--- a/browser/browser/api/current.txt
+++ b/browser/browser/api/current.txt
@@ -203,7 +203,7 @@
     method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
     method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
     method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
-    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallbackRemote, android.os.Bundle);
+    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle);
     method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
     method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
     method protected abstract boolean warmup(long);
@@ -265,12 +265,6 @@
     method public default void onVerticalScrollEvent(boolean, android.os.Bundle);
   }
 
-  public final class EngagementSignalsCallbackRemote {
-    method public void onGreatestScrollPercentageIncreased(@IntRange(from=1, to=100) int, android.os.Bundle) throws android.os.RemoteException;
-    method public void onSessionEnded(boolean, android.os.Bundle) throws android.os.RemoteException;
-    method public void onVerticalScrollEvent(boolean, android.os.Bundle) throws android.os.RemoteException;
-  }
-
   public class PostMessageService extends android.app.Service {
     ctor public PostMessageService();
     method public android.os.IBinder onBind(android.content.Intent?);
diff --git a/browser/browser/api/public_plus_experimental_current.txt b/browser/browser/api/public_plus_experimental_current.txt
index 1cb34f0..42d5a3b 100644
--- a/browser/browser/api/public_plus_experimental_current.txt
+++ b/browser/browser/api/public_plus_experimental_current.txt
@@ -203,7 +203,7 @@
     method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
     method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
     method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
-    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallbackRemote, android.os.Bundle);
+    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle);
     method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
     method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
     method protected abstract boolean warmup(long);
@@ -265,12 +265,6 @@
     method public default void onVerticalScrollEvent(boolean, android.os.Bundle);
   }
 
-  public final class EngagementSignalsCallbackRemote {
-    method public void onGreatestScrollPercentageIncreased(@IntRange(from=1, to=100) int, android.os.Bundle) throws android.os.RemoteException;
-    method public void onSessionEnded(boolean, android.os.Bundle) throws android.os.RemoteException;
-    method public void onVerticalScrollEvent(boolean, android.os.Bundle) throws android.os.RemoteException;
-  }
-
   public class PostMessageService extends android.app.Service {
     ctor public PostMessageService();
     method public android.os.IBinder onBind(android.content.Intent?);
diff --git a/browser/browser/api/restricted_current.txt b/browser/browser/api/restricted_current.txt
index 20835a4..19a1da2 100644
--- a/browser/browser/api/restricted_current.txt
+++ b/browser/browser/api/restricted_current.txt
@@ -214,7 +214,7 @@
     method @androidx.browser.customtabs.CustomTabsService.Result protected abstract int postMessage(androidx.browser.customtabs.CustomTabsSessionToken, String, android.os.Bundle?);
     method protected abstract boolean receiveFile(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri, int, android.os.Bundle?);
     method protected abstract boolean requestPostMessageChannel(androidx.browser.customtabs.CustomTabsSessionToken, android.net.Uri);
-    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallbackRemote, android.os.Bundle);
+    method protected boolean setEngagementSignalsCallback(androidx.browser.customtabs.CustomTabsSessionToken, androidx.browser.customtabs.EngagementSignalsCallback, android.os.Bundle);
     method protected abstract boolean updateVisuals(androidx.browser.customtabs.CustomTabsSessionToken, android.os.Bundle?);
     method protected abstract boolean validateRelationship(androidx.browser.customtabs.CustomTabsSessionToken, @androidx.browser.customtabs.CustomTabsService.Relation int, android.net.Uri, android.os.Bundle?);
     method protected abstract boolean warmup(long);
@@ -276,12 +276,6 @@
     method public default void onVerticalScrollEvent(boolean, android.os.Bundle);
   }
 
-  public final class EngagementSignalsCallbackRemote {
-    method public void onGreatestScrollPercentageIncreased(@IntRange(from=1, to=100) int, android.os.Bundle) throws android.os.RemoteException;
-    method public void onSessionEnded(boolean, android.os.Bundle) throws android.os.RemoteException;
-    method public void onVerticalScrollEvent(boolean, android.os.Bundle) throws android.os.RemoteException;
-  }
-
   public class PostMessageService extends android.app.Service {
     ctor public PostMessageService();
     method public android.os.IBinder onBind(android.content.Intent?);
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
index 8747f31..ee1e791 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/CustomTabsService.java
@@ -274,7 +274,7 @@
         public boolean setEngagementSignalsCallback(
                 @NonNull ICustomTabsCallback customTabsCallback, @NonNull IBinder callback,
                 @NonNull Bundle extras) {
-            EngagementSignalsCallbackRemote remote = EngagementSignalsCallbackRemote.fromBinder(
+            EngagementSignalsCallback remote = EngagementSignalsCallbackRemote.fromBinder(
                     callback);
             return CustomTabsService.this.setEngagementSignalsCallback(
                     new CustomTabsSessionToken(customTabsCallback, getSessionIdFromBundle(extras)),
@@ -506,11 +506,11 @@
     }
 
     /**
-     * Sets an {@link EngagementSignalsCallbackRemote} to execute callbacks for events related to
+     * Sets an {@link EngagementSignalsCallback} to execute callbacks for events related to
      * the user's engagement with the webpage within the tab.
      *
      * @param sessionToken The unique identifier for the session.
-     * @param callback The {@link EngagementSignalsCallbackRemote} to execute the callbacks.
+     * @param callback The {@link EngagementSignalsCallback} to execute the callbacks.
      * @param extras Reserved for future use.
      * @return Whether the callback connection is allowed. If false, no callbacks will be called for
      *         this session.
@@ -520,7 +520,7 @@
     @SuppressWarnings("ExecutorRegistration")
     protected boolean setEngagementSignalsCallback(
             @NonNull CustomTabsSessionToken sessionToken,
-            @NonNull EngagementSignalsCallbackRemote callback, @NonNull Bundle extras) {
+            @NonNull EngagementSignalsCallback callback, @NonNull Bundle extras) {
         return false;
     }
 
diff --git a/browser/browser/src/main/java/androidx/browser/customtabs/EngagementSignalsCallbackRemote.java b/browser/browser/src/main/java/androidx/browser/customtabs/EngagementSignalsCallbackRemote.java
index 9e02c71..44dcc86 100644
--- a/browser/browser/src/main/java/androidx/browser/customtabs/EngagementSignalsCallbackRemote.java
+++ b/browser/browser/src/main/java/androidx/browser/customtabs/EngagementSignalsCallbackRemote.java
@@ -20,16 +20,21 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.support.customtabs.IEngagementSignalsCallback;
+import android.util.Log;
 
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 
 /**
  * Remote class used to execute callbacks from a binder of {@link EngagementSignalsCallback}. This
  * is a thin wrapper around {@link IEngagementSignalsCallback} that is passed to the Custom Tabs
  * implementation for the calls across process boundaries.
  */
-public final class EngagementSignalsCallbackRemote {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+/* package */ final class EngagementSignalsCallbackRemote implements EngagementSignalsCallback {
+    private static final String TAG = "EngagementSigsCallbkRmt";
+
     private final IEngagementSignalsCallback mCallbackBinder;
 
     private EngagementSignalsCallbackRemote(@NonNull IEngagementSignalsCallback callbackBinder) {
@@ -53,9 +58,13 @@
      *                      user scrolls back up toward the top of the page.
      * @param extras Reserved for future use.
      */
-    public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) throws
-            RemoteException {
-        mCallbackBinder.onVerticalScrollEvent(isDirectionUp, extras);
+    @Override
+    public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) {
+        try {
+            mCallbackBinder.onVerticalScrollEvent(isDirectionUp, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException during IEngagementSignalsCallback transaction");
+        }
     }
 
     /**
@@ -68,10 +77,14 @@
      *                         made down the current page.
      * @param extras Reserved for future use.
      */
+    @Override
     public void onGreatestScrollPercentageIncreased(
-            @IntRange(from = 1, to = 100) int scrollPercentage, @NonNull Bundle extras) throws
-            RemoteException {
-        mCallbackBinder.onGreatestScrollPercentageIncreased(scrollPercentage, extras);
+            @IntRange(from = 1, to = 100) int scrollPercentage, @NonNull Bundle extras) {
+        try {
+            mCallbackBinder.onGreatestScrollPercentageIncreased(scrollPercentage, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException during IEngagementSignalsCallback transaction");
+        }
     }
 
     /**
@@ -82,8 +95,12 @@
      *                        scrolling.
      * @param extras Reserved for future use.
      */
-    public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras)
-            throws RemoteException {
-        mCallbackBinder.onSessionEnded(didUserInteract, extras);
+    @Override
+    public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) {
+        try {
+            mCallbackBinder.onSessionEnded(didUserInteract, extras);
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException during IEngagementSignalsCallback transaction");
+        }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AeFpsRangeLegacyQuirk.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AeFpsRangeLegacyQuirk.kt
new file mode 100644
index 0000000..e8c12f7
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/AeFpsRangeLegacyQuirk.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.camera2.pipe.integration.compat.quirk
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.util.Range
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.impl.Quirk
+
+/**
+ *
+ * QuirkSummary
+ * - Bug Id: b/167425305
+ * - Description: Quirk required to maintain good exposure on legacy devices by specifying a
+ *                proper [android.hardware.camera2.CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE].
+ *                Legacy devices set the AE target FPS range to [30, 30]. This can potentially
+ *                cause underexposure issues.
+ *                [androidx.camera.camera2.internal.compat.workaround.AeFpsRange]
+ *                contains a workaround that is used on legacy devices to set a AE FPS range
+ *                whose upper bound is 30, which guarantees a smooth frame rate, and whose lower
+ *                bound is as small as possible to properly expose frames in low light
+ *                conditions. The default behavior on non legacy devices does not add the AE
+ *                FPS range option.
+ * - Device(s): All legacy devices
+ *
+ * TODO(b/270421716): enable CameraXQuirksClassDetector lint check when kotlin is supported.
+ */
+@SuppressLint("CameraXQuirksClassDetector")
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class AeFpsRangeLegacyQuirk(cameraMetadata: CameraMetadata) : Quirk {
+    /**
+     * Returns the fps range whose upper is 30 and whose lower is the smallest, or null if no
+     * range has an upper equal to 30.  The rationale is:
+     * - Range upper is always 30 so that a smooth frame rate is guaranteed.
+     * - Range lower contains the smallest supported value so that it can adapt as much as
+     * possible to low light conditions.
+     */
+    val range: Range<Int>? by lazy {
+        val availableFpsRanges: Array<out Range<Int>>? =
+            cameraMetadata[CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
+        pickSuitableFpsRange(availableFpsRanges)
+    }
+
+    private fun pickSuitableFpsRange(
+        availableFpsRanges: Array<out Range<Int>>?
+    ): Range<Int>? {
+        if (availableFpsRanges.isNullOrEmpty()) {
+            return null
+        }
+        var pickedRange: Range<Int>? = null
+        for (fpsRangeBeforeCorrection in availableFpsRanges) {
+            val fpsRange = getCorrectedFpsRange(fpsRangeBeforeCorrection)
+            if (fpsRange.upper != 30) {
+                continue
+            }
+            if (pickedRange == null) {
+                pickedRange = fpsRange
+            } else {
+                if (fpsRange.lower < pickedRange.lower) {
+                    pickedRange = fpsRange
+                }
+            }
+        }
+        return pickedRange
+    }
+
+    /**
+     * On android 5.0/5.1, [CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES]
+     * returns wrong ranges whose values were multiplied by 1000. So we need to convert them to the
+     * correct values.
+     */
+    private fun getCorrectedFpsRange(fpsRange: Range<Int>): Range<Int> {
+        var newUpper = fpsRange.upper
+        var newLower = fpsRange.lower
+        if (fpsRange.upper >= 1000) {
+            newUpper = fpsRange.upper / 1000
+        }
+        if (fpsRange.lower >= 1000) {
+            newLower = fpsRange.lower / 1000
+        }
+        return Range(newLower, newUpper)
+    }
+
+    companion object {
+        fun isEnabled(cameraMetadata: CameraMetadata): Boolean {
+            val level = cameraMetadata[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
+            return level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
index d2fbf1e..c46831d 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/quirk/CameraQuirks.kt
@@ -47,6 +47,9 @@
 
         // Go through all defined camera quirks in lexicographical order,
         // and add them to `quirks` if they should be loaded
+        if (AeFpsRangeLegacyQuirk.isEnabled(cameraMetadata)) {
+            quirks.add(AeFpsRangeLegacyQuirk(cameraMetadata))
+        }
         if (AfRegionFlipHorizontallyQuirk.isEnabled(cameraMetadata)) {
             quirks.add(AfRegionFlipHorizontallyQuirk())
         }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRange.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRange.kt
new file mode 100644
index 0000000..8f1c132
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRange.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.camera.camera2.pipe.integration.compat.workaround
+
+import android.util.Range
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.integration.compat.quirk.AeFpsRangeLegacyQuirk
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import javax.inject.Inject
+
+/**
+ * Sets an AE target FPS range on legacy devices from [AeFpsRangeLegacyQuirk] to maintain good
+ * exposure.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+
+@CameraScope
+class AeFpsRange @Inject constructor(cameraQuirks: CameraQuirks) {
+    private val aeTargetFpsRange: Range<Int>? by lazy {
+        /** Chooses the AE target FPS range on legacy devices.  */
+        cameraQuirks.quirks[AeFpsRangeLegacyQuirk::class.java]?.range
+    }
+
+    /**
+     * Sets the [android.hardware.camera2.CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE]
+     * option on legacy device when possible.
+     */
+    fun getTargetAeFpsRange(): Range<Int>? {
+        return aeTargetFpsRange
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
index b402f17..f61c4bc 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/State3AControl.kt
@@ -19,10 +19,12 @@
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice
 import android.hardware.camera2.CaptureRequest
+import android.util.Range
 import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
 import androidx.camera.camera2.pipe.integration.adapter.propagateTo
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.AutoFlashAEModeDisabler
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.core.CameraControl
@@ -43,6 +45,7 @@
 class State3AControl @Inject constructor(
     val cameraProperties: CameraProperties,
     private val aeModeDisabler: AutoFlashAEModeDisabler,
+    private val aeFpsRange: AeFpsRange
 ) : UseCaseCameraControl, UseCaseCamera.RunningUseCasesChangeListener {
     private var _useCaseCamera: UseCaseCamera? = null
     override var useCaseCamera: UseCaseCamera?
@@ -94,10 +97,12 @@
     var template by updateOnPropertyChange(DEFAULT_REQUEST_TEMPLATE)
     var preferredAeMode: Int? by updateOnPropertyChange(null)
     var preferredFocusMode: Int? by updateOnPropertyChange(null)
+    var preferredAeFpsRange: Range<Int>? by updateOnPropertyChange(aeFpsRange.getTargetAeFpsRange())
 
     override fun reset() {
         synchronized(lock) { updateSignals.toList() }.cancelAll()
         preferredAeMode = null
+        preferredAeFpsRange = null
         preferredFocusMode = null
         flashMode = DEFAULT_FLASH_MODE
         template = DEFAULT_REQUEST_TEMPLATE
@@ -114,6 +119,8 @@
     }
 
     fun invalidate() {
+        // TODO(b/276779600): Refactor and move the setting of these parameter to
+        //  CameraGraph.Config(requiredParameters = mapOf(....)).
         val preferAeMode = preferredAeMode ?: when (flashMode) {
             ImageCapture.FLASH_MODE_OFF -> CaptureRequest.CONTROL_AE_MODE_ON
             ImageCapture.FLASH_MODE_ON -> CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
@@ -125,14 +132,18 @@
 
         val preferAfMode = preferredFocusMode ?: getDefaultAfMode()
 
+        val parameters: MutableMap<CaptureRequest.Key<*>, Any> = mutableMapOf(
+            CaptureRequest.CONTROL_AE_MODE to getSupportedAeMode(preferAeMode),
+            CaptureRequest.CONTROL_AF_MODE to getSupportedAfMode(preferAfMode),
+            CaptureRequest.CONTROL_AWB_MODE to getSupportedAwbMode(
+                CaptureRequest.CONTROL_AWB_MODE_AUTO))
+
+        preferredAeFpsRange?.let {
+            parameters[CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE] = it
+        }
+
         useCaseCamera?.requestControl?.addParametersAsync(
-            values = mapOf(
-                CaptureRequest.CONTROL_AE_MODE to getSupportedAeMode(preferAeMode),
-                CaptureRequest.CONTROL_AF_MODE to getSupportedAfMode(preferAfMode),
-                CaptureRequest.CONTROL_AWB_MODE to getSupportedAwbMode(
-                    CaptureRequest.CONTROL_AWB_MODE_AUTO
-                ),
-            )
+            values = parameters
         )?.apply {
             toCompletableDeferred().also { signal ->
                 synchronized(lock) {
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 3b0b4c2..b51e10f 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
@@ -34,6 +34,7 @@
 import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
 import androidx.camera.camera2.pipe.integration.compat.ZoomCompat
 import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpMeteringRegionCorrection
@@ -1584,6 +1585,18 @@
     ) = State3AControl(
         properties,
         NoOpAutoFlashAEModeDisabler,
+        AeFpsRange(
+            CameraQuirks(
+                FakeCameraMetadata(),
+                StreamConfigurationMapCompat(
+                    StreamConfigurationMapBuilder.newBuilder().build(),
+                    OutputSizesCorrector(
+                        FakeCameraMetadata(),
+                        StreamConfigurationMapBuilder.newBuilder().build()
+                    )
+                )
+            )
+        )
     ).apply {
         this.useCaseCamera = useCaseCamera
     }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRangeTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRangeTest.kt
new file mode 100644
index 0000000..1f0626e
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/compat/workaround/AeFpsRangeTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.camera2.pipe.integration.compat.workaround
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import android.util.Range
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+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
+import org.robolectric.shadows.StreamConfigurationMapBuilder
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class AeFpsRangeTest {
+    @Test
+    fun validEntryExists_correctRangeIsSelected() {
+        val availableFpsRanges: Array<Range<Int>> = arrayOf(
+            Range(25, 30),
+            Range(7, 33),
+            Range(15, 30),
+            Range(11, 22),
+            Range(30, 30)
+        )
+        val aeFpsRange: AeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+                availableFpsRanges
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isEqualTo(Range(15, 30))
+    }
+
+    @Test
+    fun noValidEntry_doesNotSetFpsRange() {
+        val availableFpsRanges: Array<Range<Int>> = arrayOf(
+            Range(25, 25),
+            Range(7, 33),
+            Range(15, 24),
+            Range(11, 22)
+        )
+        val aeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+                availableFpsRanges
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isNull()
+    }
+
+    @Test
+    fun availableArrayIsNull_doesNotSetFpsRange() {
+        val aeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+                null
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isNull()
+    }
+
+    @Test
+    fun limitedDevices_doesNotSetFpsRange() {
+        val availableFpsRanges: Array<Range<Int>> = arrayOf(
+            Range(15, 30)
+        )
+        val aeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+                availableFpsRanges
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isNull()
+    }
+
+    @Test
+    fun fullDevices_doesNotSetFpsRange() {
+        val availableFpsRanges: Array<Range<Int>> = arrayOf(
+            Range(15, 30)
+        )
+        val aeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+                availableFpsRanges
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isNull()
+    }
+
+    @Test
+    fun level3Devices_doesNotSetFpsRange() {
+        val availableFpsRanges: Array<Range<Int>> = arrayOf(
+            Range(15, 30)
+        )
+        val aeFpsRange =
+            createAeFpsRange(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3,
+                availableFpsRanges
+            )
+        val pick = getAeFpsRange(aeFpsRange)
+        assertThat(pick).isNull()
+    }
+
+    private fun createAeFpsRange(
+        hardwareLevel: Int,
+        availableFpsRanges: Array<Range<Int>>?
+    ): AeFpsRange {
+        val streamConfigurationMap = StreamConfigurationMapBuilder.newBuilder().build()
+
+        val metadata = FakeCameraMetadata(
+            mapOf(
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to hardwareLevel,
+                CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES to availableFpsRanges,
+                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP to streamConfigurationMap
+            )
+        )
+        return AeFpsRange(
+            CameraQuirks(
+                metadata,
+                StreamConfigurationMapCompat(
+                    streamConfigurationMap,
+                    OutputSizesCorrector(metadata, streamConfigurationMap)
+                )
+            )
+        )
+    }
+
+    private fun getAeFpsRange(aeFpsRange: AeFpsRange): Range<Int>? {
+        return aeFpsRange.getTargetAeFpsRange()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
index 34b3175..28e3c559 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/CapturePipelineTest.kt
@@ -38,9 +38,13 @@
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
 import androidx.camera.camera2.pipe.integration.compat.workaround.CapturePipelineTorchCorrection
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
 import androidx.camera.camera2.pipe.integration.compat.workaround.NotUseTorchAsFlash
 import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlashImpl
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraph
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraGraphSession
@@ -77,6 +81,7 @@
 import org.mockito.Mockito.mock
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
 import org.robolectric.util.ReflectionHelpers
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
@@ -207,6 +212,18 @@
             State3AControl(
                 fakeCameraProperties,
                 NoOpAutoFlashAEModeDisabler,
+                AeFpsRange(
+                    CameraQuirks(
+                        FakeCameraMetadata(),
+                        StreamConfigurationMapCompat(
+                            StreamConfigurationMapBuilder.newBuilder().build(),
+                            OutputSizesCorrector(
+                                FakeCameraMetadata(),
+                                StreamConfigurationMapBuilder.newBuilder().build()
+                            )
+                        )
+                    )
+                )
             ).apply {
                 useCaseCamera = fakeUseCaseCamera
             },
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
index 1666ff2..d281927 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/FlashControlTest.kt
@@ -20,7 +20,11 @@
 import android.hardware.camera2.CaptureRequest
 import android.os.Build
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
@@ -43,6 +47,7 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
@@ -71,8 +76,24 @@
     )
     private val fakeRequestControl = FakeUseCaseCameraRequestControl()
     private val fakeUseCaseCamera = FakeUseCaseCamera(requestControl = fakeRequestControl)
+    private val aeFpsRange = AeFpsRange(
+        CameraQuirks(
+            FakeCameraMetadata(),
+            StreamConfigurationMapCompat(
+                StreamConfigurationMapBuilder.newBuilder().build(),
+                OutputSizesCorrector(
+                    FakeCameraMetadata(),
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
+            )
+        )
+    )
     private val state3AControl =
-        State3AControl(FakeCameraProperties(metadata), NoOpAutoFlashAEModeDisabler).apply {
+        State3AControl(
+            FakeCameraProperties(metadata),
+            NoOpAutoFlashAEModeDisabler,
+            aeFpsRange
+            ).apply {
             useCaseCamera = fakeUseCaseCamera
         }
     private lateinit var flashControl: FlashControl
@@ -92,7 +113,11 @@
         val fakeCameraProperties = FakeCameraProperties()
 
         val flashControl = FlashControl(
-            State3AControl(fakeCameraProperties, NoOpAutoFlashAEModeDisabler).apply {
+            State3AControl(
+                fakeCameraProperties,
+                NoOpAutoFlashAEModeDisabler,
+                aeFpsRange
+            ).apply {
                 useCaseCamera = fakeUseCaseCamera
             },
             fakeUseCaseThreads,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
index aba1a20..e5d846e 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/TorchControlTest.kt
@@ -20,7 +20,11 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.Result3A
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
+import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
+import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCamera
 import androidx.camera.camera2.pipe.integration.testing.FakeUseCaseCameraRequestControl
@@ -49,6 +53,7 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.StreamConfigurationMapBuilder
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
@@ -80,6 +85,18 @@
         // Set a CompletableDeferred without set it to completed.
         setTorchResult = CompletableDeferred()
     }
+    private val aeFpsRange = AeFpsRange(
+        CameraQuirks(
+            FakeCameraMetadata(),
+            StreamConfigurationMapCompat(
+                StreamConfigurationMapBuilder.newBuilder().build(),
+                OutputSizesCorrector(
+                    FakeCameraMetadata(),
+                    StreamConfigurationMapBuilder.newBuilder().build()
+                )
+            )
+        )
+    )
 
     private lateinit var torchControl: TorchControl
 
@@ -92,6 +109,7 @@
             State3AControl(
                 fakeCameraProperties,
                 NoOpAutoFlashAEModeDisabler,
+                aeFpsRange
             ).apply {
                 useCaseCamera = fakeUseCaseCamera
             },
@@ -109,7 +127,11 @@
             // Without a flash unit, this Job will complete immediately with a IllegalStateException
             TorchControl(
                 fakeCameraProperties,
-                State3AControl(fakeCameraProperties, NoOpAutoFlashAEModeDisabler).apply {
+                State3AControl(
+                    fakeCameraProperties,
+                    NoOpAutoFlashAEModeDisabler,
+                    aeFpsRange
+                ).apply {
                     useCaseCamera = fakeUseCaseCamera
                 },
                 fakeUseCaseThreads,
@@ -126,7 +148,11 @@
 
         val torchState = TorchControl(
             fakeCameraProperties,
-            State3AControl(fakeCameraProperties, NoOpAutoFlashAEModeDisabler).apply {
+            State3AControl(
+                fakeCameraProperties,
+                NoOpAutoFlashAEModeDisabler,
+                aeFpsRange
+            ).apply {
 
                 useCaseCamera = fakeUseCaseCamera
             },
@@ -146,7 +172,11 @@
 
             TorchControl(
                 fakeCameraProperties,
-                State3AControl(fakeCameraProperties, NoOpAutoFlashAEModeDisabler).apply {
+                State3AControl(
+                    fakeCameraProperties,
+                    NoOpAutoFlashAEModeDisabler,
+                    aeFpsRange
+                ).apply {
                     useCaseCamera = fakeUseCaseCamera
                 },
                 fakeUseCaseThreads,
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
index f0b520c..3e93b84 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/testing/FakeCameraInfoAdapterCreator.kt
@@ -28,6 +28,7 @@
 import androidx.camera.camera2.pipe.integration.adapter.EncoderProfilesProviderAdapter
 import androidx.camera.camera2.pipe.integration.compat.StreamConfigurationMapCompat
 import androidx.camera.camera2.pipe.integration.compat.quirk.CameraQuirks
+import androidx.camera.camera2.pipe.integration.compat.workaround.AeFpsRange
 import androidx.camera.camera2.pipe.integration.compat.workaround.MeteringRegionCorrection
 import androidx.camera.camera2.pipe.integration.compat.workaround.NoOpAutoFlashAEModeDisabler
 import androidx.camera.camera2.pipe.integration.compat.workaround.OutputSizesCorrector
@@ -95,9 +96,6 @@
         zoomControl: ZoomControl = this.zoomControl,
     ): CameraInfoAdapter {
         val fakeUseCaseCamera = FakeUseCaseCamera()
-        val state3AControl = State3AControl(cameraProperties, NoOpAutoFlashAEModeDisabler).apply {
-            useCaseCamera = fakeUseCaseCamera
-        }
         val fakeStreamConfigurationMap = StreamConfigurationMapCompat(
             streamConfigurationMap,
             OutputSizesCorrector(cameraProperties.metadata, streamConfigurationMap)
@@ -106,6 +104,13 @@
             cameraProperties.metadata,
             fakeStreamConfigurationMap,
         )
+        val state3AControl = State3AControl(
+            cameraProperties,
+            NoOpAutoFlashAEModeDisabler,
+            AeFpsRange(fakeCameraQuirks)
+        ).apply {
+            useCaseCamera = fakeUseCaseCamera
+        }
         return CameraInfoAdapter(
             cameraProperties,
             CameraConfig(cameraId),
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index 2c32dd7..7e50da5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -27,6 +27,7 @@
 import android.os.Build
 import android.os.Handler
 import android.view.Surface
+import androidx.annotation.GuardedBy
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
@@ -333,3 +334,154 @@
             else -> null
         }
 }
+
+/**
+ * VirtualAndroidCameraDevice creates a simple wrapper around a [AndroidCameraDevice], augmenting
+ * it by enabling it to reject further capture session/request calls when it is "disconnected'.
+ */
+internal class VirtualAndroidCameraDevice(
+    internal val androidCameraDevice: AndroidCameraDevice,
+) : CameraDeviceWrapper {
+    private val lock = Any()
+
+    @GuardedBy("lock")
+    private var disconnected = false
+
+    override val cameraId: CameraId
+        get() = androidCameraDevice.cameraId
+
+    override fun createCaptureSession(
+        outputs: List<Surface>,
+        stateCallback: CameraCaptureSessionWrapper.StateCallback,
+        handler: Handler?
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn { "createCaptureSession failed: Virtual device disconnected" }
+            false
+        } else {
+            androidCameraDevice.createCaptureSession(outputs, stateCallback, handler)
+        }
+    }
+
+    @RequiresApi(23)
+    override fun createReprocessableCaptureSession(
+        input: InputConfiguration,
+        outputs: List<Surface>,
+        stateCallback: CameraCaptureSessionWrapper.StateCallback,
+        handler: Handler?
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn { "createReprocessableCaptureSession failed: Virtual device disconnected" }
+            false
+        } else {
+            androidCameraDevice.createReprocessableCaptureSession(
+                input,
+                outputs,
+                stateCallback,
+                handler
+            )
+        }
+    }
+
+    @RequiresApi(23)
+    override fun createConstrainedHighSpeedCaptureSession(
+        outputs: List<Surface>,
+        stateCallback: CameraCaptureSessionWrapper.StateCallback,
+        handler: Handler?
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn {
+                "createConstrainedHighSpeedCaptureSession failed: Virtual device disconnected"
+            }
+            false
+        } else {
+            androidCameraDevice.createConstrainedHighSpeedCaptureSession(
+                outputs,
+                stateCallback,
+                handler
+            )
+        }
+    }
+
+    @RequiresApi(24)
+    override fun createCaptureSessionByOutputConfigurations(
+        outputConfigurations: List<OutputConfigurationWrapper>,
+        stateCallback: CameraCaptureSessionWrapper.StateCallback,
+        handler: Handler?
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn {
+                "createCaptureSessionByOutputConfigurations failed: Virtual device disconnected"
+            }
+            false
+        } else {
+            androidCameraDevice.createCaptureSessionByOutputConfigurations(
+                outputConfigurations,
+                stateCallback,
+                handler
+            )
+        }
+    }
+
+    @RequiresApi(24)
+    override fun createReprocessableCaptureSessionByConfigurations(
+        inputConfig: InputConfigData,
+        outputs: List<OutputConfigurationWrapper>,
+        stateCallback: CameraCaptureSessionWrapper.StateCallback,
+        handler: Handler?
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn {
+                "createReprocessableCaptureSessionByConfigurations failed: " +
+                    "Virtual device disconnected"
+            }
+            false
+        } else {
+            androidCameraDevice.createReprocessableCaptureSessionByConfigurations(
+                inputConfig,
+                outputs,
+                stateCallback,
+                handler
+            )
+        }
+    }
+
+    @RequiresApi(28)
+    override fun createCaptureSession(config: SessionConfigData) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn { "createCaptureSession failed: Virtual device disconnected" }
+            false
+        } else {
+            androidCameraDevice.createCaptureSession(config)
+        }
+    }
+
+    override fun createCaptureRequest(template: RequestTemplate) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn { "createCaptureRequest failed: Virtual device disconnected" }
+            null
+        } else {
+            androidCameraDevice.createCaptureRequest(template)
+        }
+    }
+
+    @RequiresApi(23)
+    override fun createReprocessCaptureRequest(
+        inputResult: TotalCaptureResult
+    ) = synchronized(lock) {
+        if (disconnected) {
+            Log.warn { "createReprocessCaptureRequest failed: Virtual device disconnected" }
+            null
+        } else {
+            androidCameraDevice.createReprocessCaptureRequest(inputResult)
+        }
+    }
+
+    override fun onDeviceClosed() = androidCameraDevice.onDeviceClosed()
+
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = androidCameraDevice.unwrapAs(type)
+
+    internal fun disconnect() = synchronized(lock) {
+        disconnected = true
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
index e1c6df8..45f13c5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
@@ -127,6 +127,9 @@
     @GuardedBy("lock")
     private var closed = false
 
+    @GuardedBy("lock")
+    private var currentVirtualAndroidCamera: VirtualAndroidCameraDevice? = null
+
     // This is intended so that it will only ever replay the most recent event to new subscribers,
     // but to never drop events for existing subscribers.
     private val _stateFlow = MutableSharedFlow<CameraState>(replay = 1, extraBufferCapacity = 3)
@@ -134,6 +137,7 @@
 
     @GuardedBy("lock")
     private var _lastState: CameraState = CameraStateUnopened
+
     override val state: Flow<CameraState>
         get() = _states
 
@@ -155,12 +159,40 @@
                 return@coroutineScope
             }
 
+            // Here we generally relay what we receive from AndroidCameraState's state flow, except
+            // for CameraStateOpen. When the AndroidCameraDevice is provided through
+            // CameraStateOpen, we create a wrapper (VirtualAndroidCameraDevice) around it,
+            // allowing the AndroidCameraDevice to be "disconnected". This prevents additional calls
+            // such as createCaptureSession() from being executed on the camera device.
+            //
+            // Why it's needed: When 2 CameraGraphs are created and started in quick succession, say
+            // we have CameraGraph-1 and CameraGraph-2, it is possible for CameraGraph-2 to create
+            // its capture session _earlier_ than CameraGraph-1, as they run on separate threads.
+            // Because the two createCaptureSession() calls happen out of order, the more recent
+            // call wins, causing the session for CameraGraph-1 to succeed (even when it's already
+            // closed) and the session for CameraGraph-2 to fail (even though it was started most
+            // recently).
+            //
+            // Relevant bug: b/269619541
             job =
                 launch(EmptyCoroutineContext) {
                     state.collect {
                         synchronized(lock) {
                             if (!closed) {
-                                emitState(it)
+                                if (it is CameraStateOpen) {
+                                    val virtualAndroidCamera = VirtualAndroidCameraDevice(
+                                        it.cameraDevice as AndroidCameraDevice
+                                    )
+                                    // The ordering here is important. We need to set the current
+                                    // VirtualAndroidCameraDevice before emitting it out. Otherwise,
+                                    // the capture session can be started while we still don't have
+                                    // the current VirtualAndroidCameraDevice to disconnect when
+                                    // VirtualCameraState.disconnect() is called in parallel.
+                                    currentVirtualAndroidCamera = virtualAndroidCamera
+                                    emitState(CameraStateOpen(virtualAndroidCamera))
+                                } else {
+                                    emitState(it)
+                                }
                             }
                         }
                     }
@@ -178,6 +210,7 @@
 
             Log.info { "Disconnecting $this" }
 
+            currentVirtualAndroidCamera?.disconnect()
             job?.cancel()
             wakelockToken?.release()
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
index 1def65c..f1360d3 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/Controller3A.kt
@@ -187,7 +187,7 @@
         afRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AF_REGIONS, it.toTypedArray()) }
         awbRegions?.let { extra3AParams.put(CaptureRequest.CONTROL_AWB_REGIONS, it.toTypedArray()) }
 
-        if (!graphProcessor.submit(extra3AParams)) {
+        if (!graphProcessor.trySubmit(extra3AParams)) {
             graphListener3A.removeListener(listener)
             return CompletableDeferred(result3ASubmitFailed)
         }
@@ -245,7 +245,7 @@
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
         if (afLockBehaviorSanitized.shouldUnlockAf()) {
             debug { "lock3A - sending a request to unlock af first." }
-            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+            if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
                 return CompletableDeferred(result3ASubmitFailed)
             }
         }
@@ -333,7 +333,7 @@
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
         if (afSanitized == true) {
             debug { "unlock3A - sending a request to unlock af first." }
-            if (!graphProcessor.submit(parameterForAfTriggerCancel)) {
+            if (!graphProcessor.trySubmit(parameterForAfTriggerCancel)) {
                 debug { "unlock3A - request to unlock af failed, returning early." }
                 return CompletableDeferred(result3ASubmitFailed)
             }
@@ -373,7 +373,7 @@
         graphListener3A.addListener(listener)
 
         debug { "lock3AForCapture - sending a request to trigger ae precapture metering and af." }
-        if (!graphProcessor.submit(parametersForAePrecaptureAndAfTrigger)) {
+        if (!graphProcessor.trySubmit(parametersForAePrecaptureAndAfTrigger)) {
             debug {
                 "lock3AForCapture - request to trigger ae precapture metering and af failed, " +
                     "returning early."
@@ -401,7 +401,7 @@
      */
     private suspend fun unlock3APostCaptureAndroidLAndBelow(): Deferred<Result3A> {
         debug { "unlock3AForCapture - sending a request to cancel af and turn on ae." }
-        if (!graphProcessor.submit(
+        if (!graphProcessor.trySubmit(
                 mapOf(CONTROL_AF_TRIGGER to CONTROL_AF_TRIGGER_CANCEL, CONTROL_AE_LOCK to true)
             )
         ) {
@@ -415,7 +415,10 @@
         graphListener3A.addListener(listener)
 
         debug { "unlock3AForCapture - sending a request to turn off ae." }
-        if (!graphProcessor.submit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))) {
+        if (!graphProcessor.trySubmit(
+                mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false)
+            )
+        ) {
             debug { "unlock3AForCapture - request to unlock ae failed." }
             graphListener3A.removeListener(listener)
             return CompletableDeferred(result3ASubmitFailed)
@@ -438,7 +441,7 @@
                 CONTROL_AE_PRECAPTURE_TRIGGER to
                     CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
             )
-        if (!graphProcessor.submit(parametersForAePrecaptureAndAfCancel)) {
+        if (!graphProcessor.trySubmit(parametersForAePrecaptureAndAfCancel)) {
             debug {
                 "unlock3APostCapture - request to reset af and ae precapture metering failed, " +
                     "returning early."
@@ -511,7 +514,7 @@
         }
 
         debug { "lock3A - submitting a request to lock af." }
-        val submitSuccess = graphProcessor.submit(parameterForAfTriggerStart)
+        val submitSuccess = graphProcessor.trySubmit(parameterForAfTriggerStart)
 
         lastAeMode?.let {
             graphState3A.update(aeMode = it)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
index 355643f..e195621 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/graph/GraphProcessor.kt
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.pipe.formatForLogs
 import androidx.camera.camera2.pipe.putAllMetadata
 import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -55,7 +56,31 @@
 
     fun submit(request: Request)
     fun submit(requests: List<Request>)
-    suspend fun submit(parameters: Map<*, Any?>): Boolean
+
+    /**
+     * This tries to submit a list of parameters — essentially a list of request settings usually
+     * from 3A methods. It does this by setting the given parameters onto the current repeating
+     * request on a best-effort basis.
+     *
+     * If the CameraGraph hasn't been started yet, or we haven't yet submitted a repeating request,
+     * the method will suspend until we've met the criteria and only then submits the parameters.
+     *
+     * This behavior is required if users call 3A methods immediately after start. For example:
+     *
+     * ```
+     * cameraGraph.start()
+     * cameraGraph.acquireSession().use {
+     *     it.startRepeating(request)
+     *     it.lock3A(...)
+     * }
+     * ```
+     *
+     * Under this scenario, developers should reasonably expect things to work, and therefore
+     * the implementation handles this on a best-effort basis for the developer.
+     *
+     * Please read b/263211462 for more context.
+     */
+    suspend fun trySubmit(parameters: Map<*, Any?>): Boolean
 
     fun startRepeating(request: Request)
     fun stopRepeating()
@@ -114,6 +139,12 @@
     @GuardedBy("lock")
     private var closed = false
 
+    @GuardedBy("lock")
+    private var pendingParameters: Map<*, Any?>? = null
+
+    @GuardedBy("lock")
+    private var pendingParametersDeferred: CompletableDeferred<Boolean>? = null
+
     private val _graphState = MutableStateFlow<GraphState>(GraphStateStopped)
 
     override val graphState: StateFlow<GraphState>
@@ -248,11 +279,12 @@
     }
 
     /** Submit a request to the camera using only the current repeating request. */
-    override suspend fun submit(parameters: Map<*, Any?>): Boolean =
+    override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean =
         withContext(threads.lightweightDispatcher) {
             val processor: GraphRequestProcessor?
             val request: Request?
             val requiredParameters: MutableMap<Any, Any?> = mutableMapOf()
+            var deferredResult: CompletableDeferred<Boolean>? = null
 
             synchronized(lock) {
                 if (closed) return@withContext false
@@ -262,10 +294,20 @@
                 requiredParameters.putAllMetadata(parameters.toMutableMap())
                 graphState3A.writeTo(requiredParameters)
                 requiredParameters.putAllMetadata(cameraGraphConfig.requiredParameters)
+
+                if (processor == null || request == null) {
+                    // If a previous set of parameters haven't been submitted yet, consider it stale
+                    pendingParametersDeferred?.complete(false)
+
+                    debug { "Holding parameters to be submitted later" }
+                    deferredResult = CompletableDeferred<Boolean>()
+                    pendingParametersDeferred = deferredResult
+                    pendingParameters = requiredParameters
+                }
             }
 
             return@withContext when {
-                processor == null || request == null -> false
+                processor == null || request == null -> deferredResult?.await() == true
                 else ->
                     processor.submit(
                         isRepeating = false,
@@ -389,6 +431,7 @@
                     synchronized(lock) {
                         if (processor === _requestProcessor) {
                             currentRepeatingRequest = request
+                            trySubmitPendingParameters(processor, request)
                         }
                     }
                     succeededIndex = index
@@ -411,6 +454,25 @@
         }
     }
 
+    @GuardedBy("lock")
+    private fun trySubmitPendingParameters(processor: GraphRequestProcessor, request: Request) {
+        val parameters = pendingParameters
+        val deferred = pendingParametersDeferred
+        if (parameters != null && deferred != null) {
+            val resubmitResult = processor.submit(
+                isRepeating = false,
+                requests = listOf(request),
+                defaultParameters = cameraGraphConfig.defaultParameters,
+                requiredParameters = parameters,
+                listeners = graphListeners
+            )
+            deferred.complete(resubmitResult)
+
+            pendingParameters = null
+            pendingParametersDeferred = null
+        }
+    }
+
     private fun submitLoop() {
         var burst: List<Request>
         var processor: GraphRequestProcessor
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
index 5c7d156..af52872 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
@@ -21,6 +21,7 @@
 import android.os.Looper.getMainLooper
 import androidx.camera.camera2.pipe.CameraError
 import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.core.SystemTimeSource
 import androidx.camera.camera2.pipe.core.TimeSource
 import androidx.camera.camera2.pipe.core.Timestamps
@@ -127,22 +128,24 @@
         // This tests that a listener attached to the virtualCamera.state property will receive all
         // of the events, starting from CameraStateUnopened.
         val virtualCamera = VirtualCameraState(cameraId, graphListener)
+        val androidCameraDevice = AndroidCameraDevice(
+            testCamera.metadata,
+            testCamera.cameraDevice,
+            testCamera.cameraId,
+            cameraErrorListener,
+        )
+        val cameraStateClosing = CameraStateClosing()
+        val cameraStateClosed =
+            CameraStateClosed(
+                cameraId,
+                ClosedReason.CAMERA2_ERROR,
+                cameraErrorCode = CameraError.ERROR_CAMERA_SERVICE
+            )
         val states =
             listOf(
-                CameraStateOpen(
-                    AndroidCameraDevice(
-                        testCamera.metadata,
-                        testCamera.cameraDevice,
-                        testCamera.cameraId,
-                        cameraErrorListener,
-                    )
-                ),
-                CameraStateClosing(),
-                CameraStateClosed(
-                    cameraId,
-                    ClosedReason.CAMERA2_ERROR,
-                    cameraErrorCode = CameraError.ERROR_CAMERA_SERVICE
-                )
+                CameraStateOpen(androidCameraDevice),
+                cameraStateClosing,
+                cameraStateClosed
             )
 
         val events = mutableListOf<CameraState>()
@@ -159,8 +162,54 @@
         advanceUntilIdle()
         job.cancelAndJoin()
 
-        val expectedStates = listOf(CameraStateUnopened).plus(states)
-        assertThat(events).containsExactlyElementsIn(expectedStates)
+        assertThat(events[0]).isSameInstanceAs(CameraStateUnopened)
+
+        assertThat(events[1]).isInstanceOf(CameraStateOpen::class.java)
+        val deviceWrapper = (events[1] as CameraStateOpen).cameraDevice
+        assertThat(deviceWrapper).isInstanceOf(VirtualAndroidCameraDevice::class.java)
+        val androidCameraStateInside =
+            (deviceWrapper as VirtualAndroidCameraDevice).androidCameraDevice
+
+        assertThat(androidCameraStateInside).isSameInstanceAs(androidCameraDevice)
+        assertThat(events[2]).isSameInstanceAs(cameraStateClosing)
+        assertThat(events[3]).isSameInstanceAs(cameraStateClosed)
+    }
+
+    @Test
+    fun virtualAndroidCameraDeviceRejectsCallsWhenVirtualCameraStateIsDisconnected() = runTest {
+        val virtualCamera = VirtualCameraState(cameraId, graphListener)
+        val cameraState =
+            flowOf(
+                CameraStateOpen(
+                    AndroidCameraDevice(
+                        testCamera.metadata,
+                        testCamera.cameraDevice,
+                        testCamera.cameraId,
+                        cameraErrorListener,
+                    )
+                )
+            )
+        virtualCamera.connect(
+            cameraState,
+            object : Token {
+                override fun release(): Boolean {
+                    return true
+                }
+            })
+
+        virtualCamera.state.first { it !is CameraStateUnopened }
+
+        val virtualCameraState = virtualCamera.value
+        assertThat(virtualCameraState).isInstanceOf(CameraStateOpen::class.java)
+        val deviceWrapper = (virtualCameraState as CameraStateOpen).cameraDevice
+        assertThat(deviceWrapper).isInstanceOf(VirtualAndroidCameraDevice::class.java)
+
+        val virtualAndroidCameraState = deviceWrapper as VirtualAndroidCameraDevice
+        val result1 = virtualAndroidCameraState.createCaptureRequest(RequestTemplate(2))
+        virtualCamera.disconnect()
+        val result2 = virtualAndroidCameraState.createCaptureRequest(RequestTemplate(2))
+        assertThat(result1).isNotNull()
+        assertThat(result2).isNull()
     }
 }
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
index 29210c7..d95bfd2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/graph/GraphProcessorTest.kt
@@ -18,6 +18,7 @@
 
 import android.graphics.SurfaceTexture
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK
 import android.os.Build
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraError
@@ -33,6 +34,7 @@
 import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.firstOrNull
@@ -430,6 +432,67 @@
     }
 
     @Test
+    fun graphProcessorResubmitsParametersAfterGraphStarts() = runTest {
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener)
+            )
+
+        val result = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+        }
+        advanceUntilIdle()
+
+        graphProcessor.onGraphStarted(graphRequestProcessor1)
+        graphProcessor.startRepeating(request1)
+        advanceUntilIdle()
+
+        assertThat(result.await()).isTrue()
+    }
+
+    @Test
+    fun graphProcessorSubmitsLatestParametersWhenSubmittedTwiceBeforeGraphStarts() = runTest {
+        val graphProcessor =
+            GraphProcessorImpl(
+                FakeThreads.fromTestScope(this),
+                FakeGraphConfigs.graphConfig,
+                graphState3A,
+                this,
+                arrayListOf(globalListener)
+            )
+
+        val result1 = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to false))
+        }
+        advanceUntilIdle()
+        val result2 = async {
+            graphProcessor.trySubmit(mapOf<CaptureRequest.Key<*>, Any>(CONTROL_AE_LOCK to true))
+        }
+        advanceUntilIdle()
+
+        graphProcessor.onGraphStarted(graphRequestProcessor1)
+        advanceUntilIdle()
+
+        graphProcessor.startRepeating(request1)
+        advanceUntilIdle()
+
+        val event1 = fakeProcessor1.nextEvent()
+        assertThat(event1.requestSequence?.repeating).isTrue()
+        val event2 = fakeProcessor1.nextEvent()
+        assertThat(event2.requestSequence?.repeating).isFalse()
+        assertThat(
+            event2.requestSequence?.requestMetadata?.get(request1)?.get(CONTROL_AE_LOCK)
+        ).isTrue()
+
+        assertThat(result1.await()).isFalse()
+        assertThat(result2.await()).isTrue()
+    }
+
+    @Test
     fun graphProcessorChangesGraphStateOnError() = runTest {
         val graphProcessor =
             GraphProcessorImpl(
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index 128e70c..6f450b9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -71,7 +71,7 @@
         _requestQueue.add(requests)
     }
 
-    override suspend fun submit(parameters: Map<*, Any?>): Boolean {
+    override suspend fun trySubmit(parameters: Map<*, Any?>): Boolean {
         if (closed) {
             return false
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 0ed3b9a..088aa5d 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -1185,7 +1185,7 @@
         Map<DeferrableSurface, Long> streamUseCaseMap = new HashMap<>();
         StreamUseCaseUtil.populateSurfaceToStreamUseCaseMapping(
                 mUseCaseAttachState.getAttachedSessionConfigs(),
-                streamUseCaseMap, mCameraCharacteristicsCompat, false);
+                streamUseCaseMap, mCameraCharacteristicsCompat, true);
 
         mCaptureSession.setStreamUseCaseMap(streamUseCaseMap);
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/AeFpsRangeLegacyQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/AeFpsRangeLegacyQuirk.java
index 5f3c1b4..5e78461 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/AeFpsRangeLegacyQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/AeFpsRangeLegacyQuirk.java
@@ -64,8 +64,8 @@
     /**
      * Returns the fps range whose upper is 30 and whose lower is the smallest, or null if no
      * range has an upper equal to 30.  The rational is:
-     * (1) Range upper is always 30 so that a smooth frame rate is guaranteed.
-     * (2) Range lower contains the smallest supported value so that it can adapt as much as
+     * 1. Range upper is always 30 so that a smooth frame rate is guaranteed.
+     * 2. Range lower contains the smallest supported value so that it can adapt as much as
      * possible to low light conditions.
      */
     @Nullable
diff --git a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
index b80fcd6..d9d3573 100644
--- a/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
+++ b/car/app/app-samples/showcase/common/src/main/res/values-or/strings.xml
@@ -38,7 +38,7 @@
     <string name="settings_action_title" msgid="8616900063253887861">"ସେଟିଂସ"</string>
     <string name="accept_action_title" msgid="4899660585470647578">"ଗ୍ରହଣ କରନ୍ତୁ"</string>
     <string name="reject_action_title" msgid="6730366705938402668">"ଅଗ୍ରାହ୍ୟ କରନ୍ତୁ"</string>
-    <string name="ok_action_title" msgid="7128494973966098611">"ଠିକ୍ ଅଛି"</string>
+    <string name="ok_action_title" msgid="7128494973966098611">"ଠିକ ଅଛି"</string>
     <string name="throw_action_title" msgid="7163710562670220163">"ଥ୍ରୋ କରନ୍ତୁ"</string>
     <string name="commute_action_title" msgid="2585755255290185096">"ଯାତାୟାତ"</string>
     <string name="sign_out_action_title" msgid="1653943000866713010">"ସାଇନ ଆଉଟ କରନ୍ତୁ"</string>
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
index 9f94b64..6a7ba4b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/BasicTextField2Test.kt
@@ -75,6 +75,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Ignore
@@ -874,6 +875,7 @@
         }
     }
 
+    @SdkSuppress(minSdkVersion = 23)
     @Test
     fun textField_showsKeyboardAgainWhenTapped_ifFocused() {
         val keyboardHelper = KeyboardHelper(rule)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
index 78064c7..45f4155 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text2/TextFieldCodepointTransformationTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
 import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.TextFieldLineLimits
 import androidx.compose.foundation.text2.input.TextFieldState
 import androidx.compose.foundation.text2.input.mask
 import androidx.compose.foundation.text2.input.setTextAndPlaceCursorAtEnd
@@ -160,6 +161,54 @@
         assertLayoutText("*".repeat("Hello, World!".length))
     }
 
+    @Test
+    fun textField_singleLine_removesLineFeedViaCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello World")
+        rule.onNodeWithTag(Tag).performTextInput("\n")
+        assertLayoutText("Hello World ")
+    }
+
+    @Test
+    fun textField_singleLine_removesCarriageReturnViaCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\rWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello\uFEFFWorld")
+    }
+
+    @Test
+    fun textField_singleLine_doesNotOverrideGivenCodepointTransformation() {
+        val state = TextFieldState()
+        state.setTextAndPlaceCursorAtEnd("Hello\nWorld")
+        rule.setContent {
+            BasicTextField2(
+                state = state,
+                lineLimits = TextFieldLineLimits.SingleLine,
+                codepointTransformation = CodepointTransformation.None,
+                modifier = Modifier.testTag(Tag)
+            )
+        }
+
+        assertLayoutText("Hello\nWorld")
+    }
+
     // TODO: add more tests when selection is added
 
     private fun assertLayoutText(text: String) {
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
index b51170c..a338feb 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/BasicTextField2.kt
@@ -34,6 +34,7 @@
 import androidx.compose.foundation.text.heightInLines
 import androidx.compose.foundation.text.textFieldMinSize
 import androidx.compose.foundation.text2.input.CodepointTransformation
+import androidx.compose.foundation.text2.input.SingleLineCodepointTransformation
 import androidx.compose.foundation.text2.input.TextEditFilter
 import androidx.compose.foundation.text2.input.TextFieldLineLimits
 import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine
@@ -91,7 +92,10 @@
  * is called. Note that this IME action may be different from what you specified in
  * [KeyboardOptions.imeAction].
  * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and
- * ignore newlines; or [MultiLine] and grow and scroll vertically.
+ * ignore newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed without
+ * specifying the [codepointTransformation] parameter, a [CodepointTransformation] is automatically
+ * applied. This transformation replaces any newline characters ('\n') within the text with regular
+ * whitespace (' '), ensuring that the contents of the text field are presented in a single line.
  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
  * [TextLayoutResult] object that callback provides contains paragraph information, size of the
  * text, baselines and other details. The callback can be used to add additional decoration or
@@ -225,7 +229,15 @@
 
             Layout(modifier = coreModifiers) { _, constraints ->
                 val result = with(textLayoutState) {
-                    val visualText = state.text.toVisualText(codepointTransformation)
+                    // First prefer provided codepointTransformation if not null, e.g.
+                    // BasicSecureTextField would send Password Transformation.
+                    // Second, apply a SingleLineCodepointTransformation if text field is configured
+                    // to be single line.
+                    // Else, don't apply any visual transformation.
+                    val appliedCodepointTransformation = codepointTransformation
+                         ?: SingleLineCodepointTransformation.takeIf { lineLimits == SingleLine }
+
+                    val visualText = state.text.toVisualText(appliedCodepointTransformation)
                     layout(
                         text = AnnotatedString(visualText.toString()),
                         textStyle = textStyle,
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
index c0f5d9e..df7909b 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/text2/input/CodepointTransformation.kt
@@ -77,6 +77,31 @@
     }
 }
 
+/**
+ * [CodepointTransformation] that converts all line breaks (\n) into white space(U+0020) and
+ * carriage returns(\r) to zero-width no-break space (U+FEFF). This transformation forces any
+ * content to appear as single line.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal object SingleLineCodepointTransformation : CodepointTransformation {
+
+    private const val LINE_FEED = '\n'.code
+    private const val CARRIAGE_RETURN = '\r'.code
+
+    private const val WHITESPACE = ' '.code
+    private const val ZERO_WIDTH_SPACE = '\uFEFF'.code
+
+    override fun transform(codepointIndex: Int, codepoint: Int): Int {
+        if (codepoint == LINE_FEED) return WHITESPACE
+        if (codepoint == CARRIAGE_RETURN) return ZERO_WIDTH_SPACE
+        return codepoint
+    }
+
+    override fun toString(): String {
+        return "SingleLineCodepointTransformation"
+    }
+}
+
 @OptIn(ExperimentalFoundationApi::class)
 internal fun CharSequence.toVisualText(
     codepointTransformation: CodepointTransformation?
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
index 02a9933..a304e5d 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SlotTable.kt
@@ -2187,7 +2187,11 @@
     fun moveFrom(table: SlotTable, index: Int, removeSourceGroup: Boolean = true): List<Anchor> {
         runtimeCheck(insertCount > 0)
 
-        if (index == 0 && currentGroup == 0 && this.table.groupsSize == 0) {
+        if (
+            index == 0 && currentGroup == 0 &&
+            this.table.groupsSize == 0 &&
+            table.groups.groupSize(index) == table.groupsSize
+        ) {
             // Special case for moving the entire slot table into an empty table. This case occurs
             // during initial composition.
             val myGroups = groups
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index dd53854..06d1f47 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -23,11 +23,11 @@
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.SnapshotThreadLocal
 import androidx.compose.runtime.collection.IdentityArraySet
+import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
 import androidx.compose.runtime.synchronized
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
-import androidx.compose.runtime.internal.JvmDefaultWithCompatibility
 
 /**
  * A snapshot of the values return by mutable states and other state objects. All state object
@@ -850,7 +850,7 @@
             // id to be forgotten as no state records will refer to it.
             this.modified = null
             val id = id
-            for (state in modified) {
+            modified.fastForEach { state ->
                 var current: StateRecord? = state.firstStateRecord
                 while (current != null) {
                     if (current.snapshotId == id || current.snapshotId in previousIds) {
@@ -885,12 +885,12 @@
         val start = this.invalid.set(id).or(this.previousIds)
         val modified = modified!!
         var statesToRemove: MutableList<StateObject>? = null
-        for (state in modified) {
+        modified.fastForEach { state ->
             val first = state.firstStateRecord
             // If either current or previous cannot be calculated the object was created
             // in a nested snapshot that was committed then changed.
-            val current = readable(first, snapshotId, invalidSnapshots) ?: continue
-            val previous = readable(first, id, start) ?: continue
+            val current = readable(first, snapshotId, invalidSnapshots) ?: return@fastForEach
+            val previous = readable(first, id, start) ?: return@fastForEach
             if (current != previous) {
                 val applied = readable(first, id, this.invalid) ?: readError()
                 val merged = optimisticMerges?.get(current) ?: run {
@@ -1403,12 +1403,12 @@
                 val result = innerApplyLocked(parent.id, optimisticMerges, parent.invalid)
                 if (result != SnapshotApplyResult.Success) return result
 
-                // Add all modified objects in this set to the parent
-                (
-                    parent.modified ?: IdentityArraySet<StateObject>().also {
+                parent.modified?.apply { addAll(modified) }
+                    ?: modified.also {
+                        // Ensure modified reference is only used by one snapshot
                         parent.modified = it
+                        this.modified = null
                     }
-                ).addAll(modified)
             }
 
             // Ensure the parent is newer than the current snapshot
@@ -2176,10 +2176,10 @@
     if (modified == null) return null
     val start = applyingSnapshot.invalid.set(applyingSnapshot.id).or(applyingSnapshot.previousIds)
     var result: MutableMap<StateRecord, StateRecord>? = null
-    for (state in modified) {
+    modified.fastForEach { state ->
         val first = state.firstStateRecord
-        val current = readable(first, id, invalidSnapshots) ?: continue
-        val previous = readable(first, id, start) ?: continue
+        val current = readable(first, id, invalidSnapshots) ?: return@fastForEach
+        val previous = readable(first, id, start) ?: return@fastForEach
         if (current != previous) {
             // Try to produce a merged state record
             val applied = readable(first, applyingSnapshot.id, applyingSnapshot.invalid)
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
index a4b3600..d6505b04 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/SlotTableTests.kt
@@ -1401,6 +1401,56 @@
     }
 
     @Test
+    fun testMovingFromMultiRootGroup() {
+        val sourceTable = SlotTable()
+        val destinationTable = SlotTable()
+
+        val anchors = mutableListOf<Anchor>()
+        sourceTable.write { writer ->
+            writer.insert {
+                writer.group(10) {
+                    anchors.add(writer.anchor(writer.parent))
+                    writer.group(100) {
+                        writer.group(1000) { }
+                        writer.group(1001) { }
+                        writer.group(1002) { }
+                        writer.group(10003) { }
+                    }
+                }
+                writer.group(20) {
+                    anchors.add(writer.anchor(writer.parent))
+                    writer.group(200) {
+                        writer.group(2000) { }
+                        writer.group(2001) { }
+                        writer.group(2002) { }
+                        writer.group(20003) { }
+                    }
+                }
+                writer.group(30) {
+                    anchors.add(writer.anchor(writer.parent))
+                    writer.group(300) {
+                        writer.group(3000) { }
+                        writer.group(3001) { }
+                        writer.group(3002) { }
+                        writer.group(30003) { }
+                    }
+                }
+            }
+        }
+        sourceTable.verifyWellFormed()
+
+        destinationTable.write { writer ->
+            for (anchor in anchors) {
+                writer.insert {
+                    writer.moveFrom(sourceTable, sourceTable.anchorIndex(anchor))
+                    sourceTable.verifyWellFormed()
+                }
+            }
+        }
+        destinationTable.verifyWellFormed()
+    }
+
+    @Test
     fun testToIndexFor() {
         val (slots, anchors) = narrowTrees()
         val indexes = anchors.map { it.toIndexFor(slots) }
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 17391e0..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
@@ -25,7 +25,6 @@
 import android.os.Handler
 import android.os.Looper
 import android.os.SystemClock
-import android.text.SpannableString
 import android.util.Log
 import android.view.MotionEvent
 import android.view.View
@@ -724,17 +723,6 @@
         }
     }
 
-    private fun isScreenReaderFocusable(
-        node: SemanticsNode
-    ): Boolean {
-        val isSpeakingNode = node.infoContentDescriptionOrNull != null ||
-            getInfoText(node) != null || getInfoStateDescriptionOrNull(node) != null ||
-            getInfoIsCheckable(node)
-
-        return node.unmergedConfig.isMergingSemanticsOfDescendants ||
-            node.isUnmergedLeafNode && isSpeakingNode
-    }
-
     @VisibleForTesting
     @OptIn(ExperimentalComposeUiApi::class)
     fun populateAccessibilityNodeInfoProperties(
@@ -742,6 +730,15 @@
         info: AccessibilityNodeInfoCompat,
         semanticsNode: SemanticsNode
     ) {
+        val isUnmergedLeafNode =
+            !semanticsNode.isFake &&
+                semanticsNode.replacedChildren.isEmpty() &&
+                semanticsNode.layoutNode.findClosestParentNode {
+                    it.outerSemantics
+                        ?.collapsedSemanticsConfiguration()
+                        ?.isMergingSemanticsOfDescendants == true
+                } == null
+
         // set classname
         info.className = ClassName
         val role = semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Role)
@@ -756,7 +753,7 @@
                     // Images are often minor children of larger widgets, so we only want to
                     // announce the Image role when the image itself is focusable.
                     if (role != Role.Image ||
-                        semanticsNode.isUnmergedLeafNode ||
+                        isUnmergedLeafNode ||
                         semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants
                     ) {
                         info.className = className
@@ -801,17 +798,39 @@
 
         setText(semanticsNode, info)
         setContentInvalid(semanticsNode, info)
-        setStateDescription(semanticsNode, info)
-        setIsCheckable(semanticsNode, info)
 
+        info.stateDescription =
+            semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.StateDescription)
         val toggleState = semanticsNode.unmergedConfig.getOrNull(
             SemanticsProperties.ToggleableState
         )
         toggleState?.let {
-            if (toggleState == ToggleableState.On) {
-                info.isChecked = true
-            } else if (toggleState == ToggleableState.Off) {
-                info.isChecked = false
+            info.isCheckable = true
+            when (it) {
+                ToggleableState.On -> {
+                    info.isChecked = true
+                    // Unfortunately, talback has a bug of using "checked", so we set state
+                    // description here
+                    if (role == Role.Switch && info.stateDescription == null) {
+                        info.stateDescription = view.context.resources.getString(R.string.on)
+                    }
+                }
+
+                ToggleableState.Off -> {
+                    info.isChecked = false
+                    // Unfortunately, talkback has a bug of using "not checked", so we set state
+                    // description here
+                    if (role == Role.Switch && info.stateDescription == null) {
+                        info.stateDescription = view.context.resources.getString(R.string.off)
+                    }
+                }
+
+                ToggleableState.Indeterminate -> {
+                    if (info.stateDescription == null) {
+                        info.stateDescription =
+                            view.context.resources.getString(R.string.indeterminate)
+                    }
+                }
             }
         }
         semanticsNode.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
@@ -819,7 +838,18 @@
                 // Tab in native android uses selected property
                 info.isSelected = it
             } else {
+                info.isCheckable = true
                 info.isChecked = it
+                if (info.stateDescription == null) {
+                    // If a radio entry (radio button + text) is selectable, it won't have the role
+                    // RadioButton, so if we use info.isCheckable/info.isChecked, talkback will say
+                    // "checked/not checked" instead "selected/note selected".
+                    info.stateDescription = if (it) {
+                        view.context.resources.getString(R.string.selected)
+                    } else {
+                        view.context.resources.getString(R.string.not_selected)
+                    }
+                }
             }
         }
 
@@ -1035,6 +1065,29 @@
                     rangeInfo.range.endInclusive,
                     rangeInfo.current
                 )
+                // let's set state description here and use state description change events.
+                // otherwise, we need to send out type_view_selected event, as the old android
+                // versions do. But the support for type_view_selected event for progress bars
+                // maybe deprecated in talkback in the future.
+                if (info.stateDescription == null) {
+                    val valueRange = rangeInfo.range
+                    val progress = (
+                        if (valueRange.endInclusive - valueRange.start == 0f) 0f
+                        else (rangeInfo.current - valueRange.start) /
+                            (valueRange.endInclusive - valueRange.start)
+                        ).coerceIn(0f, 1f)
+
+                    // We only display 0% or 100% when it is exactly 0% or 100%.
+                    val percent = when (progress) {
+                        0f -> 0
+                        1f -> 100
+                        else -> (progress * 100).roundToInt().coerceIn(1, 99)
+                    }
+                    info.stateDescription =
+                        view.context.resources.getString(R.string.template_percent, percent)
+                }
+            } else if (info.stateDescription == null) {
+                info.stateDescription = view.context.resources.getString(R.string.in_progress)
             }
             if (semanticsNode.unmergedConfig.contains(SemanticsActions.SetProgress) &&
                 semanticsNode.enabled()
@@ -1210,7 +1263,12 @@
             }
         }
 
-        info.isScreenReaderFocusable = isScreenReaderFocusable(semanticsNode)
+        val isSpeakingNode = info.contentDescription != null || info.text != null ||
+            info.hintText != null || info.stateDescription != null || info.isCheckable
+
+        info.isScreenReaderFocusable =
+            semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants ||
+                isUnmergedLeafNode && isSpeakingNode
 
         if (idToBeforeMap[virtualViewId] != null) {
             idToBeforeMap[virtualViewId]?.let { info.setTraversalBefore(view, it) }
@@ -1235,131 +1293,10 @@
         }
     }
 
-    private fun getInfoStateDescriptionOrNull(
-        node: SemanticsNode
-    ): String? {
-        var stateDescription = node.unmergedConfig.getOrNull(SemanticsProperties.StateDescription)
-        val toggleState = node.unmergedConfig.getOrNull(SemanticsProperties.ToggleableState)
-        val role = node.unmergedConfig.getOrNull(SemanticsProperties.Role)
-
-        // Check toggle state and retrieve description accordingly
-        toggleState?.let {
-            when (it) {
-                ToggleableState.On -> {
-                    // Unfortunately, talkback has a bug of using "checked", so we set state
-                    // description here
-                    if (role == Role.Switch && stateDescription == null) {
-                        stateDescription = view.context.resources.getString(R.string.on)
-                    }
-                }
-
-                ToggleableState.Off -> {
-                    // Unfortunately, talkback has a bug of using "not checked", so we set state
-                    // description here
-                    if (role == Role.Switch && stateDescription == null) {
-                        stateDescription = view.context.resources.getString(R.string.off)
-                    }
-                }
-
-                ToggleableState.Indeterminate -> {
-                    if (stateDescription == null) {
-                        stateDescription =
-                            view.context.resources.getString(R.string.indeterminate)
-                    }
-                }
-            }
-        }
-
-        // Check Selected property and retrieve description accordingly
-        node.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
-            if (role != Role.Tab) {
-                if (stateDescription == null) {
-                    // If a radio entry (radio button + text) is selectable, it won't have the role
-                    // RadioButton, so if we use info.isCheckable/info.isChecked, talkback will say
-                    // "checked/not checked" instead "selected/note selected".
-                    stateDescription = if (it) {
-                        view.context.resources.getString(R.string.selected)
-                    } else {
-                        view.context.resources.getString(R.string.not_selected)
-                    }
-                }
-            }
-        }
-
-        // Check if a node has progress bar range info and retrieve description accordingly
-        val rangeInfo =
-            node.unmergedConfig.getOrNull(SemanticsProperties.ProgressBarRangeInfo)
-        rangeInfo?.let {
-            // let's set state description here and use state description change events.
-            // otherwise, we need to send out type_view_selected event, as the old android
-            // versions do. But the support for type_view_selected event for progress bars
-            // maybe deprecated in talkback in the future.
-            if (rangeInfo !== ProgressBarRangeInfo.Indeterminate) {
-                if (stateDescription == null) {
-                    val valueRange = rangeInfo.range
-                    val progress = (
-                        if (valueRange.endInclusive - valueRange.start == 0f) 0f
-                        else (rangeInfo.current - valueRange.start) /
-                            (valueRange.endInclusive - valueRange.start)
-                        ).coerceIn(0f, 1f)
-
-                    // We only display 0% or 100% when it is exactly 0% or 100%.
-                    val percent = when (progress) {
-                        0f -> 0
-                        1f -> 100
-                        else -> (progress * 100).roundToInt().coerceIn(1, 99)
-                    }
-                    stateDescription =
-                        view.context.resources.getString(R.string.template_percent, percent)
-                }
-            } else if (stateDescription == null) {
-                stateDescription = view.context.resources.getString(R.string.in_progress)
-            }
-        }
-
-        return stateDescription
-    }
-
-    private fun setStateDescription(
+    private fun setText(
         node: SemanticsNode,
         info: AccessibilityNodeInfoCompat,
     ) {
-        info.stateDescription = getInfoStateDescriptionOrNull(node)
-    }
-
-    private fun getInfoIsCheckable(
-        node: SemanticsNode
-    ): Boolean {
-        var isCheckable = false
-        val toggleState = node.unmergedConfig.getOrNull(SemanticsProperties.ToggleableState)
-        val role = node.unmergedConfig.getOrNull(SemanticsProperties.Role)
-
-        toggleState?.let {
-            isCheckable = true
-        }
-
-        node.unmergedConfig.getOrNull(SemanticsProperties.Selected)?.let {
-            if (role != Role.Tab) {
-                isCheckable = true
-            }
-        }
-
-        return isCheckable
-    }
-
-    private fun setIsCheckable(
-        node: SemanticsNode,
-        info: AccessibilityNodeInfoCompat
-    ) {
-        info.isCheckable = getInfoIsCheckable(node)
-    }
-
-    // This needs to be here instead of around line 3000 because we need access to the `view`
-    // that is inside the `AndroidComposeViewAccessibilityDelegateCompat` class
-    @OptIn(InternalTextApi::class)
-    private fun getInfoText(
-        node: SemanticsNode
-    ): SpannableString? {
         val fontFamilyResolver: FontFamily.Resolver = view.fontFamilyResolver
         val editableTextToAssign = trimToSize(
             node.unmergedConfig.getTextForTextField()
@@ -1380,14 +1317,8 @@
                 ),
             ParcelSafeTextLength
         )
-        return editableTextToAssign ?: textToAssign
-    }
 
-    private fun setText(
-        node: SemanticsNode,
-        info: AccessibilityNodeInfoCompat,
-    ) {
-        info.text = getInfoText(node)
+        info.text = editableTextToAssign ?: textToAssign
     }
 
     /**
@@ -2817,10 +2748,10 @@
         }
         if (bufferedContentCaptureDisappearedNodes.isNotEmpty()) {
             session.notifyViewsDisappeared(
-                bufferedContentCaptureDisappearedNodes
-                    .toList()
-                    .fastMap { it.toLong() }
-                    .toLongArray())
+                    bufferedContentCaptureDisappearedNodes
+                        .toList()
+                        .fastMap { it.toLong() }
+                        .toLongArray())
             bufferedContentCaptureDisappearedNodes.clear()
         }
     }
@@ -3277,9 +3208,6 @@
         return false
     }
 
-private val SemanticsNode.infoContentDescriptionOrNull get() = this.unmergedConfig.getOrNull(
-    SemanticsProperties.ContentDescription)?.firstOrNull()
-
 @OptIn(ExperimentalComposeUiApi::class)
 private fun SemanticsNode.excludeLineAndPageGranularities(): Boolean {
     // text field that is not in focus
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index c56a5d2..ab6678e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -78,13 +78,6 @@
 
     internal val unmergedConfig = outerSemanticsNode.collapsedSemanticsConfiguration()
 
-    internal var isUnmergedLeafNode =
-        !isFake && replacedChildren.isEmpty() && layoutNode.findClosestParentNode {
-            it.outerSemantics
-                ?.collapsedSemanticsConfiguration()
-                ?.isMergingSemanticsOfDescendants == true
-        } == null
-
     /**
      * The [LayoutInfo] that this is associated with.
      */
diff --git a/constraintlayout/constraintlayout-compose/api/api_lint.ignore b/constraintlayout/constraintlayout-compose/api/api_lint.ignore
index 77dd380..33c2cfc 100644
--- a/constraintlayout/constraintlayout-compose/api/api_lint.ignore
+++ b/constraintlayout/constraintlayout-compose/api/api_lint.ignore
@@ -5,29 +5,11 @@
     Parameter type is concrete collection (`java.util.HashMap`); must be higher-level interface
 
 
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.compose.ui.Modifier, int, androidx.constraintlayout.compose.MotionLayoutState, androidx.constraintlayout.compose.MotionScene, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #0:
+KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, int, androidx.constraintlayout.compose.LayoutInformationReceiver, int, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #2:
     Parameter `modifier` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.compose.ui.Modifier, int, androidx.constraintlayout.compose.MotionLayoutState, androidx.constraintlayout.compose.MotionScene, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #1:
-    Parameter `optimizationLevel` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags>, androidx.constraintlayout.compose.LayoutInformationReceiver, int, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag>, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #2:
-    Parameter `modifier` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags>, androidx.constraintlayout.compose.LayoutInformationReceiver, int, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag>, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #3:
+KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, int, androidx.constraintlayout.compose.LayoutInformationReceiver, int, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #3:
     Parameter `transition` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags>, int, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag>, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #2:
+KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, int, int, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #2:
     Parameter `modifier` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags>, int, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag>, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #3:
+KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionLayoutKt#MotionLayout(androidx.constraintlayout.compose.ConstraintSet, androidx.constraintlayout.compose.ConstraintSet, androidx.compose.ui.Modifier, androidx.constraintlayout.compose.Transition, float, int, int, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit>) parameter #3:
     Parameter `transition` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionSceneScope#addConstraintSet(String, androidx.constraintlayout.compose.ConstraintSet) parameter #0:
-    Parameter `name` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionSceneScope#addTransition(String, androidx.constraintlayout.compose.Transition) parameter #0:
-    Parameter `name` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-KotlinDefaultParameterOrder: androidx.constraintlayout.compose.MotionSceneScope#transition(String, androidx.constraintlayout.compose.ConstraintSetRef, androidx.constraintlayout.compose.ConstraintSetRef, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.TransitionScope,kotlin.Unit>) parameter #0:
-    Parameter `name` has a default value and should come after all parameters without default values (except for a trailing lambda parameter)
-
-
-MethodNameUnits: androidx.constraintlayout.compose.TransitionScope#getDurationMs():
-    Expected method name units to be `Millis or Micros`, was `Ms` in `getDurationMs`
-
-
-MutableBareField: androidx.constraintlayout.compose.State#layoutDirection:
-    Bare field layoutDirection must be marked final, or moved behind accessors if mutable
diff --git a/constraintlayout/constraintlayout-compose/api/current.txt b/constraintlayout/constraintlayout-compose/api/current.txt
index 5190382..3d50674 100644
--- a/constraintlayout/constraintlayout-compose/api/current.txt
+++ b/constraintlayout/constraintlayout-compose/api/current.txt
@@ -442,11 +442,11 @@
     enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags UNKNOWN;
   }
 
-  public enum MotionLayoutFlag {
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
+  @Deprecated public enum MotionLayoutFlag {
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
   }
 
   @kotlin.jvm.JvmInline public final value class Skip {
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index c230eb2..92a3feb 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -335,6 +335,24 @@
     property public final androidx.constraintlayout.compose.CurveFit Spline;
   }
 
+  @androidx.constraintlayout.compose.ExperimentalMotionApi @kotlin.jvm.JvmInline public final value class DebugFlags {
+    ctor public DebugFlags(optional boolean showBounds, optional boolean showPaths, optional boolean showKeyPositions);
+    method public boolean getShowBounds();
+    method public boolean getShowKeyPositions();
+    method public boolean getShowPaths();
+    property public final boolean showBounds;
+    property public final boolean showKeyPositions;
+    property public final boolean showPaths;
+    field public static final androidx.constraintlayout.compose.DebugFlags.Companion Companion;
+  }
+
+  public static final class DebugFlags.Companion {
+    method public int getAll();
+    method public int getNone();
+    property public final int All;
+    property public final int None;
+  }
+
   public final class DesignElements {
     method public void define(String name, kotlin.jvm.functions.Function2<? super java.lang.String,? super java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit> function);
     method public java.util.HashMap<java.lang.String,kotlin.jvm.functions.Function2<java.lang.String,java.util.HashMap<java.lang.String,java.lang.String>,kotlin.Unit>> getMap();
@@ -617,18 +635,18 @@
     enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags UNKNOWN;
   }
 
-  public enum MotionLayoutFlag {
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
+  @Deprecated public enum MotionLayoutFlag {
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
   }
 
   public final class MotionLayoutKt {
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, float progress, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, float progress, optional java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, optional androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, float progress, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, float progress, optional androidx.compose.ui.Modifier modifier, optional String transitionName, optional int debugFlags, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional int debugFlags, optional int optimizationLevel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, optional androidx.compose.ui.Modifier modifier, optional androidx.constraintlayout.compose.Transition? transition, float progress, optional int debugFlags, optional androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @androidx.constraintlayout.compose.ExperimentalMotionApi public static inline void MotionLayout(androidx.constraintlayout.compose.MotionScene motionScene, androidx.constraintlayout.compose.MotionLayoutState motionLayoutState, optional androidx.compose.ui.Modifier modifier, optional int optimizationLevel, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
   }
 
diff --git a/constraintlayout/constraintlayout-compose/api/restricted_current.txt b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
index 4611a20..6038cb7 100644
--- a/constraintlayout/constraintlayout-compose/api/restricted_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/restricted_current.txt
@@ -558,16 +558,16 @@
     enum_constant public static final androidx.constraintlayout.compose.MotionLayoutDebugFlags UNKNOWN;
   }
 
-  public enum MotionLayoutFlag {
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
-    method public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
-    enum_constant public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
+  @Deprecated public enum MotionLayoutFlag {
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
+    method @Deprecated public static androidx.constraintlayout.compose.MotionLayoutFlag[] values();
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag Default;
+    enum_constant @Deprecated public static final androidx.constraintlayout.compose.MotionLayoutFlag FullMeasure;
   }
 
   public final class MotionLayoutKt {
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional androidx.constraintlayout.compose.MotionLayoutDebugFlags debugFlag, optional int optimizationLevel, optional java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, java.util.Set<? extends androidx.constraintlayout.compose.MotionLayoutFlag> motionLayoutFlags, java.util.EnumSet<androidx.constraintlayout.compose.MotionLayoutDebugFlags> debug, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, optional androidx.compose.ui.Modifier modifier, optional String? constraintSetName, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional int debugFlags, optional int optimizationLevel, optional kotlin.jvm.functions.Function0<kotlin.Unit>? finishedAnimationListener, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, float progress, String transitionName, int optimizationLevel, int debugFlags, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.MotionScene motionScene, String transitionName, androidx.constraintlayout.compose.MotionLayoutStateImpl motionLayoutState, int optimizationLevel, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static inline void MotionLayoutCore(androidx.constraintlayout.compose.ConstraintSet start, androidx.constraintlayout.compose.ConstraintSet end, androidx.constraintlayout.compose.TransitionImpl? transition, androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver, int optimizationLevel, boolean showBounds, boolean showPaths, boolean showKeyPositions, androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.constraintlayout.compose.MotionLayoutScope,kotlin.Unit> content);
     method @androidx.compose.runtime.Composable @kotlin.PublishedApi internal static void UpdateWithForcedIfNoUserChange(androidx.constraintlayout.compose.MotionProgress motionProgress, androidx.constraintlayout.compose.LayoutInformationReceiver? informationReceiver);
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
index ba68ca1..2656198 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionCarousel.kt
@@ -238,7 +238,6 @@
     MotionLayout(motionScene = motionScene,
         transitionName = transitionName.value,
         progress = mprogress,
-        motionLayoutFlags = setOf(MotionLayoutFlag.FullMeasure), // TODO: only apply as needed
         modifier = Modifier
             .fillMaxSize()
             .background(Color.White)
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
index b961b63..61b5b95 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/MotionLayout.kt
@@ -50,15 +50,17 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.constraintlayout.core.widgets.Optimizer
-import java.util.EnumSet
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.launch
 
 /**
  * Measure flags for MotionLayout
  */
+@Deprecated("Unnecessary, MotionLayout remeasures when its content changes.")
 enum class MotionLayoutFlag(@Suppress("UNUSED_PARAMETER") value: Long) {
     Default(0),
+
+    @Suppress("unused")
     FullMeasure(1)
 }
 
@@ -72,7 +74,6 @@
  * Layout that interpolate its children layout given two sets of constraint and
  * a progress (from 0 to 1)
  */
-@Suppress("UNUSED_PARAMETER")
 @ExperimentalMotionApi
 @Composable
 inline fun MotionLayout(
@@ -81,13 +82,11 @@
     modifier: Modifier = Modifier,
     transition: Transition? = null,
     progress: Float,
-    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
+    debugFlags: DebugFlags = DebugFlags.None,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
     crossinline content: @Composable MotionLayoutScope.() -> Unit
 ) {
     val motionProgress = createAndUpdateMotionProgress(progress = progress)
-    val showDebug = debug.firstOrNull() == MotionLayoutDebugFlags.SHOW_ALL
     MotionLayoutCore(
         start = start,
         end = end,
@@ -95,9 +94,9 @@
         motionProgress = motionProgress,
         informationReceiver = null,
         optimizationLevel = optimizationLevel,
-        showBounds = showDebug,
-        showPaths = showDebug,
-        showKeyPositions = showDebug,
+        showBounds = debugFlags.showBounds,
+        showPaths = debugFlags.showPaths,
+        showKeyPositions = debugFlags.showKeyPositions,
         modifier = modifier,
         content = content
     )
@@ -114,19 +113,17 @@
     progress: Float,
     modifier: Modifier = Modifier,
     transitionName: String = "default",
-    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
+    debugFlags: DebugFlags = DebugFlags.None,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
     crossinline content: @Composable (MotionLayoutScope.() -> Unit),
 ) {
     MotionLayoutCore(
         motionScene = motionScene,
         progress = progress,
-        debug = debug,
+        debugFlags = debugFlags,
         modifier = modifier,
         optimizationLevel = optimizationLevel,
         transitionName = transitionName,
-        motionLayoutFlags = motionLayoutFlags,
         content = content
     )
 }
@@ -150,9 +147,8 @@
     modifier: Modifier = Modifier,
     constraintSetName: String? = null,
     animationSpec: AnimationSpec<Float> = tween(),
-    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
+    debugFlags: DebugFlags = DebugFlags.None,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
     noinline finishedAnimationListener: (() -> Unit)? = null,
     crossinline content: @Composable (MotionLayoutScope.() -> Unit)
 ) {
@@ -160,16 +156,14 @@
         motionScene = motionScene,
         constraintSetName = constraintSetName,
         animationSpec = animationSpec,
-        debugFlag = debug.firstOrNull() ?: MotionLayoutDebugFlags.NONE,
+        debugFlags = debugFlags,
         modifier = modifier,
         optimizationLevel = optimizationLevel,
         finishedAnimationListener = finishedAnimationListener,
-        motionLayoutFlags = motionLayoutFlags,
         content = content
     )
 }
 
-@Suppress("UNUSED_PARAMETER")
 @ExperimentalMotionApi
 @Composable
 inline fun MotionLayout(
@@ -178,14 +172,12 @@
     modifier: Modifier = Modifier,
     transition: Transition? = null,
     progress: Float,
-    debug: EnumSet<MotionLayoutDebugFlags> = EnumSet.of(MotionLayoutDebugFlags.NONE),
+    debugFlags: DebugFlags = DebugFlags.None,
     informationReceiver: LayoutInformationReceiver? = null,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
     crossinline content: @Composable (MotionLayoutScope.() -> Unit)
 ) {
     val motionProgress = createAndUpdateMotionProgress(progress = progress)
-    val showDebug = debug.firstOrNull() == MotionLayoutDebugFlags.SHOW_ALL
     MotionLayoutCore(
         start = start,
         end = end,
@@ -193,9 +185,9 @@
         motionProgress = motionProgress,
         informationReceiver = informationReceiver,
         optimizationLevel = optimizationLevel,
-        showBounds = showDebug,
-        showPaths = showDebug,
-        showKeyPositions = showDebug,
+        showBounds = debugFlags.showBounds,
+        showPaths = debugFlags.showPaths,
+        showKeyPositions = debugFlags.showKeyPositions,
         modifier = modifier,
         content = content
     )
@@ -204,16 +196,15 @@
 @ExperimentalMotionApi
 @PublishedApi
 @Composable
-@Suppress("UnavailableSymbol", "UNUSED_PARAMETER")
+@Suppress("UnavailableSymbol")
 internal inline fun MotionLayoutCore(
     @Suppress("HiddenTypeParameter")
     motionScene: MotionScene,
     modifier: Modifier = Modifier,
     constraintSetName: String? = null,
     animationSpec: AnimationSpec<Float> = tween(),
-    debugFlag: MotionLayoutDebugFlags = MotionLayoutDebugFlags.NONE,
+    debugFlags: DebugFlags = DebugFlags.None,
     optimizationLevel: Int = Optimizer.OPTIMIZATION_STANDARD,
-    motionLayoutFlags: Set<MotionLayoutFlag> = setOf<MotionLayoutFlag>(),
     noinline finishedAnimationListener: (() -> Unit)? = null,
     @Suppress("HiddenTypeParameter")
     crossinline content: @Composable (MotionLayoutScope.() -> Unit)
@@ -293,9 +284,9 @@
         motionProgress = motionProgress,
         informationReceiver = motionScene as? LayoutInformationReceiver,
         optimizationLevel = optimizationLevel,
-        showBounds = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
-        showPaths = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
-        showKeyPositions = debugFlag == MotionLayoutDebugFlags.SHOW_ALL,
+        showBounds = debugFlags.showBounds,
+        showPaths = debugFlags.showPaths,
+        showKeyPositions = debugFlags.showKeyPositions,
         modifier = modifier,
         content = content
     )
@@ -311,8 +302,7 @@
     progress: Float,
     transitionName: String,
     optimizationLevel: Int,
-    motionLayoutFlags: Set<MotionLayoutFlag>,
-    debug: EnumSet<MotionLayoutDebugFlags>,
+    debugFlags: DebugFlags,
     modifier: Modifier,
     @Suppress("HiddenTypeParameter")
     crossinline content: @Composable MotionLayoutScope.() -> Unit,
@@ -338,11 +328,10 @@
         end = end,
         transition = transition,
         progress = progress,
-        debug = debug,
+        debugFlags = debugFlags,
         informationReceiver = motionScene as? LayoutInformationReceiver,
         modifier = modifier,
         optimizationLevel = optimizationLevel,
-        motionLayoutFlags = motionLayoutFlags,
         content = content
     )
 }
@@ -958,4 +947,78 @@
      * states.
      */
     Content
+}
+
+/**
+ * Flags to use with MotionLayout to enable visual debugging.
+ *
+ * @property showBounds
+ * @property showPaths
+ * @property showKeyPositions
+ *
+ * @see None
+ * @see All
+ */
+@ExperimentalMotionApi
+@JvmInline
+value class DebugFlags internal constructor(private val flags: Int) {
+    /**
+     * @param showBounds Whether to show the bounds of widgets at the start and end of the current transition.
+     * @param showPaths Whether to show the paths each widget will take through the current transition.
+     * @param showKeyPositions Whether to show a diamond icon representing KeyPositions defined for each widget along the path.
+     */
+    constructor(
+        showBounds: Boolean = false,
+        showPaths: Boolean = false,
+        showKeyPositions: Boolean = false
+    ) : this(
+        (if (showBounds) BOUNDS_FLAG else 0) or
+            (if (showPaths) PATHS_FLAG else 0) or
+            (if (showKeyPositions) KEY_POSITIONS_FLAG else 0)
+    )
+
+    /**
+     * When enabled, shows the bounds of widgets at the start and end of the current transition.
+     */
+    val showBounds: Boolean
+        get() = flags and BOUNDS_FLAG > 0
+
+    /**
+     * When enabled, shows the paths each widget will take through the current transition.
+     */
+    val showPaths: Boolean
+        get() = flags and PATHS_FLAG > 0
+
+    /**
+     *
+     * When enabled, shows a diamond icon representing KeyPositions defined for each widget along
+     * the path.
+     */
+    val showKeyPositions: Boolean
+        get() = flags and KEY_POSITIONS_FLAG > 0
+
+    override fun toString(): String =
+        "DebugFlags(" +
+            "showBounds = $showBounds, " +
+            "showPaths = $showPaths, " +
+            "showKeyPositions = $showKeyPositions" +
+            ")"
+
+    companion object {
+        private const val BOUNDS_FLAG = 1
+        private const val PATHS_FLAG = 1 shl 1
+        private const val KEY_POSITIONS_FLAG = 1 shl 2
+
+        /**
+         * [DebugFlags] instance with all flags disabled.
+         */
+        val None = DebugFlags(0)
+
+        /**
+         * [DebugFlags] instance with all flags enabled.
+         *
+         * Note that this includes any flags added in the future.
+         */
+        val All = DebugFlags(-1)
+    }
 }
\ No newline at end of file
diff --git a/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt b/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt
new file mode 100644
index 0000000..76d06f2
--- /dev/null
+++ b/constraintlayout/constraintlayout-compose/src/androidTest/kotlin/androidx/constraintlayout/compose/DebugFlagsTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.constraintlayout.compose
+
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
+import org.junit.Test
+
+@OptIn(ExperimentalMotionApi::class)
+class DebugFlagsTest {
+    @Test
+    fun testFlags() {
+        assertEquals(
+            DebugFlags.None,
+            DebugFlags()
+        )
+        assertEquals(
+            DebugFlags.None,
+            DebugFlags(
+                showBounds = false,
+                showPaths = false,
+                showKeyPositions = false
+            )
+        )
+
+        // Not equals because All includes potential future flags
+        assertNotEquals(
+            DebugFlags.All,
+            DebugFlags(
+                showBounds = true,
+                showPaths = true,
+                showKeyPositions = true
+            )
+        )
+
+        var flags = DebugFlags.All
+        assertTrue(flags.showBounds)
+        assertTrue(flags.showPaths)
+        assertTrue(flags.showKeyPositions)
+
+        flags = DebugFlags.None
+        assertFalse(flags.showBounds)
+        assertFalse(flags.showPaths)
+        assertFalse(flags.showKeyPositions)
+
+        flags = DebugFlags(showBounds = true, showPaths = true, showKeyPositions = true)
+        assertTrue(flags.showBounds)
+        assertTrue(flags.showPaths)
+        assertTrue(flags.showKeyPositions)
+
+        flags = DebugFlags(showBounds = false, showPaths = false, showKeyPositions = false)
+        assertFalse(flags.showBounds)
+        assertFalse(flags.showPaths)
+        assertFalse(flags.showKeyPositions)
+
+        flags = DebugFlags(showBounds = true)
+        assertTrue(flags.showBounds)
+        assertFalse(flags.showPaths)
+        assertFalse(flags.showKeyPositions)
+
+        flags = DebugFlags(showPaths = true)
+        assertFalse(flags.showBounds)
+        assertTrue(flags.showPaths)
+        assertFalse(flags.showKeyPositions)
+
+        flags = DebugFlags(showKeyPositions = true)
+        assertFalse(flags.showBounds)
+        assertFalse(flags.showPaths)
+        assertTrue(flags.showKeyPositions)
+    }
+
+    @Test
+    fun testToString() {
+        assertEquals(
+            "DebugFlags(showBounds = true, showPaths = true, showKeyPositions = true)",
+            DebugFlags.All.toString()
+        )
+        assertEquals(
+            "DebugFlags(showBounds = false, showPaths = false, showKeyPositions = false)",
+            DebugFlags.None.toString()
+        )
+
+        assertEquals(
+            "DebugFlags(showBounds = false, showPaths = false, showKeyPositions = false)",
+            DebugFlags().toString()
+        )
+        assertEquals(
+            "DebugFlags(showBounds = true, showPaths = true, showKeyPositions = true)",
+            DebugFlags(showBounds = true, showPaths = true, showKeyPositions = true).toString()
+        )
+        assertEquals(
+            "DebugFlags(showBounds = true, showPaths = false, showKeyPositions = false)",
+            DebugFlags(showBounds = true, showPaths = false, showKeyPositions = false).toString()
+        )
+        assertEquals(
+            "DebugFlags(showBounds = false, showPaths = true, showKeyPositions = false)",
+            DebugFlags(showBounds = false, showPaths = true, showKeyPositions = false).toString()
+        )
+        assertEquals(
+            "DebugFlags(showBounds = false, showPaths = false, showKeyPositions = true)",
+            DebugFlags(showBounds = false, showPaths = false, showKeyPositions = true).toString()
+        )
+    }
+}
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
index 1df0251..725d21b 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/HiddenActivity.kt
@@ -44,6 +44,7 @@
 open class HiddenActivity : Activity() {
 
     private var resultReceiver: ResultReceiver? = null
+    private var mWaitingForActivityResult = false
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -56,6 +57,11 @@
             finish()
         }
 
+        restoreState(savedInstanceState)
+        if (mWaitingForActivityResult) {
+            return; // Past call still active
+        }
+
         when (type) {
             CredentialProviderBaseController.BEGIN_SIGN_IN_TAG -> {
                 handleBeginSignIn()
@@ -72,6 +78,12 @@
         }
     }
 
+    private fun restoreState(savedInstanceState: Bundle?) {
+        if (savedInstanceState != null) {
+            mWaitingForActivityResult = savedInstanceState.getBoolean(KEY_AWAITING_RESULT, false)
+        }
+    }
+
     private fun handleCreatePublicKeyCredential() {
         val fidoRegistrationRequest: PublicKeyCredentialCreationOptions? = intent
             .getParcelableExtra(CredentialProviderBaseController.REQUEST_TAG)
@@ -83,6 +95,7 @@
                 .getRegisterPendingIntent(fidoRegistrationRequest)
                 .addOnSuccessListener { result: PendingIntent ->
                     try {
+                        mWaitingForActivityResult = true
                         startIntentSenderForResult(
                             result.intentSender,
                             requestCode,
@@ -125,6 +138,11 @@
         finish()
     }
 
+    override fun onSaveInstanceState(outState: Bundle) {
+        outState.putBoolean(KEY_AWAITING_RESULT, mWaitingForActivityResult)
+        super.onSaveInstanceState(outState)
+    }
+
     private fun handleBeginSignIn() {
         val params: BeginSignInRequest? = intent.getParcelableExtra(
             CredentialProviderBaseController.REQUEST_TAG)
@@ -134,6 +152,7 @@
         params?.let {
             Identity.getSignInClient(this).beginSignIn(params).addOnSuccessListener {
                 try {
+                    mWaitingForActivityResult = true
                     startIntentSenderForResult(
                         it.pendingIntent.intentSender,
                         requestCode,
@@ -175,6 +194,7 @@
             Identity.getCredentialSavingClient(this).savePassword(params)
                 .addOnSuccessListener {
                     try {
+                        mWaitingForActivityResult = true
                         startIntentSenderForResult(
                             it.pendingIntent.intentSender,
                             requestCode,
@@ -186,7 +206,7 @@
                         )
                     } catch (e: IntentSender.SendIntentException) {
                         setupFailure(resultReceiver!!,
-                            GetCredentialUnknownException::class.java.name,
+                            CreateCredentialUnknownException::class.java.name,
                                 "During save password, found UI intent sender " +
                                     "failure: ${e.message}")
                     }
@@ -213,11 +233,13 @@
         bundle.putInt(CredentialProviderBaseController.ACTIVITY_REQUEST_CODE_TAG, requestCode)
         bundle.putParcelable(CredentialProviderBaseController.RESULT_DATA_TAG, data)
         resultReceiver?.send(resultCode, bundle)
+        mWaitingForActivityResult = false
         finish()
     }
 
     companion object {
         private const val DEFAULT_VALUE: Int = 1
         private val TAG: String = HiddenActivity::class.java.name
+        private const val KEY_AWAITING_RESULT = "androidx.credentials.playservices.AWAITING_RESULT"
     }
 }
\ No newline at end of file
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
index c6d6fcb..7e9365d 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderBaseController.kt
@@ -19,9 +19,11 @@
 import android.content.Intent
 import android.os.Parcel
 import android.os.ResultReceiver
+import androidx.credentials.exceptions.CreateCredentialCancellationException
 import androidx.credentials.exceptions.CreateCredentialException
 import androidx.credentials.exceptions.CreateCredentialInterruptedException
 import androidx.credentials.exceptions.CreateCredentialUnknownException
+import androidx.credentials.exceptions.GetCredentialCancellationException
 import androidx.credentials.exceptions.GetCredentialException
 import androidx.credentials.exceptions.GetCredentialInterruptedException
 import androidx.credentials.exceptions.GetCredentialUnknownException
@@ -86,6 +88,9 @@
         internal fun getCredentialExceptionTypeToException(typeName: String?, msg: String?):
             GetCredentialException {
             return when (typeName) {
+                GetCredentialCancellationException::class.java.name -> {
+                    GetCredentialCancellationException(msg)
+                }
                 GetCredentialInterruptedException::class.java.name -> {
                     GetCredentialInterruptedException(msg)
                 }
@@ -101,6 +106,9 @@
         internal fun createCredentialExceptionTypeToException(typeName: String?, msg: String?):
             CreateCredentialException {
             return when (typeName) {
+                CreateCredentialCancellationException::class.java.name -> {
+                    CreateCredentialCancellationException(msg)
+                }
                 CreateCredentialInterruptedException::class.java.name -> {
                     CreateCredentialInterruptedException(msg)
                 }
diff --git a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
index 2e75b82..4278e5f 100644
--- a/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
+++ b/credentials/credentials-play-services-auth/src/main/java/androidx/credentials/playservices/controllers/CredentialProviderController.kt
@@ -155,9 +155,10 @@
         val errType = resultData.getString(EXCEPTION_TYPE_TAG)
         val errMsg = resultData.getString(EXCEPTION_MESSAGE_TAG)
         val exception = conversionFn(errType, errMsg)
-        cancelOrCallbackExceptionOrResult(cancellationSignal) {
+        cancelOrCallbackExceptionOrResult(cancellationSignal = cancellationSignal,
+            onResultOrException = {
             executor.execute { callback.onError(exception) }
-        }
+        })
         return true
     }
 
diff --git a/datastore/datastore-benchmark/build.gradle b/datastore/datastore-benchmark/build.gradle
new file mode 100644
index 0000000..97ed619
--- /dev/null
+++ b/datastore/datastore-benchmark/build.gradle
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+import androidx.build.Publish
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("androidx.benchmark")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    androidTestImplementation(project(":datastore:datastore-core"))
+    androidTestImplementation(project(":internal-testutils-datastore"))
+    androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.kotlinCoroutinesTest)
+}
+
+android {
+    namespace "androidx.collection.benchmark"
+}
diff --git a/datastore/datastore-benchmark/src/androidTest/AndroidManifest.xml b/datastore/datastore-benchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..20b8acb
--- /dev/null
+++ b/datastore/datastore-benchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+<manifest
+        xmlns:android="http://schemas.android.com/apk/res/android">
+    <application>
+        <!-- enable profiling by shell for non-intrusive profiling tools -->
+        <profileable android:shell="true"/>
+    </application>
+</manifest>
diff --git a/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/SingleProcessDatastoreTest.kt b/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/SingleProcessDatastoreTest.kt
new file mode 100644
index 0000000..f74d565
--- /dev/null
+++ b/datastore/datastore-benchmark/src/androidTest/java/androidx/datastore/core/SingleProcessDatastoreTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.core
+
+import androidx.benchmark.junit4.BenchmarkRule
+import androidx.benchmark.junit4.measureRepeated
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.test.filters.MediumTest
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SingleProcessDatastoreTest {
+    @get:Rule
+    val benchmark = BenchmarkRule()
+
+    @get:Rule
+    val tmp = TemporaryFolder()
+    private lateinit var testScope: TestScope
+    private lateinit var dataStoreScope: TestScope
+
+    @Before
+    fun setUp() {
+        testScope = TestScope(UnconfinedTestDispatcher())
+        dataStoreScope = TestScope(UnconfinedTestDispatcher())
+    }
+
+    @Test
+    @LargeTest
+    fun create() = testScope.runTest {
+        benchmark.measureRepeated {
+            val testFile = tmp.newFile()
+            val store = DataStoreFactory.create(
+                serializer = TestingSerializer(),
+                scope = dataStoreScope
+            ) { testFile }
+            runWithTimingDisabled {
+                Assert.assertNotNull(store)
+            }
+        }
+    }
+
+    @Test
+    @MediumTest
+    fun read() = testScope.runTest {
+        val scope = this
+        val testFile = tmp.newFile()
+        val store = DataStoreFactory.create(
+            serializer = TestingSerializer(),
+            scope = dataStoreScope
+        ) { testFile }
+        store.updateData { 1 }
+        benchmark.measureRepeated {
+            runBlocking(scope.coroutineContext) {
+                val data = store.data.first()
+                runWithTimingDisabled {
+                    val exp: Byte = 1
+                    Assert.assertEquals(exp, data)
+                }
+            }
+        }
+    }
+
+    @Test
+    @MediumTest
+    fun update() = testScope.runTest {
+        val scope = this
+        val testFile = tmp.newFile()
+        val store = DataStoreFactory.create(
+            serializer = TestingSerializer(),
+            scope = dataStoreScope
+        ) { testFile }
+        benchmark.measureRepeated {
+            runBlocking(scope.coroutineContext) {
+                store.updateData { 1 }
+                val data = store.data.first()
+                runWithTimingDisabled {
+                    val exp: Byte = 1
+                    Assert.assertEquals(exp, data)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index d905952..59d4db0 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -804,6 +804,7 @@
 public abstract java\.util\.List<androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameEntity> jvmQuery\(\);
 public abstract androidx\.room\.integration\.kotlintestapp\.test\.JvmNameInDaoTest\.JvmNameDao jvmDao\(\);
 \^
+Note: \$SUPPORT/wear/tiles/tiles\-material/src/main/java/androidx/wear/tiles/material/TitleChip\.java has additional uses or overrides of a deprecated API\.
 \$SUPPORT/wear/tiles/tiles\-material/src/main/java/androidx/wear/tiles/material/layouts/PrimaryLayout\.java:[0-9]+: warning: \[deprecation\] CompactChip in androidx\.wear\.tiles\.material has been deprecated
 Note: \$SUPPORT/wear/tiles/tiles\-material/src/main/java/androidx/wear/tiles/material/Button\.java has additional uses or overrides of a deprecated API\.
 \$SUPPORT/wear/tiles/tiles\-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator\.java:[0-9]+: warning: \[deprecation\] ButtonDefaults in androidx\.wear\.tiles\.material has been deprecated
@@ -1011,4 +1012,19 @@
 \$SUPPORT/wear/tiles/tiles\-material/src/androidTest/java/androidx/wear/tiles/material/layouts/TestCasesGenerator\.java:[0-9]+: warning: \[deprecation\] Button in androidx\.wear\.tiles\.material has been deprecated
 # > Configure project :androidx-demos
 WARNING: The option setting 'android\.experimental\.disableCompileSdkChecks=true' is experimental\.
-The current default is 'false'\.
\ No newline at end of file
+The current default is 'false'\.
+# > Task :camera:camera-video:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/camera/camera\-video/src/androidTest/java/androidx/camera/video/internal/audio/AudioStreamImplTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+# > Task :benchmark:benchmark-macro:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/benchmark/benchmark\-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+# > Task :graphics:graphics-core:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/graphics/graphics\-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/graphics/graphics\-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+# > Task :compose:foundation:foundation:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/AbstractSelectionMagnifierTests\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandlePopupPositionTest\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
+w: file://\$SUPPORT/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/TextFieldVisualTransformationMagnifierTest\.kt:[0-9]+:[0-9]+ 'RequiresDevice' is deprecated\. Deprecated in Java
+# > Task :compose:ui:ui:compileDebugAndroidTestKotlin
+w: file://\$SUPPORT/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupTestUtils\.kt:[0-9]+:[0-9]+ 'getter for windowLayoutParams: EspressoOptional<WindowManager\.LayoutParams!>!' is deprecated\. Deprecated in Java
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 66b74fc0..6a2a82b 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -319,25 +319,25 @@
     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")
-    docs("androidx.test:core-ktx:1.5.0")
-    docs("androidx.test:monitor:1.6.1")
-    docs("androidx.test:rules:1.5.0")
-    docs("androidx.test:runner:1.5.2")
-    docs("androidx.test.espresso:espresso-accessibility:3.5.1")
-    docs("androidx.test.espresso:espresso-contrib:3.5.1")
-    docs("androidx.test.espresso:espresso-core:3.5.1")
-    docs("androidx.test.espresso:espresso-device:1.0.0-alpha03")
-    docs("androidx.test.espresso:espresso-idling-resource:3.5.1")
-    docs("androidx.test.espresso:espresso-intents:3.5.1")
-    docs("androidx.test.espresso:espresso-remote:3.5.1")
-    docs("androidx.test.espresso:espresso-web:3.5.1")
-    docs("androidx.test.espresso.idling:idling-concurrent:3.5.1")
-    docs("androidx.test.espresso.idling:idling-net:3.5.1")
-    docs("androidx.test.ext:junit:1.1.5")
-    docs("androidx.test.ext:junit-ktx:1.1.5")
-    docs("androidx.test.ext:truth:1.5.0")
-    docs("androidx.test.services:storage:1.4.2")
+    docs("androidx.test:core:1.6.0-alpha01")
+    docs("androidx.test:core-ktx:1.6.0-alpha01")
+    docs("androidx.test:monitor:1.7.0-alpha01")
+    docs("androidx.test:rules:1.6.0-alpha01")
+    docs("androidx.test:runner:1.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-accessibility:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-contrib:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-core:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-device:1.0.0-alpha04")
+    docs("androidx.test.espresso:espresso-idling-resource:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-intents:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-remote:3.6.0-alpha01")
+    docs("androidx.test.espresso:espresso-web:3.6.0-alpha01")
+    docs("androidx.test.espresso.idling:idling-concurrent:3.6.0-alpha01")
+    docs("androidx.test.espresso.idling:idling-net:3.6.0-alpha01")
+    docs("androidx.test.ext:junit:1.2.0-alpha01")
+    docs("androidx.test.ext:junit-ktx:1.2.0-alpha01")
+    docs("androidx.test.ext:truth:1.6.0-alpha01")
+    docs("androidx.test.services:storage:1.5.0-alpha01")
     docs("androidx.test.uiautomator:uiautomator:2.3.0-alpha02")
     docs("androidx.textclassifier:textclassifier:1.0.0-alpha04")
     docs("androidx.tracing:tracing:1.2.0-beta03")
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
index 763a9bb..fb7c81e 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/PostponedTransitionTest.kt
@@ -1049,6 +1049,43 @@
     }
 
     @Test
+    fun testTimedPostponeTwiceBeforeAttachedNoLeak() {
+        val beginningFragment = PostponedConstructorFragment(100000)
+        beginningFragment.postponeEnterTransition(1, TimeUnit.HOURS)
+        setupContainer(beginningFragment)
+
+        val fm = activityRule.activity.supportFragmentManager
+        val startBlue = activityRule.findBlue()
+
+        val fragment = TransitionFragment()
+        fm.beginTransaction()
+            .addSharedElement(startBlue, "blueSquare")
+            .replace(R.id.fragmentContainer, fragment)
+            .setReorderingAllowed(true)
+            .commit()
+
+        activityRule.waitForExecution()
+    }
+
+    @Test
+    fun testTimedPostponeBeforeAttachedAndAfterAttachedNoLeak() {
+        val beginningFragment = PostponedConstructorAndAttachFragment(100000)
+        setupContainer(beginningFragment)
+
+        val fm = activityRule.activity.supportFragmentManager
+        val startBlue = activityRule.findBlue()
+
+        val fragment = TransitionFragment()
+        fm.beginTransaction()
+            .addSharedElement(startBlue, "blueSquare")
+            .replace(R.id.fragmentContainer, fragment)
+            .setReorderingAllowed(true)
+            .commit()
+
+        activityRule.waitForExecution()
+    }
+
+    @Test
     fun testTimedPostponeStartOnTestThreadNoLeak() {
         val beginningFragment = PostponedFragment3(100000)
         setupContainer(beginningFragment)
@@ -1420,6 +1457,22 @@
         }
     }
 
+    class PostponedConstructorAndAttachFragment(private val duration: Long = 1000) :
+        TransitionFragment(R.layout.scene2) {
+
+        init {
+            postponeEnterTransition(duration, TimeUnit.MILLISECONDS)
+        }
+
+        override fun onCreateView(
+            inflater: LayoutInflater,
+            container: ViewGroup?,
+            savedInstanceState: Bundle?
+        ) = super.onCreateView(inflater, container, savedInstanceState).also {
+            postponeEnterTransition(duration, TimeUnit.MILLISECONDS)
+        }
+    }
+
     class CommitNowFragment : PostponedFragment1() {
         override fun onResume() {
             super.onResume()
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 06729a3..53224f7 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -2855,6 +2855,9 @@
      */
     public final void postponeEnterTransition(long duration, @NonNull TimeUnit timeUnit) {
         ensureAnimationInfo().mEnterTransitionPostponed = true;
+        if (mPostponedHandler != null) {
+            mPostponedHandler.removeCallbacks(mPostponedDurationRunnable);
+        }
         if (mFragmentManager != null) {
             mPostponedHandler = mFragmentManager.getHost().getHandler();
         } else {
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
index de50aac..2430d3f 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
@@ -38,6 +38,7 @@
 import androidx.work.WorkManager
 import androidx.work.testing.WorkManagerTestInitHelper
 import com.google.common.truth.Truth.assertThat
+import java.lang.ref.WeakReference
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertIs
@@ -82,15 +83,16 @@
 
     private val mInnerRules = RuleChain.outerRule(mActivityRule).around(mOrientationRule)
 
+    private lateinit var mMaybeHostView: WeakReference<TestAppWidgetHostView?>
+
     private var mHostStarted = false
-    private var mMaybeHostView: TestAppWidgetHostView? = null
     private var mAppWidgetId = 0
     private val mScenario: ActivityScenario<AppWidgetHostTestActivity>
         get() = mActivityRule.scenario
     private val mContext = ApplicationProvider.getApplicationContext<Context>()
 
     val mHostView: TestAppWidgetHostView
-        get() = checkNotNull(mMaybeHostView) { "No app widget installed on the host" }
+        get() = checkNotNull(mMaybeHostView.get()) { "No app widget installed on the host" }
 
     val appWidgetId: Int get() = mAppWidgetId
 
@@ -118,14 +120,12 @@
         mHostStarted = true
 
         mActivityRule.scenario.onActivity { activity ->
-            mMaybeHostView = activity.bindAppWidget(mPortraitSize, mLandscapeSize)
+            mMaybeHostView = WeakReference(activity.bindAppWidget(mPortraitSize, mLandscapeSize))
         }
 
-        val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
-
         runAndWaitForChildren {
-            mAppWidgetId = hostView.appWidgetId
-            hostView.waitForRemoteViews()
+            mAppWidgetId = mHostView.appWidgetId
+            mHostView.waitForRemoteViews()
         }
     }
 
@@ -136,8 +136,7 @@
      * This should not be called from the main thread, i.e. in [onHostView] or [onHostActivity].
      */
     suspend fun runAndWaitForUpdate(block: suspend () -> Unit) {
-        val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
-        hostView.resetRemoteViewsLatch()
+        mHostView.resetRemoteViewsLatch()
         withContext(Dispatchers.Main) { block() }
 
         // b/267494219 these tests are currently flaking due to possible changes to the views after
@@ -147,7 +146,7 @@
 
         // Do not wait on the main thread so that the UI handlers can run.
         runAndWaitForChildren {
-            hostView.waitForRemoteViews()
+            mHostView.waitForRemoteViews()
         }
     }
 
@@ -166,8 +165,7 @@
 
     fun removeAppWidget() {
         mActivityRule.scenario.onActivity { activity ->
-            val hostView = checkNotNull(mMaybeHostView) { "No app widget to remove" }
-            activity.deleteAppWidget(hostView)
+            activity.deleteAppWidget(mHostView)
         }
     }
 
@@ -275,7 +273,7 @@
         mPortraitSize = portrait
         if (!mHostStarted) return
 
-        val hostView = mMaybeHostView
+        val hostView = mMaybeHostView.get()
         if (hostView != null) {
             mScenario.onActivity {
                 hostView.setSizes(portrait, landscape)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c9987b2..a69e504 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -14,12 +14,12 @@
 androidGradlePluginMin = "7.0.4"
 androidLintMin = "30.0.4"
 androidLintMinCompose = "30.0.0"
-androidxTestRunner = "1.5.2"
-androidxTestRules = "1.5.0"
-androidxTestMonitor = "1.6.1"
-androidxTestCore = "1.5.0"
-androidxTestExtJunit = "1.1.5"
-androidxTestExtTruth = "1.5.0"
+androidxTestRunner = "1.6.0-alpha01"
+androidxTestRules = "1.6.0-alpha01"
+androidxTestMonitor = "1.7.0-alpha01"
+androidxTestCore = "1.6.0-alpha01"
+androidxTestExtJunit = "1.2.0-alpha01"
+androidxTestExtTruth = "1.6.0-alpha01"
 atomicFu = "0.17.0"
 autoService = "1.0-rc6"
 autoValue = "1.6.3"
@@ -29,8 +29,8 @@
 dagger = "2.44"
 dexmaker = "2.28.3"
 dokka = "1.8.10-dev-203"
-espresso = "3.5.1"
-espressoDevice = "1.0.0-alpha03"
+espresso = "3.6.0-alpha01"
+espressoDevice = "1.0.0-alpha04"
 grpc = "1.52.0"
 guavaJre = "31.1-jre"
 hilt = "2.44"
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 5aa9291..0150fc4 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -476,6 +476,14 @@
             <sha256 value="d46777ad3ea8bca73491b2e02fc85b3664486abf5314cc4dc6740908bd855330" origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="com.google.android.apps.common.testing.accessibility.framework" name="accessibility-test-framework" version="3.1">
+         <artifact name="accessibility-test-framework-3.1.aar">
+            <sha256 value="e641e2a2c7287afd41b85310dd8f1344a8668034bbbfc4b02f58a48fd9c05ec7" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+         <artifact name="accessibility-test-framework-3.1.pom">
+            <sha256 value="80567228cdbd44d61e5320cd090883de7232dbc1ed7ebf5ab5c9810c11cd67e0" origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="com.google.android.apps.common.testing.accessibility.framework" name="accessibility-test-framework" version="3.1.2">
          <artifact name="accessibility-test-framework-3.1.2.aar">
             <sha256 value="9b586dc8eeeb4f601038e23ef8ffd6a1deeca1163276d02797b0d2b8f9764b62" origin="Generated by Gradle" reason="Artifact is not signed"/>
diff --git a/graphics/graphics-shapes/api/current.txt b/graphics/graphics-shapes/api/current.txt
index 9455b94..8c1144b 100644
--- a/graphics/graphics-shapes/api/current.txt
+++ b/graphics/graphics-shapes/api/current.txt
@@ -81,8 +81,6 @@
   }
 
   public final class RoundedPolygon {
-    ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional android.graphics.PointF? center);
-    ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center);
     ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF? center);
     ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
     ctor public RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
@@ -104,12 +102,13 @@
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius, optional android.graphics.PointF center);
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius);
     method public static androidx.graphics.shapes.RoundedPolygon Circle();
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius);
   }
 
 }
diff --git a/graphics/graphics-shapes/api/public_plus_experimental_current.txt b/graphics/graphics-shapes/api/public_plus_experimental_current.txt
index 9455b94..8c1144b 100644
--- a/graphics/graphics-shapes/api/public_plus_experimental_current.txt
+++ b/graphics/graphics-shapes/api/public_plus_experimental_current.txt
@@ -81,8 +81,6 @@
   }
 
   public final class RoundedPolygon {
-    ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional android.graphics.PointF? center);
-    ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center);
     ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF? center);
     ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
     ctor public RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
@@ -104,12 +102,13 @@
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius, optional android.graphics.PointF center);
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius);
     method public static androidx.graphics.shapes.RoundedPolygon Circle();
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius);
   }
 
 }
diff --git a/graphics/graphics-shapes/api/restricted_current.txt b/graphics/graphics-shapes/api/restricted_current.txt
index 9455b94..8c1144b 100644
--- a/graphics/graphics-shapes/api/restricted_current.txt
+++ b/graphics/graphics-shapes/api/restricted_current.txt
@@ -81,8 +81,6 @@
   }
 
   public final class RoundedPolygon {
-    ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional android.graphics.PointF? center);
-    ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center);
     ctor public RoundedPolygon(java.util.List<? extends android.graphics.PointF> vertices, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF? center);
     ctor public RoundedPolygon(int numVertices, optional float radius, optional android.graphics.PointF center, optional androidx.graphics.shapes.CornerRounding rounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
     ctor public RoundedPolygon(androidx.graphics.shapes.RoundedPolygon source);
@@ -104,12 +102,13 @@
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius, optional android.graphics.PointF center);
     method public static androidx.graphics.shapes.RoundedPolygon Circle(optional float radius);
     method public static androidx.graphics.shapes.RoundedPolygon Circle();
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius, optional androidx.graphics.shapes.CornerRounding rounding);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio, optional float radius);
-    method public static androidx.graphics.shapes.RoundedPolygon Star(int numOuterVertices, @FloatRange(from=0.0, to=1.0, fromInclusive=false, toInclusive=false) float innerRadiusRatio);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding, optional android.graphics.PointF center);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding, optional java.util.List<androidx.graphics.shapes.CornerRounding>? perVertexRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding, optional androidx.graphics.shapes.CornerRounding? innerRounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius, optional androidx.graphics.shapes.CornerRounding rounding);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius, optional float innerRadius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius, optional float radius);
+    method public static androidx.graphics.shapes.RoundedPolygon Star(int numVerticesPerRadius);
   }
 
 }
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
index c715b1e..610fa3e 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/PolygonTest.kt
@@ -65,8 +65,8 @@
         assertInBounds(manualSquare.toCubicShape(), min, max)
 
         val offset = PointF(1f, 2f)
-        val manualSquareOffset =
-            RoundedPolygon(listOf(p0 + offset, p1 + offset, p2 + offset, p3 + offset), offset)
+        val manualSquareOffset = RoundedPolygon(
+            vertices = listOf(p0 + offset, p1 + offset, p2 + offset, p3 + offset), center = offset)
         min = PointF(0f, 1f)
         max = PointF(2f, 3f)
         assertInBounds(manualSquareOffset.toCubicShape(), min, max)
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedRoundedPolygonTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt
similarity index 98%
rename from graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedRoundedPolygonTest.kt
rename to graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt
index 46f71be..11925c6 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedRoundedPolygonTest.kt
+++ b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/RoundedPolygonTest.kt
@@ -23,7 +23,7 @@
 import org.junit.Test
 
 @SmallTest
-class RoundedRoundedPolygonTest {
+class RoundedPolygonTest {
 
     val rounding = CornerRounding(.1f)
     val perVtxRounded = listOf<CornerRounding>(rounding, rounding, rounding, rounding)
diff --git a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt
index 05f1f8e9..83755ae 100644
--- a/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt
+++ b/graphics/graphics-shapes/src/androidTest/java/androidx/graphics/shapes/ShapesTest.kt
@@ -108,24 +108,27 @@
      */
     @Test
     fun starTest() {
-        var star = Star(4, innerRadiusRatio = .5f)
+        var star = Star(4, innerRadius = .5f)
         var shape = star.toCubicShape()
+        var radius = 1f
+        var innerRadius = .5f
         for (cubic in shape.cubics) {
-            assertCubicOnRadii(cubic, 1f, .5f)
+            assertCubicOnRadii(cubic, radius, innerRadius)
         }
 
         val center = PointF(1f, 2f)
-        star = Star(4, innerRadiusRatio = .5f, center = center)
+        star = Star(4, innerRadius = innerRadius, center = center)
         shape = star.toCubicShape()
         for (cubic in shape.cubics) {
-            assertCubicOnRadii(cubic, 1f, .5f, center)
+            assertCubicOnRadii(cubic, radius, innerRadius, center)
         }
 
-        val radius = 4f
-        star = Star(4, radius = radius, innerRadiusRatio = .5f)
+        radius = 4f
+        innerRadius = 2f
+        star = Star(4, radius, innerRadius)
         shape = star.toCubicShape()
         for (cubic in shape.cubics) {
-            assertCubicOnRadii(cubic, radius, .5f * radius)
+            assertCubicOnRadii(cubic, radius, innerRadius)
         }
     }
 
@@ -136,24 +139,28 @@
         val perVtxRounded = listOf<CornerRounding>(rounding, innerRounding, rounding, innerRounding,
             rounding, innerRounding, rounding, innerRounding)
 
-        var star = Star(4, innerRadiusRatio = .5f, rounding = rounding)
+        var star = Star(4, innerRadius = .5f, rounding = rounding)
         val min = PointF(-1f, -1f)
         val max = PointF(1f, 1f)
         assertInBounds(star.toCubicShape(), min, max)
 
-        star = Star(4, innerRadiusRatio = .5f, innerRounding = innerRounding)
+        star = Star(4, innerRadius = .5f, innerRounding = innerRounding)
         assertInBounds(star.toCubicShape(), min, max)
 
-        star = Star(4, innerRadiusRatio = .5f, rounding = rounding,
-            innerRounding = innerRounding)
+        star = Star(
+            4, innerRadius = .5f, rounding = rounding,
+            innerRounding = innerRounding
+        )
         assertInBounds(star.toCubicShape(), min, max)
 
-        star = Star(4, innerRadiusRatio = .5f, perVertexRounding = perVtxRounded)
+        star = Star(4, innerRadius = .5f, perVertexRounding = perVtxRounded)
         assertInBounds(star.toCubicShape(), min, max)
 
         assertThrows(IllegalArgumentException::class.java) {
-            star = Star(6, innerRadiusRatio = .5f,
-                perVertexRounding = perVtxRounded)
+            star = Star(
+                6, innerRadius = .5f,
+                perVertexRounding = perVtxRounded
+            )
         }
     }
 }
\ No newline at end of file
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt
index 60061ce..69a19c6 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt
+++ b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/RoundedPolygon.kt
@@ -78,34 +78,6 @@
      * Constructs a RoundedPolygon object from a given list of vertices, with optional
      * corner-rounding parameters for all corners or per-corner.
      *
-     * @param vertices The list of vertices in this polygon. This should be an ordered list
-     * (with the outline of the shape going from each vertex to the next in order of this
-     * list), otherwise the results will be undefined.
-     * @param center An optionally declared center of the polygon. If null or not supplied, this
-     * will be calculated based on the supplied vertices.
-     */
-    constructor(vertices: List<PointF>, center: PointF? = null) :
-        this(vertices, rounding = CornerRounding.Unrounded, perVertexRounding = null, center)
-
-    /**
-     * This constructor takes the number of vertices in the resulting polygon. These vertices are
-     * positioned on a virtual circle around a given center with each vertex positioned [radius]
-     * distance from that center, equally spaced (with equal angles between them).
-     *
-     * @param numVertices The number of vertices in this polygon.
-     * @param radius The radius of the polygon, in pixels. This radius determines the
-     * initial size of the object, which can be resized later by setting
-     * a [transform matrix][transform].
-     * @param center The center of the polygon, around which all vertices will be placed. The
-     * default center is at (0,0).
-     */
-    constructor(numVertices: Int, radius: Float = 1f, center: PointF = PointF(0f, 0f)) :
-        this(numVertices, radius = radius, center = center, rounding = CornerRounding.Unrounded)
-
-    /**
-     * Constructs a RoundedPolygon object from a given list of vertices, with optional
-     * corner-rounding parameters for all corners or per-corner.
-     *
      * A RoundedPolygon without any rounding parameters is equivalent to a [RoundedPolygon] constructed
      * with the same [vertices] and [center].
      *
diff --git a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt
index 9f8a5ec..4e8898d 100644
--- a/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt
+++ b/graphics/graphics-shapes/src/main/java/androidx/graphics/shapes/Shapes.kt
@@ -17,7 +17,6 @@
 package androidx.graphics.shapes
 
 import android.graphics.PointF
-import androidx.annotation.FloatRange
 
 private const val Sqrt2 = 1.41421356f
 
@@ -36,42 +35,63 @@
 
 /**
  * Creates a star polygon, which is like a regular polygon except every other vertex is
- * on either an inner or outer radius. The two radii are specified in the constructor by
- * providing the [innerRadiusRatio], which is a value representing the proportion (from
- * 0 to 1, non-inclusive) of the inner radius compared to the outer one.
- * @throws IllegalArgumentException if radius ratio not in [0,1]
+ * on either an inner or outer radius. The two radii specified in the constructor must both
+ * both nonzero. If the radii are equal, the result will be a regular (not star) polygon with twice
+ * the number of vertices specified in [numVerticesPerRadius].
+ *
+ * @param numVerticesPerRadius The number of vertices along each of the two radii.
+ * @param radius Outer radius for this star shape, must be greater than 0. Default value is 1.
+ * @param innerRadius Inner radius for this star shape, must be greater than 0 and less
+ * than or equal to [radius]. Note that equal radii would be the same as creating a
+ * [RoundedPolygon] directly, but with 2 * [numVerticesPerRadius] vertices. Default value is .5.
+ * @param rounding The [CornerRounding] properties of every vertex. If some vertices should
+ * have different rounding properties, then use [perVertexRounding] instead. The default
+ * rounding value is [CornerRounding.Unrounded], meaning that the polygon will use the vertices
+ * themselves in the final shape and not curves rounded around the vertices.
+ * @param innerRounding Optional rounding parameters for the vertices on the [innerRadius]. If
+ * null (the default value), inner vertices will use the [rounding] or [perVertexRounding]
+ * parameters instead.
+ * @param perVertexRounding The [CornerRounding] properties of every vertex. If this
+ * parameter is not null, then it must have the same size as 2 * [numVerticesPerRadius]. If this
+ * parameter is null, then the polygon will use the [rounding] parameter for every vertex instead.
+ * The default value is null.
+ * @param center The center of the polygon, around which all vertices will be placed. The
+ * default center is at (0,0).
+ *
+ * @throws IllegalArgumentException if either [radius] or [innerRadius] are <= 0 or
+ * [innerRadius] > [radius].
  */
 @JvmOverloads
 fun Star(
-    numOuterVertices: Int,
-    @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
-    innerRadiusRatio: Float,
+    numVerticesPerRadius: Int,
     radius: Float = 1f,
+    innerRadius: Float = .5f,
     rounding: CornerRounding = CornerRounding.Unrounded,
     innerRounding: CornerRounding? = null,
     perVertexRounding: List<CornerRounding>? = null,
     center: PointF = Zero
 ): RoundedPolygon {
-    if (innerRadiusRatio <= 0f || innerRadiusRatio >= 1f) {
-        throw IllegalArgumentException("Inner radius ratio must be in range (0,1), exclusive" +
-            "of 0 and 1")
+    if (radius <= 0f || innerRadius <= 0f) {
+        throw IllegalArgumentException("Star radii must both be greater than 0")
+    }
+    if (innerRadius >= radius) {
+        throw IllegalArgumentException("innerRadius must be less than radius")
     }
 
     var pvRounding = perVertexRounding
     // If no per-vertex rounding supplied and caller asked for inner rounding,
     // create per-vertex rounding list based on supplied outer/inner rounding parameters
     if (pvRounding == null && innerRounding != null) {
-        pvRounding = (0 until numOuterVertices).flatMap {
+        pvRounding = (0 until numVerticesPerRadius).flatMap {
             listOf(rounding, innerRounding)
         }
     }
     // Star polygon is just a polygon with all vertices supplied (where we generate
     // those vertices to be on the inner/outer radii)
-    return RoundedPolygon((0 until numOuterVertices).flatMap {
+    return RoundedPolygon((0 until numVerticesPerRadius).flatMap {
         listOf(
-            radialToCartesian(radius, (FloatPi / numOuterVertices * 2 * it), center),
-            radialToCartesian(radius * innerRadiusRatio,
-                FloatPi / numOuterVertices * (2 * it + 1), center)
+            radialToCartesian(radius, (FloatPi / numVerticesPerRadius * 2 * it), center),
+            radialToCartesian(innerRadius, FloatPi / numVerticesPerRadius * (2 * it + 1), center)
         )
     }, rounding, pvRounding, center)
 }
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
index 26166e8..91a8db6 100644
--- a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/MainActivity.kt
@@ -182,28 +182,28 @@
             //
             ShapeParameters(
                 sides = 12,
-                innerRadiusRatio = .928f,
+                innerRadius = .928f,
                 roundness = .1f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
             // Clover
             ShapeParameters(
                 sides = 4,
-                innerRadiusRatio = .352f,
+                innerRadius = .352f,
                 roundness = .32f,
                 rotation = 45f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
             // Alice
             ShapeParameters(
-                innerRadiusRatio = 0.1f,
+                innerRadius = 0.1f,
                 roundness = 0.22f,
                 shapeId = ShapeParameters.ShapeId.Triangle
             ),
             // Wiggle Star
             ShapeParameters(
                 sides = 8,
-                innerRadiusRatio = .784f,
+                innerRadius = .784f,
                 roundness = .16f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
@@ -212,20 +212,20 @@
             // Wovel
             ShapeParameters(
                 sides = 15,
-                innerRadiusRatio = .892f,
+                innerRadius = .892f,
                 roundness = 1f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
             // BlobR
             ShapeParameters(
-                innerRadiusRatio = .19f,
+                innerRadius = .19f,
                 roundness = 0.86f,
                 rotation = -45f,
                 shapeId = ShapeParameters.ShapeId.Blob
             ),
             // BlobL
             ShapeParameters(
-                innerRadiusRatio = .19f,
+                innerRadius = .19f,
                 roundness = 0.86f,
                 rotation = 45f,
                 shapeId = ShapeParameters.ShapeId.Blob
@@ -233,7 +233,7 @@
             // Scalop
             ShapeParameters(
                 sides = 12,
-                innerRadiusRatio = .928f,
+                innerRadius = .928f,
                 roundness = .928f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
@@ -268,14 +268,14 @@
             ShapeParameters(
                 sides = 5,
                 rotation = -360f / 20,
-                innerRadiusRatio = .3f,
+                innerRadius = .3f,
                 shapeId = ShapeParameters.ShapeId.Star
             ),
 
             // 8-Sided Star
             ShapeParameters(
                 sides = 8,
-                innerRadiusRatio = .6f,
+                innerRadius = .6f,
                 shapeId = ShapeParameters.ShapeId.Star
             )
         )
diff --git a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
index aec2f4b..c7f98bb 100644
--- a/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
+++ b/graphics/integration-tests/testapp-compose/src/main/java/androidx/graphics/shapes/testcompose/ShapeEditor.kt
@@ -75,7 +75,7 @@
 
 class ShapeParameters(
     sides: Int = 5,
-    innerRadiusRatio: Float = 0.5f,
+    innerRadius: Float = 0.5f,
     roundness: Float = 0f,
     smooth: Float = 0f,
     innerRoundness: Float = roundness,
@@ -84,7 +84,7 @@
     shapeId: ShapeId = ShapeId.Polygon
 ) {
     internal val sides = mutableStateOf(sides.toFloat())
-    internal val innerRadiusRatio = mutableStateOf(innerRadiusRatio)
+    internal val innerRadius = mutableStateOf(innerRadius)
     internal val roundness = mutableStateOf(roundness)
     internal val smooth = mutableStateOf(smooth)
     internal val innerRoundness = mutableStateOf(innerRoundness)
@@ -95,7 +95,7 @@
 
     fun copy() = ShapeParameters(
         this.sides.value.roundToInt(),
-        this.innerRadiusRatio.value,
+        this.innerRadius.value,
         this.roundness.value,
         this.smooth.value,
         this.innerRoundness.value,
@@ -124,8 +124,8 @@
     internal val shapes = listOf(
         ShapeItem("Star", shapegen = {
                 Star(
-                    numOuterVertices = this.sides.value.roundToInt(),
-                    innerRadiusRatio = this.innerRadiusRatio.value,
+                    numVerticesPerRadius = this.sides.value.roundToInt(),
+                    innerRadius = this.innerRadius.value,
                     rounding = CornerRounding(this.roundness.value, this.smooth.value),
                     innerRounding = CornerRounding(
                         this.innerRoundness.value,
@@ -136,7 +136,7 @@
             debugDump = {
                 debugLog(
                     "ShapeParameters(sides = ${this.sides.value.roundToInt()}, " +
-                        "innerRadiusRatio = ${this.innerRadiusRatio.value}f, " +
+                        "innerRadius = ${this.innerRadius.value}f, " +
                         "roundness = ${this.roundness.value}f, " +
                         "smooth = ${this.smooth.value}f, " +
                         "innerRoundness = ${this.innerRoundness.value}f, " +
@@ -167,7 +167,7 @@
                 val points = listOf(
                     radialToCartesian(1f, 270f.toRadians()),
                     radialToCartesian(1f, 30f.toRadians()),
-                    radialToCartesian(this.innerRadiusRatio.value, 90f.toRadians()),
+                    radialToCartesian(this.innerRadius.value, 90f.toRadians()),
                     radialToCartesian(1f, 150f.toRadians()),
                 )
                 RoundedPolygon(
@@ -178,7 +178,7 @@
             },
             debugDump = {
                 debugLog(
-                    "ShapeParameters(innerRadiusRatio = ${this.innerRadiusRatio.value}f, " +
+                    "ShapeParameters(innerRadius = ${this.innerRadius.value}f, " +
                         "smooth = ${this.smooth.value}f, " +
                         rotationAsString() +
                         "shapeId = ShapeParameters.ShapeId.Triangle)"
@@ -188,7 +188,7 @@
         ),
         ShapeItem(
             "Blob", shapegen = {
-                val sx = this.innerRadiusRatio.value.coerceAtLeast(0.1f)
+                val sx = this.innerRadius.value.coerceAtLeast(0.1f)
                 val sy = this.roundness.value.coerceAtLeast(0.1f)
                 RoundedPolygon(
                     listOf(
@@ -283,11 +283,11 @@
         }
         MySlider("Sides", 3f, 20f, 1f, params.sides, shapeParams.usesSides)
         MySlider(
-            "InnerRadiusRatio",
+            "InnerRadius",
             0.1f,
             0.999f,
             0f,
-            params.innerRadiusRatio,
+            params.innerRadius,
             shapeParams.usesInnerRatio
         )
         MySlider("RoundRadius", 0f, 1f, 0f, params.roundness, shapeParams.usesRoundness)
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MaterialShapes.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MaterialShapes.kt
index 0a78604..e3b0444 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MaterialShapes.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/MaterialShapes.kt
@@ -104,18 +104,18 @@
 
         @JvmStatic
         fun scallop(): RoundedPolygon {
-            return Star(12, .928f, rounding = CornerRounding(radius = .928f))
+            return Star(12, innerRadius = .928f, rounding = CornerRounding(radius = .928f))
         }
 
         @JvmOverloads
         @JvmStatic
         fun clover(
             rounding: Float = .32f,
-            innerRadiusRatio: Float = .352f,
+            innerRadius: Float = .352f,
             innerRounding: CornerRounding? = null,
             scale: Float = 1f
         ): RoundedPolygon {
-            val poly = Star(4, innerRadiusRatio,
+            val poly = Star(4, innerRadius = innerRadius,
                 rounding = CornerRounding(rounding * scale),
                 innerRounding = innerRounding)
             return poly
diff --git a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
index d24d838..5571e62 100644
--- a/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
+++ b/graphics/integration-tests/testapp/src/main/java/androidx/graphics/shapes/test/ShapeActivity.kt
@@ -115,11 +115,11 @@
         val rounding = CornerRounding(.1f, .5f)
         val starRounding = CornerRounding(.05f, .25f)
         shapes.add(RoundedPolygon(numVertices = 4, rounding = rounding))
-        shapes.add(Star(8, .4f, rounding = starRounding))
-        shapes.add(Star(8, innerRadiusRatio = .4f, rounding = starRounding,
+        shapes.add(Star(8, radius = 1f, innerRadius = .4f, rounding = starRounding))
+        shapes.add(Star(8, radius = 1f, innerRadius = .4f, rounding = starRounding,
             innerRounding = CornerRounding.Unrounded))
         shapes.add(
-            MaterialShapes.clover(rounding = .352f, innerRadiusRatio = .1f,
+            MaterialShapes.clover(rounding = .352f, innerRadius = .1f,
             innerRounding = Unrounded))
         shapes.add(RoundedPolygon(3))
     }
diff --git a/media2/media2-widget/src/main/res/values-or/strings.xml b/media2/media2-widget/src/main/res/values-or/strings.xml
index 79b2be3..d527d032 100644
--- a/media2/media2-widget/src/main/res/values-or/strings.xml
+++ b/media2/media2-widget/src/main/res/values-or/strings.xml
@@ -30,7 +30,7 @@
     <string name="mcv2_music_title_unknown_text" msgid="6037645626002038645">"ଗୀତର ଟାଇଟେଲ୍ ଅଜଣା ଅଟେ"</string>
     <string name="mcv2_music_artist_unknown_text" msgid="5393558204040775454">"କଳାକାର ଅଜଣା ଅଟନ୍ତି"</string>
     <string name="mcv2_playback_error_text" msgid="6061787693725630293">"ଆପଣ ଅନୁରୋଧ କରିଥିବା ଆଇଟମ୍ ଚଲାଯାଇପାରିଲା ନାହିଁ"</string>
-    <string name="mcv2_error_dialog_button" msgid="5940167897992933850">"ଠିକ୍ ଅଛି"</string>
+    <string name="mcv2_error_dialog_button" msgid="5940167897992933850">"ଠିକ ଅଛି"</string>
     <string name="mcv2_back_button_desc" msgid="1540894858499118373">"ପଛକୁ ଫେରନ୍ତୁ"</string>
     <string name="mcv2_overflow_left_button_desc" msgid="2749567167276435888">"ପୂର୍ବବର୍ତ୍ତୀ ବଟନ୍ ତାଲିକାକୁ ଫେରନ୍ତୁ"</string>
     <string name="mcv2_overflow_right_button_desc" msgid="7388732945289831383">"ଅଧିକ ବଟନ୍ ଦେଖନ୍ତୁ"</string>
diff --git a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
index aede27e..50470e1 100644
--- a/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
+++ b/paging/paging-compose/src/androidTest/java/androidx/paging/compose/LazyPagingItemsTest.kt
@@ -74,8 +74,9 @@
             initialLoadSize = 3,
             prefetchDistance = 1,
         ),
+        loadDelay: Long = 0,
         pagingSourceFactory: () -> PagingSource<Int, Int> = {
-            TestPagingSource(items = items, loadDelay = 0)
+            TestPagingSource(items = items, loadDelay = loadDelay)
         }
     ): Pager<Int, Int> {
         return Pager(config = config, pagingSourceFactory = pagingSourceFactory)
@@ -120,10 +121,10 @@
         assertThat(loadStates.first()).isEqualTo(expected)
     }
 
-    @Ignore // b/267374463
     @Test
     fun lazyPagingLoadStateAfterRefresh() {
-        val pager = createPager()
+        val pager = createPager(loadDelay = 100)
+
         val loadStates: MutableList<CombinedLoadStates> = mutableListOf()
 
         lateinit var lazyPagingItems: LazyPagingItems<Int>
@@ -132,10 +133,23 @@
             loadStates.add(lazyPagingItems.loadState)
         }
 
-        // we only want loadStates after manual refresh
-        loadStates.clear()
-        lazyPagingItems.refresh()
-        rule.waitForIdle()
+        // wait for both compose and paging to complete
+        rule.waitUntil {
+            rule.waitForIdle()
+            lazyPagingItems.loadState.refresh == LoadState.NotLoading(false)
+        }
+
+        rule.runOnIdle {
+            // we only want loadStates after manual refresh
+            loadStates.clear()
+            lazyPagingItems.refresh()
+        }
+
+        // wait for both compose and paging to complete
+        rule.waitUntil {
+            rule.waitForIdle()
+            lazyPagingItems.loadState.refresh == LoadState.NotLoading(false)
+        }
 
         assertThat(loadStates).isNotEmpty()
         val expected = CombinedLoadStates(
diff --git a/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt b/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
index e8db299..eaf2d0a 100644
--- a/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
+++ b/privacysandbox/plugins/plugins-privacysandbox-library/src/main/java/androidx/privacysandboxlibraryplugin/PrivacySandboxLibraryPlugin.kt
@@ -77,7 +77,7 @@
 
             // Add additional dependencies required for KSP outputs
 
-            val toolsVersion = "1.0.0-alpha02"
+            val toolsVersion = "1.0.0-alpha03"
             project.dependencies {
                 add(
                     "ksp",
diff --git a/privacysandbox/tools/tools-apicompiler/build.gradle b/privacysandbox/tools/tools-apicompiler/build.gradle
index c17bdce..6abb2a1 100644
--- a/privacysandbox/tools/tools-apicompiler/build.gradle
+++ b/privacysandbox/tools/tools-apicompiler/build.gradle
@@ -45,20 +45,24 @@
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include: "android.jar"
     ))
-    // Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
-    def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
-    def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
-    test {
-        inputs.files(aidlCompilerPath)
-                .withPropertyName("aidl_compiler_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        inputs.files(frameworkAidlPath)
-                .withPropertyName("framework_aidl_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        doFirst {
-            systemProperty "aidl_compiler_path", aidlCompilerPath
-            systemProperty "framework_aidl_path", frameworkAidlPath
-        }
+}
+
+// Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
+def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
+def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
+def testGeneratedSourcesPath = "${project.buildDir}/testGeneratedSources"
+test {
+    inputs.files(aidlCompilerPath)
+            .withPropertyName("aidl_compiler_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.files(frameworkAidlPath)
+            .withPropertyName("framework_aidl_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.dir("src/test/test-data").withPathSensitivity(PathSensitivity.RELATIVE)
+    doFirst {
+        systemProperty "aidl_compiler_path", aidlCompilerPath
+        systemProperty "framework_aidl_path", frameworkAidlPath
+        systemProperty "test_output_dir", testGeneratedSourcesPath
     }
 }
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/AbstractApiCompilerDiffTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/AbstractApiCompilerDiffTest.kt
new file mode 100644
index 0000000..cd0f1b0
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/AbstractApiCompilerDiffTest.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.privacysandbox.tools.apicompiler
+
+import androidx.privacysandbox.tools.testing.AbstractDiffTest
+import androidx.privacysandbox.tools.testing.CompilationTestHelper
+import androidx.room.compiler.processing.util.Source
+import java.nio.file.Path
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createFile
+import kotlin.io.path.writeText
+
+/** Base test class for API Compiler diff testing. */
+abstract class AbstractApiCompilerDiffTest : AbstractDiffTest() {
+
+    open val extraProcessorOptions: Map<String, String> = mapOf()
+
+    override fun generateSources(
+        inputSources: List<Source>,
+        outputDirectory: Path
+    ): List<Source> {
+        val result = compileWithPrivacySandboxKspCompiler(inputSources, extraProcessorOptions)
+        CompilationTestHelper.assertThat(result).succeeds()
+        val sources = result.generatedSources
+
+        // Writing generated sources to expected output directory.
+        sources.forEach { source ->
+            outputDirectory.resolve(source.relativePath).apply {
+                parent?.createDirectories()
+                createFile()
+                writeText(source.contents)
+            }
+        }
+        return sources
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkApiCompilerDiffTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkApiCompilerDiffTest.kt
new file mode 100644
index 0000000..8f53ceb
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkApiCompilerDiffTest.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.privacysandbox.tools.apicompiler
+
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/** Test the Privacy Sandbox API Compiler with an SDK that uses all available features. */
+@RunWith(JUnit4::class)
+class FullFeaturedSdkApiCompilerDiffTest : AbstractApiCompilerDiffTest() {
+    override val subdirectoryName = "fullfeaturedsdk"
+    override val relativePathsToExpectedAidlClasses = listOf(
+        "com/mysdk/ICancellationSignal.java",
+        "com/mysdk/IMyCallback.java",
+        "com/mysdk/IMyInterface.java",
+        "com/mysdk/IMyInterfaceTransactionCallback.java",
+        "com/mysdk/IMySdk.java",
+        "com/mysdk/IMySecondInterface.java",
+        "com/mysdk/IMyUiInterface.java",
+        "com/mysdk/IMyUiInterfaceCoreLibInfoAndBinderWrapper.java",
+        "com/mysdk/IMyUiInterfaceTransactionCallback.java",
+        "com/mysdk/IMySecondInterfaceTransactionCallback.java",
+        "com/mysdk/IResponseTransactionCallback.java",
+        "com/mysdk/IStringTransactionCallback.java",
+        "com/mysdk/IUnitTransactionCallback.java",
+        "com/mysdk/IListResponseTransactionCallback.java",
+        "com/mysdk/IListIntTransactionCallback.java",
+        "com/mysdk/IListLongTransactionCallback.java",
+        "com/mysdk/IListDoubleTransactionCallback.java",
+        "com/mysdk/IListStringTransactionCallback.java",
+        "com/mysdk/IListBooleanTransactionCallback.java",
+        "com/mysdk/IListFloatTransactionCallback.java",
+        "com/mysdk/IListCharTransactionCallback.java",
+        "com/mysdk/IListShortTransactionCallback.java",
+        "com/mysdk/ParcelableRequest.java",
+        "com/mysdk/ParcelableResponse.java",
+        "com/mysdk/ParcelableStackFrame.java",
+        "com/mysdk/ParcelableInnerValue.java",
+        "com/mysdk/PrivacySandboxThrowableParcel.java",
+    )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
deleted file mode 100644
index 9e0343d..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/FullFeaturedSdkTest.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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.privacysandbox.tools.apicompiler
-
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
-import java.io.File
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-/** Test the Privacy Sandbox API Compiler with an SDK that uses all available features. */
-@RunWith(JUnit4::class)
-class FullFeaturedSdkTest {
-    @Test
-    fun compileServiceInterface_ok() {
-        val inputTestDataDir = File("src/test/test-data/fullfeaturedsdk/input")
-        val outputTestDataDir = File("src/test/test-data/fullfeaturedsdk/output")
-        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
-
-        val result = compileWithPrivacySandboxKspCompiler(inputSources)
-        assertThat(result).succeeds()
-
-        val expectedAidlFilepath = listOf(
-            "com/mysdk/ICancellationSignal.java",
-            "com/mysdk/IMyCallback.java",
-            "com/mysdk/IMyInterface.java",
-            "com/mysdk/IMyInterfaceTransactionCallback.java",
-            "com/mysdk/IMySdk.java",
-            "com/mysdk/IMySecondInterface.java",
-            "com/mysdk/IMyUiInterface.java",
-            "com/mysdk/IMyUiInterfaceCoreLibInfoAndBinderWrapper.java",
-            "com/mysdk/IMyUiInterfaceTransactionCallback.java",
-            "com/mysdk/IMySecondInterfaceTransactionCallback.java",
-            "com/mysdk/IResponseTransactionCallback.java",
-            "com/mysdk/IStringTransactionCallback.java",
-            "com/mysdk/IUnitTransactionCallback.java",
-            "com/mysdk/IListResponseTransactionCallback.java",
-            "com/mysdk/IListIntTransactionCallback.java",
-            "com/mysdk/IListLongTransactionCallback.java",
-            "com/mysdk/IListDoubleTransactionCallback.java",
-            "com/mysdk/IListStringTransactionCallback.java",
-            "com/mysdk/IListBooleanTransactionCallback.java",
-            "com/mysdk/IListFloatTransactionCallback.java",
-            "com/mysdk/IListCharTransactionCallback.java",
-            "com/mysdk/IListShortTransactionCallback.java",
-            "com/mysdk/ParcelableRequest.java",
-            "com/mysdk/ParcelableResponse.java",
-            "com/mysdk/ParcelableStackFrame.java",
-            "com/mysdk/ParcelableInnerValue.java",
-            "com/mysdk/PrivacySandboxThrowableParcel.java",
-        )
-        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
-            expectedKotlinSources,
-            expectedAidlFilepath
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesApiCompilerDiffTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesApiCompilerDiffTest.kt
new file mode 100644
index 0000000..d9558f0
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesApiCompilerDiffTest.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.privacysandbox.tools.apicompiler
+
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+/**
+ *  Test the Privacy Sandbox API Compiler with an SDK that defines an interface in another package.
+ */
+@RunWith(JUnit4::class)
+class SdkWithPackagesApiCompilerDiffTest : AbstractApiCompilerDiffTest() {
+    override val subdirectoryName = "sdkwithpackages"
+    override val relativePathsToExpectedAidlClasses = listOf(
+        "com/myotherpackage/IMyOtherPackageInterface.java",
+        "com/myotherpackage/ParcelableMyOtherPackageDataClass.java",
+        "com/mysdk/ICancellationSignal.java",
+        "com/mysdk/IListIntTransactionCallback.java",
+        "com/mysdk/IMyOtherPackageDataClassTransactionCallback.java",
+        "com/mysdk/IUnitTransactionCallback.java",
+        "com/mysdk/IMyOtherPackageInterfaceTransactionCallback.java",
+        "com/mysdk/IMySdk.java",
+        "com/mysdk/ParcelableStackFrame.java",
+        "com/mysdk/IStringTransactionCallback.java",
+        "com/mysdk/IMyMainPackageInterfaceTransactionCallback.java",
+        "com/mysdk/IMyMainPackageInterface.java",
+        "com/mysdk/PrivacySandboxThrowableParcel.java"
+    )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt
deleted file mode 100644
index 7928c89..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/SdkWithPackagesTest.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.privacysandbox.tools.apicompiler
-
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
-import java.io.File
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-/**
- *  Test the Privacy Sandbox API Compiler with an SDK that defines an interface in another package.
- */
-@RunWith(JUnit4::class)
-class SdkWithPackagesTest {
-    @Test
-    fun compileServiceInterface_ok() {
-        val inputTestDataDir = File("src/test/test-data/sdkwithpackages/input")
-        val outputTestDataDir = File("src/test/test-data/sdkwithpackages/output")
-        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
-
-        val result = compileWithPrivacySandboxKspCompiler(inputSources)
-        assertThat(result).succeeds()
-
-        val expectedAidlFilepath = listOf(
-            "com/myotherpackage/IMyOtherPackageInterface.java",
-            "com/myotherpackage/ParcelableMyOtherPackageDataClass.java",
-            "com/mysdk/ICancellationSignal.java",
-            "com/mysdk/IListIntTransactionCallback.java",
-            "com/mysdk/IMyOtherPackageDataClassTransactionCallback.java",
-            "com/mysdk/IUnitTransactionCallback.java",
-            "com/mysdk/IMyOtherPackageInterfaceTransactionCallback.java",
-            "com/mysdk/IMySdk.java",
-            "com/mysdk/ParcelableStackFrame.java",
-            "com/mysdk/IStringTransactionCallback.java",
-            "com/mysdk/IMyMainPackageInterfaceTransactionCallback.java",
-            "com/mysdk/IMyMainPackageInterface.java",
-            "com/mysdk/PrivacySandboxThrowableParcel.java"
-        )
-        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
-            expectedKotlinSources,
-            expectedAidlFilepath
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
index 553be79..e57803a 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/TestUtils.kt
@@ -17,6 +17,7 @@
 package androidx.privacysandbox.tools.apicompiler
 
 import androidx.privacysandbox.tools.testing.CompilationTestHelper
+import androidx.privacysandbox.tools.testing.TestEnvironment
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.compiler.TestCompilationResult
 
@@ -33,13 +34,8 @@
     val provider = PrivacySandboxKspCompiler.Provider()
 
     val processorOptions = buildMap {
-        val aidlCompilerPath = (System.getProperty("aidl_compiler_path")
-            ?: throw IllegalArgumentException("aidl_compiler_path flag not set."))
-        put("aidl_compiler_path", aidlCompilerPath)
-        val frameworkAidlPath = (System.getProperty("framework_aidl_path")
-            ?: throw IllegalArgumentException("framework_aidl_path flag not set."))
-        put("aidl_compiler_path", aidlCompilerPath)
-        put("framework_aidl_path", frameworkAidlPath)
+        put("aidl_compiler_path", TestEnvironment.aidlCompilerPath.toString())
+        put("framework_aidl_path", TestEnvironment.frameworkAidlPath.toString())
         putAll(extraProcessorOptions)
     }
 
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkApiCompilerDiffTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkApiCompilerDiffTest.kt
new file mode 100644
index 0000000..8fcee4c
--- /dev/null
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkApiCompilerDiffTest.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.privacysandbox.tools.apicompiler
+
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class WithoutRuntimeLibrarySdkApiCompilerDiffTest : AbstractApiCompilerDiffTest() {
+    override val subdirectoryName = "withoutruntimelibrarysdk"
+    override val extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
+    override val relativePathsToExpectedAidlClasses = listOf(
+        "com/mysdk/ICancellationSignal.java",
+        "com/mysdk/IWithoutRuntimeLibrarySdk.java",
+        "com/mysdk/IStringTransactionCallback.java",
+        "com/mysdk/ParcelableStackFrame.java",
+        "com/mysdk/PrivacySandboxThrowableParcel.java",
+    )
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
deleted file mode 100644
index fa4ec0d..0000000
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/WithoutRuntimeLibrarySdkTest.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.privacysandbox.tools.apicompiler
-
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
-import java.io.File
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class WithoutRuntimeLibrarySdkTest {
-    @Test
-    fun compileServiceInterface_ok() {
-        val inputTestDataDir = File("src/test/test-data/withoutruntimelibrarysdk/input")
-        val outputTestDataDir = File("src/test/test-data/withoutruntimelibrarysdk/output")
-        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
-
-        val result = compileWithPrivacySandboxKspCompiler(
-            inputSources,
-            extraProcessorOptions = mapOf("skip_sdk_runtime_compat_library" to "true")
-        )
-        assertThat(result).succeeds()
-
-        val expectedAidlFilepath = listOf(
-            "com/mysdk/ICancellationSignal.java",
-            "com/mysdk/IWithoutRuntimeLibrarySdk.java",
-            "com/mysdk/IStringTransactionCallback.java",
-            "com/mysdk/ParcelableStackFrame.java",
-            "com/mysdk/PrivacySandboxThrowableParcel.java",
-        )
-        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
-            expectedKotlinSources,
-            expectedAidlFilepath
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index afe4b88..5ddde21 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -52,20 +52,24 @@
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include: "android.jar"
     ))
-    // Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
-    def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
-    def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
-    test {
-        inputs.files(aidlCompilerPath)
-                .withPropertyName("aidl_compiler_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        inputs.files(frameworkAidlPath)
-                .withPropertyName("framework_aidl_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        doFirst {
-            systemProperty "aidl_compiler_path", aidlCompilerPath
-            systemProperty "framework_aidl_path", frameworkAidlPath
-        }
+}
+
+// Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
+def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
+def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
+def testGeneratedSourcesPath = "${project.buildDir}/testGeneratedSources"
+test {
+    inputs.files(aidlCompilerPath)
+            .withPropertyName("aidl_compiler_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.files(frameworkAidlPath)
+            .withPropertyName("framework_aidl_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.dir("src/test/test-data").withPathSensitivity(PathSensitivity.RELATIVE)
+    doFirst {
+        systemProperty "aidl_compiler_path", aidlCompilerPath
+        systemProperty "framework_aidl_path", frameworkAidlPath
+        systemProperty "test_output_dir", testGeneratedSourcesPath
     }
 }
 
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/AbstractApiGeneratorDiffTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/AbstractApiGeneratorDiffTest.kt
new file mode 100644
index 0000000..feb81d0
--- /dev/null
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/AbstractApiGeneratorDiffTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.privacysandbox.tools.apigenerator
+
+import androidx.privacysandbox.tools.core.Metadata
+import androidx.privacysandbox.tools.testing.AbstractDiffTest
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
+import androidx.privacysandbox.tools.testing.TestEnvironment
+import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
+import androidx.room.compiler.processing.util.Source
+import java.nio.file.Path
+import org.junit.Test
+
+/**
+ * Base test for API Compiler diff test. It calls the API Packager to generate true-to-production
+ * SDK API descriptors, invokes the generator and compiles the generated sources.
+ */
+abstract class AbstractApiGeneratorDiffTest : AbstractDiffTest() {
+
+    override fun generateSources(
+        inputSources: List<Source>,
+        outputDirectory: Path
+    ): List<Source> {
+        val descriptors =
+            compileIntoInterfaceDescriptorsJar(
+                inputSources,
+                mapOf(Metadata.filePath to Metadata.toolMetadata.toByteArray())
+            )
+        val generator = PrivacySandboxApiGenerator()
+        generator.generate(
+            descriptors,
+            TestEnvironment.aidlCompilerPath, TestEnvironment.frameworkAidlPath, outputDirectory)
+        return loadSourcesFromDirectory(outputDirectory.toFile())
+    }
+
+    @Test
+    fun generatedSourcesCompile() {
+        assertCompiles(generatedSources)
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
deleted file mode 100644
index 349bc7f..0000000
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * 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.privacysandbox.tools.apigenerator
-
-import androidx.privacysandbox.tools.core.Metadata
-import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
-import androidx.privacysandbox.tools.testing.hasAllExpectedGeneratedSourceFilesAndContent
-import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
-import androidx.room.compiler.processing.util.Source
-import java.io.File
-import java.nio.file.Files
-import kotlin.io.path.Path
-import org.junit.Test
-
-abstract class BaseApiGeneratorTest {
-    abstract val inputDirectory: File
-    abstract val outputDirectory: File
-    abstract val relativePathsToExpectedAidlClasses: List<String>
-
-    private val generatedSources: List<Source> by lazy {
-        val descriptors =
-            compileIntoInterfaceDescriptorsJar(
-                loadSourcesFromDirectory(inputDirectory),
-                mapOf(Metadata.filePath to Metadata.toolMetadata.toByteArray())
-            )
-        val aidlCompilerPath = System.getProperty("aidl_compiler_path")?.let(::Path)
-            ?: throw IllegalArgumentException("aidl_compiler_path flag not set.")
-        val frameworkAidlPath = System.getProperty("framework_aidl_path")?.let(::Path)
-            ?: throw IllegalArgumentException("framework_aidl_path flag not set.")
-
-        val generator = PrivacySandboxApiGenerator()
-
-        val outputDir = Files.createTempDirectory("output").also { it.toFile().deleteOnExit() }
-        generator.generate(descriptors, aidlCompilerPath, frameworkAidlPath, outputDir)
-        loadSourcesFromDirectory(outputDir.toFile())
-    }
-
-    @Test
-    fun generatedApi_compiles() {
-        assertCompiles(generatedSources)
-    }
-
-    @Test
-    fun generatedApi_hasExpectedContents() {
-        val expectedKotlinSources = loadSourcesFromDirectory(outputDirectory)
-        hasAllExpectedGeneratedSourceFilesAndContent(
-            generatedSources,
-            expectedKotlinSources,
-            relativePathsToExpectedAidlClasses
-        )
-    }
-}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorDiffTest.kt
similarity index 81%
rename from privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
rename to privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorDiffTest.kt
index 03bc25d..2c60fa7 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/CallbacksApiGeneratorDiffTest.kt
@@ -16,14 +16,12 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
-import java.io.File
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class CallbacksApiGeneratorTest : BaseApiGeneratorTest() {
-    override val inputDirectory = File("src/test/test-data/callbacks/input")
-    override val outputDirectory = File("src/test/test-data/callbacks/output")
+class CallbacksApiGeneratorDiffTest : AbstractApiGeneratorDiffTest() {
+    override val subdirectoryName = "callbacks"
     override val relativePathsToExpectedAidlClasses = listOf(
         "com/sdkwithcallbacks/IMyInterface.java",
         "com/sdkwithcallbacks/ISdkCallback.java",
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorDiffTest.kt
similarity index 84%
rename from privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
rename to privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorDiffTest.kt
index 29664ab..e77fda56 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/InterfaceApiGeneratorDiffTest.kt
@@ -16,14 +16,12 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
-import java.io.File
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class InterfaceApiGeneratorTest : BaseApiGeneratorTest() {
-    override val inputDirectory = File("src/test/test-data/interfaces/input")
-    override val outputDirectory = File("src/test/test-data/interfaces/output")
+class InterfaceApiGeneratorDiffTest : AbstractApiGeneratorDiffTest() {
+    override val subdirectoryName = "interfaces"
     override val relativePathsToExpectedAidlClasses = listOf(
         "com/sdk/IMySdk.java",
         "com/sdk/IMyInterface.java",
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorDiffTest.kt
similarity index 86%
rename from privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
rename to privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorDiffTest.kt
index 7437b18..bf87924 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/PrimitivesApiGeneratorDiffTest.kt
@@ -16,14 +16,12 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
-import java.io.File
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class PrimitivesApiGeneratorTest : BaseApiGeneratorTest() {
-    override val inputDirectory = File("src/test/test-data/primitives/input")
-    override val outputDirectory = File("src/test/test-data/primitives/output")
+class PrimitivesApiGeneratorDiffTest : AbstractApiGeneratorDiffTest() {
+    override val subdirectoryName = "primitives"
     override val relativePathsToExpectedAidlClasses = listOf(
         "com/mysdk/ITestSandboxSdk.java",
         "com/mysdk/IBooleanTransactionCallback.java",
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorDiffTest.kt
similarity index 85%
rename from privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
rename to privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorDiffTest.kt
index c729c44..526e75b 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/ValuesApiGeneratorDiffTest.kt
@@ -16,14 +16,12 @@
 
 package androidx.privacysandbox.tools.apigenerator
 
-import java.io.File
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
-class ValuesApiGeneratorTest : BaseApiGeneratorTest() {
-    override val inputDirectory = File("src/test/test-data/values/input")
-    override val outputDirectory = File("src/test/test-data/values/output")
+class ValuesApiGeneratorDiffTest : AbstractApiGeneratorDiffTest() {
+    override val subdirectoryName = "values"
     override val relativePathsToExpectedAidlClasses = listOf(
         "com/sdkwithvalues/IMyInterface.java",
         "com/sdkwithvalues/ISdkInterface.java",
diff --git a/privacysandbox/tools/tools-core/build.gradle b/privacysandbox/tools/tools-core/build.gradle
index 7f2ce6b..9e56006 100644
--- a/privacysandbox/tools/tools-core/build.gradle
+++ b/privacysandbox/tools/tools-core/build.gradle
@@ -42,20 +42,22 @@
             dir: "${SdkHelperKt.getSdkPath(project)}/platforms/$SupportConfig.COMPILE_SDK_VERSION/",
             include: "android.jar"
     ))
-    // Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
-    def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
-    def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
-    test {
-        inputs.files(aidlCompilerPath)
-                .withPropertyName("aidl_compiler_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        inputs.files(frameworkAidlPath)
-                .withPropertyName("framework_aidl_path")
-                .withPathSensitivity(PathSensitivity.NAME_ONLY)
-        doFirst {
-            systemProperty "aidl_compiler_path", aidlCompilerPath
-            systemProperty "framework_aidl_path", frameworkAidlPath
-        }
+}
+
+// Get AIDL compiler path and framework.aidl path and pass to tests for code generation.
+def aidlCompilerPath = "${SdkHelperKt.getSdkPath(project)}/build-tools/${SupportConfig.buildToolsVersion(project)}/aidl"
+def frameworkAidlPath = "${SdkHelperKt.getSdkPath(project)}/platforms/${SupportConfig.COMPILE_SDK_VERSION}/framework.aidl"
+test {
+    inputs.files(aidlCompilerPath)
+            .withPropertyName("aidl_compiler_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.files(frameworkAidlPath)
+            .withPropertyName("framework_aidl_path")
+            .withPathSensitivity(PathSensitivity.NAME_ONLY)
+    inputs.dir("src/test/test-data").withPathSensitivity(PathSensitivity.RELATIVE)
+    doFirst {
+        systemProperty "aidl_compiler_path", aidlCompilerPath
+        systemProperty "framework_aidl_path", frameworkAidlPath
     }
 }
 
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/AbstractDiffTest.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/AbstractDiffTest.kt
new file mode 100644
index 0000000..bd362a5
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/AbstractDiffTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.testing
+
+import androidx.room.compiler.processing.util.Source
+import com.google.common.truth.Truth
+import java.io.File
+import java.nio.file.Path
+import kotlin.io.path.Path
+import org.junit.Test
+
+/** Base test class for diff testing Privacy Sandbox tool output. */
+abstract class AbstractDiffTest {
+    /** Name for the subdirectory used to read input and expected sources. */
+    abstract val subdirectoryName: String
+
+    /**
+     * List of relative paths to expected AIDL files. We will assert that they are present in the
+     * final output but we won't check their contents.
+     */
+    abstract val relativePathsToExpectedAidlClasses: List<String>
+
+    /**
+     * Generates the sources and stores them in the given [outputDirectory].
+     * @param inputSources List of input sources read from the test-data directory with
+     * [subdirectoryName].
+     */
+    abstract fun generateSources(
+        inputSources: List<Source>,
+        outputDirectory: Path,
+    ): List<Source>
+
+    protected val generatedSources: List<Source> by lazy {
+        val inputSources =
+            loadSourcesFromDirectory(File("src/test/test-data/$subdirectoryName/input"))
+        outputDir.toFile().also {
+            if (it.exists()) {
+                it.deleteRecursively()
+            }
+            it.mkdirs()
+        }
+        generateSources(inputSources, outputDir)
+    }
+
+    @Test
+    fun generatedSourcesHaveExpectedContents() {
+        val expectedKotlinSources =
+            loadSourcesFromDirectory(File("src/test/test-data/$subdirectoryName/output"))
+
+        val expectedRelativePaths =
+            expectedKotlinSources.map(Source::relativePath) + relativePathsToExpectedAidlClasses
+        Truth.assertThat(generatedSources.map(Source::relativePath))
+            .containsExactlyElementsIn(expectedRelativePaths)
+
+        val actualRelativePathMap = generatedSources.associateBy(Source::relativePath)
+        for (expectedKotlinSource in expectedKotlinSources) {
+            Truth.assertWithMessage(
+                "Contents of generated file ${expectedKotlinSource.relativePath} don't " +
+                    "match golden. Here's the path to generated sources: $outputDir"
+            ).that(actualRelativePathMap[expectedKotlinSource.relativePath]?.contents)
+                .isEqualTo(expectedKotlinSource.contents)
+        }
+    }
+
+    private val outputDir: Path by lazy {
+        requireNotNull(System.getProperty("test_output_dir")) {
+            "test_output_dir not set for diff test."
+        }.let { Path(it).resolve(subdirectoryName) }
+    }
+}
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestEnvironment.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestEnvironment.kt
new file mode 100644
index 0000000..2a65ddd
--- /dev/null
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/TestEnvironment.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.privacysandbox.tools.testing
+
+import kotlin.io.path.Path
+
+object TestEnvironment {
+    val aidlCompilerPath = requireNotNull(System.getProperty("aidl_compiler_path")) {
+        "aidl_compiler_path flag not set"
+    }.let(::Path)
+
+    val frameworkAidlPath = requireNotNull(System.getProperty("framework_aidl_path")) {
+        "framework_aidl_path flag not set."
+    }.let(::Path)
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 374a6bb..51235a5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -630,6 +630,7 @@
 includeProject(":customview:customview", [BuildType.MAIN])
 includeProject(":customview:customview-poolingcontainer", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":datastore:datastore", [BuildType.MAIN, BuildType.KMP])
+includeProject(":datastore:datastore-benchmark", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-core", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-core-okio", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-compose-samples", [BuildType.COMPOSE])
diff --git a/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/matchers/MSSIMMatcher.kt b/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/matchers/MSSIMMatcher.kt
index 915c248..ca12d74 100644
--- a/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/matchers/MSSIMMatcher.kt
+++ b/test/screenshot/screenshot/src/main/java/androidx/test/screenshot/matchers/MSSIMMatcher.kt
@@ -210,6 +210,12 @@
         windowWidth: Int,
         windowHeight: Int
     ): DoubleArray {
+        if (windowHeight == 1 && windowWidth == 1) {
+            // There is only one item. The variance of a single item would be 0.
+            // Since Bessel's correction is used below, it will return NaN instead of 0.
+            return doubleArrayOf(0.0, 0.0, 0.0)
+        }
+
         var var0 = 0.0
         var var1 = 0.0
         var varBoth = 0.0
@@ -223,9 +229,11 @@
                 varBoth += v0 * v1
             }
         }
-        var0 /= windowWidth * windowHeight - 1.toDouble()
-        var1 /= windowWidth * windowHeight - 1.toDouble()
-        varBoth /= windowWidth * windowHeight - 1.toDouble()
+        // Using Bessel's correction. Hence, subtracting one.
+        val denominatorWithBesselsCorrection = windowWidth * windowHeight - 1.0
+        var0 /= denominatorWithBesselsCorrection
+        var1 /= denominatorWithBesselsCorrection
+        varBoth /= denominatorWithBesselsCorrection
         return doubleArrayOf(var0, var1, varBoth)
     }
 
@@ -244,4 +252,4 @@
         l += 0.07f * (Color.blue(pixel) / 255f.toDouble()).pow(gamma)
         return l
     }
-}
\ No newline at end of file
+}
diff --git a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
index eabb43b..8902cb4 100644
--- a/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
+++ b/testutils/testutils-paparazzi/src/main/kotlin/androidx/testutils/paparazzi/ImageDiffer.kt
@@ -273,6 +273,12 @@
             windowWidth: Int,
             windowHeight: Int
         ): DoubleArray {
+            if (windowHeight == 1 && windowWidth == 1) {
+                // There is only one item. The variance of a single item would be 0.
+                // Since Bessel's correction is used below, it will return NaN instead of 0.
+                return doubleArrayOf(0.0, 0.0, 0.0)
+            }
+
             var var0 = 0.0
             var var1 = 0.0
             var varBoth = 0.0
@@ -286,9 +292,11 @@
                     varBoth += v0 * v1
                 }
             }
-            var0 /= windowWidth * windowHeight - 1.toDouble()
-            var1 /= windowWidth * windowHeight - 1.toDouble()
-            varBoth /= windowWidth * windowHeight - 1.toDouble()
+            // Using Bessel's correction. Hence, subtracting one.
+            val denominatorWithBesselsCorrection = windowWidth * windowHeight - 1.0
+            var0 /= denominatorWithBesselsCorrection
+            var1 /= denominatorWithBesselsCorrection
+            varBoth /= denominatorWithBesselsCorrection
             return doubleArrayOf(var0, var1, varBoth)
         }
 
@@ -358,4 +366,4 @@
 
         private val SSIM_THRESHOLD: Double = 0.98
     }
-}
\ No newline at end of file
+}
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 44ffb2b..3f5fc27 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -92,6 +92,20 @@
     method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideClassicCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.BoxScope,kotlin.Unit> image, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, optional androidx.compose.foundation.layout.PaddingValues contentPadding, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
   }
 
+  @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutColors {
+  }
+
+  @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardLayoutDefaults {
+    method @androidx.compose.runtime.Composable public void ImageCard(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.Modifier modifier, optional androidx.tv.material3.CardShape shape, optional androidx.tv.material3.CardColors colors, optional androidx.tv.material3.CardScale scale, optional androidx.tv.material3.CardBorder border, optional androidx.tv.material3.CardGlow glow, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method @androidx.compose.runtime.Composable @androidx.compose.runtime.ReadOnlyComposable public androidx.tv.material3.CardLayoutColors contentColor(optional long contentColor, optional long focusedContentColor, optional long pressedContentColor);
+    field public static final androidx.tv.material3.CardLayoutDefaults INSTANCE;
+  }
+
+  public final class CardLayoutKt {
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void StandardCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+    method @androidx.compose.runtime.Composable @androidx.tv.material3.ExperimentalTvMaterial3Api public static void WideCardLayout(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.interaction.MutableInteractionSource,kotlin.Unit> imageCard, kotlin.jvm.functions.Function0<kotlin.Unit> title, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> subtitle, optional kotlin.jvm.functions.Function0<kotlin.Unit> description, optional androidx.tv.material3.CardLayoutColors contentColor, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource);
+  }
+
   @androidx.compose.runtime.Immutable @androidx.tv.material3.ExperimentalTvMaterial3Api public final class CardScale {
   }
 
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt
new file mode 100644
index 0000000..5c6a7ff
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutScreenshotTest.kt
@@ -0,0 +1,276 @@
+/*
+ * 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.tv.material3
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalTvMaterial3Api::class)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class CardLayoutScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(TV_GOLDEN_MATERIAL3)
+
+    private val boxSizeModifier = Modifier.size(220.dp, 180.dp)
+    private val standardCardLayoutSizeModifier = Modifier.size(150.dp, 120.dp)
+    private val wideCardLayoutSizeModifier = Modifier.size(180.dp, 100.dp)
+
+    @Test
+    fun standardCardLayout_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    StandardCardLayout(
+                        modifier = standardCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(80.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Standard Card") }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("standardCardLayout_lightTheme")
+    }
+
+    @Test
+    fun standardCardLayout_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    StandardCardLayout(
+                        modifier = standardCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxWidth()
+                                        .height(80.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Standard Card") }
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("standardCardLayout_darkTheme")
+    }
+
+    @Test
+    fun standardCardLayout_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier
+                    .testTag(CardLayoutWrapperTag)
+                    .semantics(mergeDescendants = true) {},
+                contentAlignment = Alignment.Center
+            ) {
+                StandardCardLayout(
+                    modifier = standardCardLayoutSizeModifier,
+                    imageCard = { interactionSource ->
+                        CardLayoutDefaults.ImageCard(
+                            onClick = { },
+                            interactionSource = interactionSource
+                        ) {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxWidth()
+                                    .height(80.dp)
+                            )
+                        }
+                    },
+                    title = { Text("Standard Card", Modifier.padding(top = 5.dp)) }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("standardCardLayout_focused")
+    }
+
+    @Test
+    fun wideCardLayout_lightTheme() {
+        rule.setContent {
+            LightMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    WideCardLayout(
+                        modifier = wideCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxHeight()
+                                        .width(90.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideCardLayout_lightTheme")
+    }
+
+    @Test
+    fun wideCardLayout_darkTheme() {
+        rule.setContent {
+            DarkMaterialTheme {
+                Box(
+                    modifier = boxSizeModifier.testTag(CardLayoutWrapperTag),
+                    contentAlignment = Alignment.Center
+                ) {
+                    WideCardLayout(
+                        modifier = wideCardLayoutSizeModifier,
+                        imageCard = { interactionSource ->
+                            CardLayoutDefaults.ImageCard(
+                                onClick = { },
+                                interactionSource = interactionSource
+                            ) {
+                                SampleImage(
+                                    Modifier
+                                        .fillMaxHeight()
+                                        .width(90.dp)
+                                )
+                            }
+                        },
+                        title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                    )
+                }
+            }
+        }
+
+        assertAgainstGolden("wideCardLayout_darkTheme")
+    }
+
+    @Test
+    fun wideCardLayout_focused() {
+        rule.setContent {
+            Box(
+                modifier = boxSizeModifier
+                    .testTag(CardLayoutWrapperTag)
+                    .semantics(mergeDescendants = true) {},
+                contentAlignment = Alignment.Center
+            ) {
+                WideCardLayout(
+                    modifier = wideCardLayoutSizeModifier,
+                    imageCard = { interactionSource ->
+                        CardLayoutDefaults.ImageCard(
+                            onClick = { },
+                            interactionSource = interactionSource
+                        ) {
+                            SampleImage(
+                                Modifier
+                                    .fillMaxHeight()
+                                    .width(90.dp)
+                            )
+                        }
+                    },
+                    title = { Text("Wide Card", Modifier.padding(start = 8.dp)) },
+                )
+            }
+        }
+
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+        rule.waitForIdle()
+
+        assertAgainstGolden("wideCardLayout_focused")
+    }
+
+    @Composable
+    fun SampleImage(modifier: Modifier = Modifier) {
+        Box(
+            modifier = modifier
+                .background(Color.Blue)
+        )
+    }
+
+    private fun assertAgainstGolden(goldenName: String) {
+        rule.onNodeWithTag(CardLayoutWrapperTag)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, goldenName)
+    }
+}
+
+private const val CardLayoutWrapperTag = "card_layout_wrapper"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt
new file mode 100644
index 0000000..f180509
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardLayoutTest.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.semantics.SemanticsProperties
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertHasClickAction
+import androidx.compose.ui.test.assertIsEnabled
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performKeyInput
+import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.pressKey
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(
+    ExperimentalTestApi::class,
+    ExperimentalComposeUiApi::class,
+    ExperimentalTvMaterial3Api::class
+)
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class CardLayoutTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun standardCardLayout_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            StandardCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(StandardCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun standardCardLayout_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            StandardCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(StandardCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(StandardCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Test
+    fun wideCardLayout_semantics() {
+        val count = mutableStateOf(0)
+        rule.setContent {
+            WideCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .assertHasClickAction()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.Role))
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .assertIsEnabled()
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .assertTextEquals("0")
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .assertTextEquals("1")
+    }
+
+    @Test
+    fun wideCardLayout_clickAction() {
+        val count = mutableStateOf(0f)
+        rule.setContent {
+            WideCardLayout(
+                modifier = Modifier
+                    .semantics(mergeDescendants = true) {}
+                    .testTag(WideCardLayoutTag),
+                imageCard = { interactionSource ->
+                    CardLayoutDefaults.ImageCard(
+                        onClick = { count.value += 1 },
+                        interactionSource = interactionSource
+                    ) { SampleImage() }
+                },
+                title = { Text("${count.value}") }
+            )
+        }
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(1)
+
+        rule.onNodeWithTag(WideCardLayoutTag)
+            .onChild()
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+            .performKeyInput { pressKey(Key.DirectionCenter) }
+        Truth.assertThat(count.value).isEqualTo(3)
+    }
+
+    @Composable
+    fun SampleImage() {
+        Box(
+            Modifier
+                .size(180.dp, 150.dp)
+                .testTag(SampleImageTag)
+        )
+    }
+}
+
+private const val StandardCardLayoutTag = "standard-card-layout"
+private const val WideCardLayoutTag = "wide-card-layout"
+
+private const val SampleImageTag = "sample-image"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
index 3ea23a8..68be7c4 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/CardTest.kt
@@ -471,10 +471,8 @@
 }
 
 private const val CardTag = "card"
-private const val StandardCardTag = "standard-card"
 private const val CompactCardTag = "compact-card"
 private const val ClassicCardTag = "classic-card"
-private const val WideCardTag = "wide-card"
 private const val WideClassicCardTag = "wide-classic-card"
 
 private const val SampleImageTag = "sample-image"
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
index ba9bc9b..a522dc83 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/ModalNavigationDrawerTest.kt
@@ -109,8 +109,8 @@
                 drawerState = navigationDrawerValue,
                 drawerContent = {
                     BasicText(
-                        text =
-                        if (it == DrawerValue.Open) "Opened" else "Closed"
+                        modifier = Modifier.focusable(),
+                        text = if (it == DrawerValue.Open) "Opened" else "Closed"
                     )
                 }) { BasicText("other content") }
         }
@@ -138,7 +138,10 @@
                         .focusable(false),
                     drawerState = navigationDrawerValue,
                     drawerContent = {
-                        BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                        BasicText(
+                            modifier = Modifier.focusable(),
+                            text = if (it == DrawerValue.Open) "Opened" else "Closed"
+                        )
                     }) {
                     Box(modifier = Modifier.focusable()) {
                         BasicText("Button")
@@ -154,7 +157,7 @@
         rule.onAllNodesWithText("Closed").assertAnyAreDisplayed()
     }
 
-    @OptIn(ExperimentalComposeUiApi::class, ExperimentalTestApi::class)
+    @OptIn(ExperimentalTestApi::class)
     @Test
     fun modalNavigationDrawer_focusMovesIntoDrawer_openStateComposableDisplayed() {
         InstrumentationRegistry.getInstrumentation().setInTouchMode(false)
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerTest.kt
index d1aedfa..c7d8840 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material3/NavigationDrawerTest.kt
@@ -105,8 +105,8 @@
                 drawerState = navigationDrawerValue,
                 drawerContent = {
                     BasicText(
-                        text =
-                        if (it == DrawerValue.Open) "Opened" else "Closed"
+                        modifier = Modifier.focusable(),
+                        text = if (it == DrawerValue.Open) "Opened" else "Closed"
                     )
                 }) { BasicText("other content") }
         }
@@ -132,7 +132,10 @@
                     modifier = Modifier.focusRequester(drawerFocusRequester),
                     drawerState = navigationDrawerValue,
                     drawerContent = {
-                        BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                        BasicText(
+                            text = if (it == DrawerValue.Open) "Opened" else "Closed",
+                            modifier = Modifier.focusable()
+                        )
                     }) {
                     Box(modifier = Modifier.focusable()) {
                         BasicText("Button")
@@ -335,7 +338,10 @@
                 modifier = Modifier.focusRequester(drawerFocusRequester),
                 drawerState = navigationDrawerValue,
                 drawerContent = {
-                    BasicText(text = if (it == DrawerValue.Open) "Opened" else "Closed")
+                    BasicText(
+                        modifier = Modifier.focusable(),
+                        text = if (it == DrawerValue.Open) "Opened" else "Closed"
+                    )
                 }) { BasicText("other content") }
         }
 
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
index 53add8d..1c1fc72 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Card.kt
@@ -30,7 +30,6 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -325,19 +324,7 @@
 }
 
 @Composable
-private fun CardContent(
-    title: @Composable () -> Unit,
-    subtitle: @Composable () -> Unit = {},
-    description: @Composable () -> Unit = {},
-    contentColor: Color
-) {
-    CompositionLocalProvider(LocalContentColor provides contentColor) {
-        CardContent(title, subtitle, description)
-    }
-}
-
-@Composable
-private fun CardContent(
+internal fun CardContent(
     title: @Composable () -> Unit,
     subtitle: @Composable () -> Unit = {},
     description: @Composable () -> Unit = {}
@@ -399,22 +386,6 @@
     )
 
     /**
-     * Returns the content color [Color] from the colors [CardColors] for different
-     * interaction states.
-     */
-    internal fun contentColor(
-        focused: Boolean,
-        pressed: Boolean,
-        colors: CardColors
-    ): Color {
-        return when {
-            focused -> colors.focusedContentColor
-            pressed -> colors.pressedContentColor
-            else -> colors.contentColor
-        }
-    }
-
-    /**
      * Creates a [CardShape] that represents the default container shapes used in a Card.
      *
      * @param shape the default shape used when the Card has no other [Interaction]s.
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
new file mode 100644
index 0000000..92aa581
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/CardLayout.kt
@@ -0,0 +1,288 @@
+/*
+ * 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.tv.material3
+
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsFocusedAsState
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+
+/**
+ * [StandardCardLayout] is an opinionated TV Material Card layout with an image and text content
+ * to show information about a subject.
+ *
+ * It provides a vertical layout with an image card slot at the top. And below that, there are
+ * slots for the title, subtitle and description.
+ *
+ * @param imageCard defines the [Composable] to be used for the image card. See
+ * [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
+ * in the lambda function should be forwarded and used with the image card composable.
+ * @param title defines the [Composable] title placed below the image card in the CardLayout.
+ * @param modifier the [Modifier] to be applied to this CardLayout.
+ * @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
+ * @param description defines the [Composable] description placed below the subtitle in CardLayout.
+ * @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
+ * for different interaction states. See [CardLayoutDefaults.contentColor].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this CardLayout. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card layout in different states.
+ * This interaction source param would also be forwarded to be used with the `imageCard` composable.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun StandardCardLayout(
+    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    val focused by interactionSource.collectIsFocusedAsState()
+    val pressed by interactionSource.collectIsPressedAsState()
+
+    Column(
+        modifier = modifier
+    ) {
+        Box(
+            contentAlignment = CardDefaults.ContentImageAlignment,
+        ) {
+            imageCard(interactionSource)
+        }
+        Column(
+            modifier = Modifier
+                .align(Alignment.CenterHorizontally),
+            horizontalAlignment = Alignment.CenterHorizontally
+        ) {
+            CardLayoutContent(
+                title = title,
+                subtitle = subtitle,
+                description = description,
+                contentColor = contentColor.color(
+                    focused = focused,
+                    pressed = pressed
+                )
+            )
+        }
+    }
+}
+
+/**
+ * [WideCardLayout] is an opinionated TV Material Card layout with an image and text content
+ * to show information about a subject.
+ *
+ * It provides a horizontal layout with an image card slot at the start, followed by the title,
+ * subtitle and description at the end.
+ *
+ * @param imageCard defines the [Composable] to be used for the image card. See
+ * [CardLayoutDefaults.ImageCard] to create an image card. The `interactionSource` param provided
+ * in the lambda function should to be forwarded and used with the image card composable.
+ * @param title defines the [Composable] title placed below the image card in the CardLayout.
+ * @param modifier the [Modifier] to be applied to this CardLayout.
+ * @param subtitle defines the [Composable] supporting text placed below the title in CardLayout.
+ * @param description defines the [Composable] description placed below the subtitle in CardLayout.
+ * @param contentColor [CardLayoutColors] defines the content color used in the CardLayout
+ * for different interaction states. See [CardLayoutDefaults.contentColor].
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this CardLayout. You can create and pass in your own `remember`ed instance to observe
+ * [Interaction]s and customize the appearance / behavior of this card layout in different states.
+ * This interaction source param would also be forwarded to be used with the `imageCard` composable.
+ */
+@ExperimentalTvMaterial3Api
+@Composable
+fun WideCardLayout(
+    imageCard: @Composable (interactionSource: MutableInteractionSource) -> Unit,
+    title: @Composable () -> Unit,
+    modifier: Modifier = Modifier,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: CardLayoutColors = CardLayoutDefaults.contentColor(),
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
+) {
+    val focused by interactionSource.collectIsFocusedAsState()
+    val pressed by interactionSource.collectIsPressedAsState()
+
+    Row(
+        modifier = modifier
+    ) {
+        Box(
+            contentAlignment = CardDefaults.ContentImageAlignment
+        ) {
+            imageCard(interactionSource)
+        }
+        Column {
+            CardLayoutContent(
+                title = title,
+                subtitle = subtitle,
+                description = description,
+                contentColor = contentColor.color(
+                    focused = focused,
+                    pressed = pressed
+                )
+            )
+        }
+    }
+}
+
+@Composable
+internal fun CardLayoutContent(
+    title: @Composable () -> Unit,
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {},
+    contentColor: Color
+) {
+    CompositionLocalProvider(LocalContentColor provides contentColor) {
+        CardContent(title, subtitle, description)
+    }
+}
+
+@ExperimentalTvMaterial3Api
+object CardLayoutDefaults {
+    /**
+     * Creates [CardLayoutColors] that represents the default content colors used in a
+     * CardLayout.
+     *
+     * @param contentColor the default content color of this CardLayout.
+     * @param focusedContentColor the content color of this CardLayout when focused.
+     * @param pressedContentColor the content color of this CardLayout when pressed.
+     */
+    @ReadOnlyComposable
+    @Composable
+    fun contentColor(
+        contentColor: Color = MaterialTheme.colorScheme.onSurface,
+        focusedContentColor: Color = contentColor,
+        pressedContentColor: Color = focusedContentColor
+    ) = CardLayoutColors(
+        contentColor = contentColor,
+        focusedContentColor = focusedContentColor,
+        pressedContentColor = pressedContentColor
+    )
+
+    /**
+     * [ImageCard] is basically a [Card] composable with an image as the content. It is recommended
+     * to be used with the different CardLayout(s).
+     *
+     * This Card handles click events, calling its [onClick] lambda.
+     *
+     * @param onClick called when this card is clicked
+     * @param interactionSource the [MutableInteractionSource] representing the stream of
+     * [Interaction]s for this card. When using with the CardLayout(s), it is recommended to
+     * pass in the interaction state obtained from the parent lambda.
+     * @param modifier the [Modifier] to be applied to this card
+     * @param shape [CardShape] defines the shape of this card's container in different interaction
+     * states. See [CardDefaults.shape].
+     * @param colors [CardColors] defines the background & content colors used in this card for
+     * different interaction states. See [CardDefaults.colors].
+     * @param scale [CardScale] defines size of the card relative to its original size for different
+     * interaction states. See [CardDefaults.scale].
+     * @param border [CardBorder] defines a border around the card for different interaction states.
+     * See [CardDefaults.border].
+     * @param glow [CardGlow] defines a shadow to be shown behind the card for different interaction
+     * states. See [CardDefaults.glow].
+     * @param content defines the image content [Composable] to be displayed inside the Card.
+     */
+    @Composable
+    fun ImageCard(
+        onClick: () -> Unit,
+        interactionSource: MutableInteractionSource,
+        modifier: Modifier = Modifier,
+        shape: CardShape = CardDefaults.shape(),
+        colors: CardColors = CardDefaults.colors(),
+        scale: CardScale = CardDefaults.scale(),
+        border: CardBorder = CardDefaults.border(),
+        glow: CardGlow = CardDefaults.glow(),
+        content: @Composable () -> Unit
+    ) {
+        Card(
+            onClick = onClick,
+            modifier = modifier,
+            shape = shape,
+            colors = colors,
+            scale = scale,
+            border = border,
+            glow = glow,
+            interactionSource = interactionSource
+        ) {
+            content()
+        }
+    }
+}
+
+/**
+ * Represents the [Color] of content in a CardLayout for different interaction states.
+ */
+@ExperimentalTvMaterial3Api
+@Immutable
+class CardLayoutColors internal constructor(
+    internal val contentColor: Color,
+    internal val focusedContentColor: Color,
+    internal val pressedContentColor: Color,
+) {
+    /**
+     * Returns the content color [Color] for different interaction states.
+     */
+    internal fun color(
+        focused: Boolean,
+        pressed: Boolean
+    ): Color {
+        return when {
+            focused -> focusedContentColor
+            pressed -> pressedContentColor
+            else -> contentColor
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other == null || this::class != other::class) return false
+
+        other as CardLayoutColors
+
+        if (contentColor != other.contentColor) return false
+        if (focusedContentColor != other.focusedContentColor) return false
+        if (pressedContentColor != other.pressedContentColor) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = contentColor.hashCode()
+        result = 31 * result + focusedContentColor.hashCode()
+        result = 31 * result + pressedContentColor.hashCode()
+        return result
+    }
+
+    override fun toString(): String {
+        return "CardLayoutContentColor(" +
+            "contentColor=$contentColor, " +
+            "focusedContentColor=$focusedContentColor, " +
+            "pressedContentColor=$pressedContentColor)"
+    }
+}
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
index 4d00127..ac34cff 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/NavigationDrawer.kt
@@ -16,10 +16,10 @@
 
 package androidx.tv.material3
 
-import android.view.KeyEvent.KEYCODE_BACK
 import androidx.compose.animation.animateContentSize
 import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.focusable
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.focusGroup
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
@@ -35,28 +35,16 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
-import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusState
-import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
-import androidx.compose.ui.input.key.key
-import androidx.compose.ui.input.key.nativeKeyCode
-import androidx.compose.ui.input.key.onKeyEvent
-import androidx.compose.ui.input.key.type
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection.Ltr
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
 
@@ -96,20 +84,10 @@
     scrimColor: Color = LocalColorScheme.current.scrim.copy(alpha = 0.5f),
     content: @Composable () -> Unit
 ) {
-    val layoutDirection = LocalLayoutDirection.current
     val localDensity = LocalDensity.current
-    val exitDirection =
-        if (layoutDirection == Ltr) FocusDirection.Right else FocusDirection.Left
-    val drawerFocusRequester = remember { FocusRequester() }
     val closedDrawerWidth: MutableState<Dp?> = remember { mutableStateOf(null) }
     val internalDrawerModifier =
         Modifier
-            .modalDrawerNavigation(
-                drawerFocusRequester = drawerFocusRequester,
-                exitDirection = exitDirection,
-                drawerState = drawerState,
-                focusManager = LocalFocusManager.current
-            )
             .zIndex(Float.MAX_VALUE)
             .onSizeChanged {
                 if (closedDrawerWidth.value == null &&
@@ -252,31 +230,7 @@
 }
 
 @Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class)
-private fun Modifier.modalDrawerNavigation(
-    drawerFocusRequester: FocusRequester,
-    exitDirection: FocusDirection,
-    drawerState: DrawerState,
-    focusManager: FocusManager
-): Modifier {
-    return this
-        .focusRequester(drawerFocusRequester)
-        .focusProperties {
-            exit = {
-                if (it == exitDirection) {
-                    drawerFocusRequester.requestFocus()
-                    drawerState.setValue(DrawerValue.Closed)
-                    focusManager.moveFocus(it)
-                    FocusRequester.Cancel
-                } else {
-                    FocusRequester.Default
-                }
-            }
-        }
-}
-
-@Suppress("IllegalExperimentalApiUsage") // TODO (b/233188423): Address before moving to beta
-@OptIn(ExperimentalComposeUiApi::class, ExperimentalTvMaterial3Api::class)
+@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalFoundationApi::class)
 @Composable
 private fun DrawerSheet(
     modifier: Modifier = Modifier,
@@ -287,12 +241,7 @@
     // indicates that the drawer has been set to its initial state and has grabbed focus if
     // necessary. Controls whether focus is used to decide the state of the drawer going forward.
     var initializationComplete: Boolean by remember { mutableStateOf(false) }
-    val focusManager = LocalFocusManager.current
     var focusState by remember { mutableStateOf<FocusState?>(null) }
-
-    val isDrawerOpen = drawerState.currentValue == DrawerValue.Open
-    val isDrawerClosed = drawerState.currentValue == DrawerValue.Closed
-
     val focusRequester = remember { FocusRequester() }
     LaunchedEffect(key1 = drawerState.currentValue) {
         if (drawerState.currentValue == DrawerValue.Open && focusState?.hasFocus == false) {
@@ -312,25 +261,12 @@
             .then(modifier)
             .onFocusChanged {
                 focusState = it
-                when {
-                    it.isFocused && isDrawerClosed -> {
-                        drawerState.setValue(DrawerValue.Open)
-                        focusManager.moveFocus(FocusDirection.Enter)
-                    }
 
-                    !it.hasFocus && isDrawerOpen && initializationComplete -> {
-                        drawerState.setValue(DrawerValue.Closed)
-                    }
+                if (initializationComplete) {
+                    drawerState.setValue(if (it.hasFocus) DrawerValue.Open else DrawerValue.Closed)
                 }
             }
-            .onKeyEvent {
-                // Handle back press key event
-                if (it.key.nativeKeyCode == KEYCODE_BACK && it.type == KeyDown) {
-                    focusManager.moveFocus(FocusDirection.Exit)
-                }
-                KeyEventPropagation.ContinuePropagation
-            }
-            .focusable()
+            .focusGroup()
 
     Box(modifier = internalModifier) { content.invoke(drawerState.currentValue) }
 }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
index ac7c7f7..78a455a 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material3/Text.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.InlineTextContent
+import androidx.compose.foundation.text.NewTextRendering1_5
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.compositionLocalOf
@@ -105,6 +106,10 @@
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
+    // TODO: Remove this flag if the issue (b/277778635) is fixed or it's deprecated
+    @Suppress("DEPRECATION")
+    NewTextRendering1_5 = false
+
     val textColor = color.takeOrElse {
         style.color.takeOrElse {
             LocalContentColor.current
@@ -206,6 +211,10 @@
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
+    // TODO: Remove this flag if the issue (b/277778635) is fixed or it's deprecated
+    @Suppress("DEPRECATION")
+    NewTextRendering1_5 = false
+
     val textColor = color.takeOrElse {
         style.color.takeOrElse {
             LocalContentColor.current
diff --git a/viewpager2/integration-tests/testapp/build.gradle b/viewpager2/integration-tests/testapp/build.gradle
index 132a2b0..28dd268 100644
--- a/viewpager2/integration-tests/testapp/build.gradle
+++ b/viewpager2/integration-tests/testapp/build.gradle
@@ -31,6 +31,8 @@
     api(libs.kotlinStdlib)
     implementation(project(":viewpager2:viewpager2"))
     implementation("androidx.activity:activity-ktx:1.2.0")
+    // TODO(b/262583150): force tracing 1.1.0 since its required by androidTest
+    implementation("androidx.tracing:tracing:1.1.0")
     implementation(libs.material) {
         exclude group: "androidx.viewpager2", module: "viewpager2"
     }
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
new file mode 100644
index 0000000..f9f368f
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/ButtonScreenshotTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.compose.material3.test
+
+import android.os.Build
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.testutils.assertAgainstGolden
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.screenshot.AndroidXScreenshotTestRule
+import androidx.wear.compose.material3.Button
+import androidx.wear.compose.material3.ButtonDefaults
+import androidx.wear.compose.material3.CenteredText
+import androidx.wear.compose.material3.MaterialTheme
+import androidx.wear.compose.material3.OutlinedButton
+import androidx.wear.compose.material3.SCREENSHOT_GOLDEN_PATH
+import androidx.wear.compose.material3.TEST_TAG
+import androidx.wear.compose.material3.TestIcon
+import androidx.wear.compose.material3.Text
+import androidx.wear.compose.material3.setContentWithTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestName
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+class ButtonScreenshotTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @get:Rule
+    val screenshotRule = AndroidXScreenshotTestRule(SCREENSHOT_GOLDEN_PATH)
+
+    @get:Rule
+    val testName = TestName()
+
+    @Test
+    fun button_enabled() = verifyScreenshot() {
+        sampleBaseButton()
+    }
+
+    @Test
+    fun button_disabled() = verifyScreenshot() {
+        sampleBaseButton(enabled = false)
+    }
+
+    @Test
+    fun three_slot_button_ltr() = verifyScreenshot(layoutDirection = LayoutDirection.Ltr) {
+        sampleThreeSlotButton()
+    }
+
+    @Test
+    fun three_slot_button_rtl() = verifyScreenshot(layoutDirection = LayoutDirection.Rtl) {
+        sampleThreeSlotButton()
+    }
+
+    @Test
+    fun button_outlined_enabled() = verifyScreenshot() {
+        sampleOutlinedButton()
+    }
+
+    @Test
+    fun button_outlined_disabled() = verifyScreenshot() {
+        sampleOutlinedButton(enabled = false)
+    }
+
+    @Test
+    fun button_image_background_enabled() = verifyScreenshot {
+        sampleImageBackgroundButton()
+    }
+
+    @Test
+    fun button_image_background_disabled() = verifyScreenshot {
+        sampleImageBackgroundButton(enabled = false)
+    }
+
+    @Composable
+    private fun sampleBaseButton(enabled: Boolean = true) {
+        Button(
+            enabled = enabled,
+            onClick = {},
+            modifier = Modifier.testTag(TEST_TAG)
+        ) {
+            CenteredText("Base Button")
+        }
+    }
+
+    @Composable
+    private fun sampleThreeSlotButton(enabled: Boolean = true) {
+        Button(
+            enabled = enabled,
+            onClick = {},
+            label = { Text("Three Slot Button") },
+            secondaryLabel = { Text("Secondary Label") },
+            icon = { TestIcon() },
+            modifier = Modifier.testTag(TEST_TAG)
+        )
+    }
+
+    @Composable
+    private fun sampleOutlinedButton(enabled: Boolean = true) {
+        OutlinedButton(
+            enabled = enabled,
+            onClick = {},
+            modifier = Modifier.testTag(TEST_TAG)
+        ) {
+            CenteredText("Outlined Button")
+        }
+    }
+
+    @Composable
+    private fun sampleImageBackgroundButton(enabled: Boolean = true) {
+        Button(
+            enabled = enabled,
+            onClick = {},
+            label = { Text("Image Button") },
+            secondaryLabel = { Text("Secondary Label") },
+            colors = ButtonDefaults.imageBackgroundButtonColors(
+                backgroundImagePainter = painterResource(R.drawable.backgroundimage1)
+            ),
+            icon = { TestIcon() },
+            modifier = Modifier.testTag(TEST_TAG)
+        )
+    }
+
+    private fun verifyScreenshot(
+        layoutDirection: LayoutDirection = LayoutDirection.Ltr,
+        content: @Composable () -> Unit
+    ) {
+        rule.setContentWithTheme {
+            CompositionLocalProvider(LocalLayoutDirection provides layoutDirection) {
+                Box(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .background(MaterialTheme.colorScheme.background)
+                ) {
+                    content()
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .captureToImage()
+            .assertAgainstGolden(screenshotRule, testName.methodName)
+    }
+}
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
index f412e4f..6642e9a 100644
--- a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Material3Test.kt
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2023 The Android Open Source Project
  *
@@ -21,8 +20,11 @@
 import androidx.annotation.RequiresApi
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.material.icons.Icons
@@ -35,8 +37,8 @@
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Dp
@@ -61,6 +63,28 @@
     )
 }
 
+@Composable
+fun TestIcon(modifier: Modifier = Modifier, iconLabel: String = "TestIcon") {
+    val testImage = Icons.Outlined.Add
+    Icon(
+        imageVector = testImage,
+        contentDescription = iconLabel,
+        modifier = modifier.testTag(iconLabel)
+    )
+}
+
+@Composable
+fun CenteredText(
+    text: String
+) {
+    Column(
+        modifier = Modifier.fillMaxHeight(),
+        verticalArrangement = Arrangement.Center
+    ) {
+        Text(text)
+    }
+}
+
 fun ComposeContentTestRule.setContentWithThemeForSizeAssertions(
     parentMaxWidth: Dp = BigTestMaxWidth,
     parentMaxHeight: Dp = BigTestMaxHeight,
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt
new file mode 100644
index 0000000..0b3bb8d
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidAndroidTest/kotlin/androidx/wear/compose/material3/Screenshot.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.compose.material3
+
+internal const val SCREENSHOT_GOLDEN_PATH = "wear/compose/material3"
diff --git a/wear/compose/compose-material3/src/androidAndroidTest/res/drawable/backgroundimage1.png b/wear/compose/compose-material3/src/androidAndroidTest/res/drawable/backgroundimage1.png
new file mode 100644
index 0000000..fbb9332
--- /dev/null
+++ b/wear/compose/compose-material3/src/androidAndroidTest/res/drawable/backgroundimage1.png
Binary files differ
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
index f52e536..160d4bc 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/DynamicDataTransformNode.java
@@ -17,8 +17,10 @@
 package androidx.wear.protolayout.expression.pipeline;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
 import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * Dynamic data node that can perform a transformation from an upstream node. This should be created
@@ -34,8 +36,14 @@
     final Function<I, O> mTransformer;
 
     DynamicDataTransformNode(
+            DynamicTypeValueReceiverWithPreUpdate<O> downstream, Function<I, O> transformer) {
+        this(downstream, transformer, /* validator= */ null);
+    }
+
+    DynamicDataTransformNode(
             DynamicTypeValueReceiverWithPreUpdate<O> downstream,
-            Function<I, O> transformer) {
+            Function<I, O> transformer,
+            @Nullable Predicate<I> validator) {
         this.mDownstream = downstream;
         this.mTransformer = transformer;
 
@@ -49,6 +57,10 @@
 
                     @Override
                     public void onData(@NonNull I newData) {
+                        if (validator != null && !validator.test(newData)) {
+                            mDownstream.onInvalidated();
+                            return;
+                        }
                         O result = mTransformer.apply(newData);
                         mDownstream.onData(result);
                     }
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 eed72d2..57e7620 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
@@ -65,6 +65,7 @@
 import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicColor;
 import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicFloat;
 import androidx.wear.protolayout.expression.proto.DynamicProto.AnimatableDynamicInt32;
+import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalColorOp;
 import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalFloatOp;
 import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalInt32Op;
 import androidx.wear.protolayout.expression.proto.DynamicProto.ConditionalStringOp;
@@ -1049,6 +1050,25 @@
                 bindRecursively(
                         dynamicNode.getInput(), animationNode.getInputCallback(), resultBuilder);
                 break;
+            case CONDITIONAL_OP:
+                ConditionalOpNode<Integer> conditionalNode = new ConditionalOpNode<>(consumer);
+
+                ConditionalColorOp op = colorSource.getConditionalOp();
+                bindRecursively(
+                        op.getCondition(),
+                        conditionalNode.getConditionIncomingCallback(),
+                        resultBuilder);
+                bindRecursively(
+                        op.getValueIfTrue(),
+                        conditionalNode.getTrueValueIncomingCallback(),
+                        resultBuilder);
+                bindRecursively(
+                        op.getValueIfFalse(),
+                        conditionalNode.getFalseValueIncomingCallback(),
+                        resultBuilder);
+
+                node = conditionalNode;
+                break;
             case INNER_NOT_SET:
                 throw new IllegalArgumentException("DynamicColor has no inner source set");
             default:
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
index 3366db4..71ddbd09 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/FloatNodes.java
@@ -38,8 +38,7 @@
         private final DynamicTypeValueReceiverWithPreUpdate<Float> mDownstream;
 
         FixedFloatNode(
-                FixedFloat protoNode,
-                DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
+                FixedFloat protoNode, DynamicTypeValueReceiverWithPreUpdate<Float> downstream) {
             this.mValue = protoNode.getValue();
             this.mDownstream = downstream;
         }
@@ -53,7 +52,11 @@
         @Override
         @UiThread
         public void init() {
-            mDownstream.onData(mValue);
+            if (Float.isNaN(mValue)) {
+                mDownstream.onInvalidated();
+            } else {
+                mDownstream.onData(mValue);
+            }
         }
 
         @Override
diff --git a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
index 9d3f3dd..d542928 100644
--- a/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
+++ b/wear/protolayout/protolayout-expression-pipeline/src/main/java/androidx/wear/protolayout/expression/pipeline/Int32Nodes.java
@@ -47,8 +47,7 @@
         private final DynamicTypeValueReceiverWithPreUpdate<Integer> mDownstream;
 
         FixedInt32Node(
-                FixedInt32 protoNode,
-                DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
+                FixedInt32 protoNode, DynamicTypeValueReceiverWithPreUpdate<Integer> downstream) {
             this.mValue = protoNode.getValue();
             this.mDownstream = downstream;
         }
@@ -202,7 +201,8 @@
                             default:
                                 throw new IllegalArgumentException("Unknown rounding mode");
                         }
-                    });
+                    },
+                    x -> x - 1 < Integer.MAX_VALUE && x >= Integer.MIN_VALUE);
         }
     }
 
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 5ec9a41..fae40b2 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
@@ -20,12 +20,17 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.robolectric.Shadows.shadowOf;
+
 import static java.lang.Integer.MAX_VALUE;
 
+import android.graphics.Color;
 import android.icu.util.ULocale;
+import android.os.Looper;
 
 import androidx.annotation.NonNull;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool;
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicDuration;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter;
@@ -50,6 +55,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.BiConsumer;
 
 @RunWith(ParameterizedRobolectricTestRunner.class)
@@ -83,13 +89,16 @@
             test(DynamicInt32.fromState("state_int_15").div(DynamicFloat.constant(2.0f)), 7.5f),
             test(DynamicInt32.fromState("state_int_15").rem(DynamicFloat.constant(4.5f)), 1.5f),
             test(DynamicFloat.constant(5.0f), 5.0f),
+            testForInvalidValue(DynamicFloat.constant(Float.NaN)),
+            testForInvalidValue(DynamicFloat.constant(Float.NaN).plus(5.0f)),
             test(DynamicFloat.fromState("state_float_1.5"), 1.5f),
             test(DynamicFloat.constant(1234.567f).asInt(), 1234),
             test(DynamicFloat.constant(0.967f).asInt(), 0),
             test(DynamicFloat.constant(-1234.967f).asInt(), -1235),
             test(DynamicFloat.constant(-0.967f).asInt(), -1),
             test(DynamicFloat.constant(Float.MIN_VALUE).asInt(), 0),
-            test(DynamicFloat.constant(Float.MAX_VALUE).asInt(), (int) Float.MAX_VALUE),
+            testForInvalidValue(DynamicFloat.constant(Float.MAX_VALUE).asInt()),
+            testForInvalidValue(DynamicFloat.constant(-Float.MAX_VALUE).asInt()),
             test(DynamicInt32.constant(100).asFloat(), 100.0f),
             test(
                     DynamicInt32.constant(Integer.MIN_VALUE).asFloat(),
@@ -122,10 +131,8 @@
             test(DynamicFloat.constant(0.6f).gte(0.4f), true),
             test(DynamicFloat.constant(0.1234568f).gte(0.1234562f), true),
             test(DynamicBool.constant(true), true),
-            test(DynamicBool.constant(true).isTrue(), true),
-            test(DynamicBool.constant(false).isTrue(), false),
-            test(DynamicBool.constant(true).isFalse(), false),
-            test(DynamicBool.constant(false).isFalse(), true),
+            test(DynamicBool.constant(true).negate(), false),
+            test(DynamicBool.constant(false).negate(), true),
             test(DynamicBool.constant(true).and(DynamicBool.constant(true)), true),
             test(DynamicBool.constant(true).and(DynamicBool.constant(false)), false),
             test(DynamicBool.constant(false).and(DynamicBool.constant(true)), false),
@@ -211,44 +218,71 @@
                             .elseUse(DynamicInt32.constant(10)),
                     10),
             test(
+                    DynamicColor.onCondition(DynamicBool.constant(true))
+                            .use(DynamicColor.constant(Color.BLUE))
+                            .elseUse(DynamicColor.constant(Color.RED)),
+                    Color.BLUE),
+            test(
+                    DynamicColor.onCondition(DynamicBool.constant(false))
+                            .use(DynamicColor.constant(Color.BLUE))
+                            .elseUse(DynamicColor.constant(Color.RED)),
+                    Color.RED),
+            test(
                     DynamicFloat.constant(12.345f)
                             .format(
-                                    FloatFormatter.with()
-                                            .maxFractionDigits(2)
-                                            .minIntegerDigits(4)
-                                            .groupingUsed(true)),
+                                    new FloatFormatter.Builder()
+                                            .setMaxFractionDigits(2)
+                                            .setMinIntegerDigits(4)
+                                            .setGroupingUsed(true)
+                                            .build()),
                     "0,012.35"),
             test(
                     DynamicFloat.constant(12.345f)
                             .format(
-                                    FloatFormatter.with()
-                                            .minFractionDigits(4)
-                                            .minIntegerDigits(4)
-                                            .groupingUsed(false)),
+                                    new FloatFormatter.Builder()
+                                            .setMinFractionDigits(4)
+                                            .setMinIntegerDigits(4)
+                                            .setGroupingUsed(false)
+                                            .build()),
                     "0012.3450"),
             test(
                     DynamicFloat.constant(12.345f)
-                            .format(FloatFormatter.with().maxFractionDigits(1).groupingUsed(true))
+                            .format(
+                                    new FloatFormatter.Builder()
+                                            .setMaxFractionDigits(1)
+                                            .setGroupingUsed(true)
+                                            .build())
                             .concat(DynamicString.constant("°")),
                     "12.3°"),
             test(
                     DynamicFloat.constant(12.345678f)
                             .format(
-                                    FloatFormatter.with()
-                                            .minFractionDigits(4)
-                                            .maxFractionDigits(2)
-                                            .groupingUsed(true)),
+                                    new FloatFormatter.Builder()
+                                            .setMinFractionDigits(4)
+                                            .setMaxFractionDigits(2)
+                                            .setGroupingUsed(true)
+                                            .build()),
                     "12.3457"),
             test(
                     DynamicFloat.constant(12.345678f)
-                            .format(FloatFormatter.with().minFractionDigits(2).groupingUsed(true)),
+                            .format(
+                                    new FloatFormatter.Builder()
+                                            .setMinFractionDigits(2)
+                                            .setGroupingUsed(true)
+                                            .build()),
                     "12.346"),
-            test(DynamicFloat.constant(12.3456f).format(FloatFormatter.with()), "12.346"),
+            test(
+                    DynamicFloat.constant(12.3456f).format(new FloatFormatter.Builder().build()),
+                    "12.346"),
             test(
                     DynamicInt32.constant(12)
-                            .format(IntFormatter.with().minIntegerDigits(4).groupingUsed(true)),
+                            .format(
+                                    new IntFormatter.Builder()
+                                            .setMinIntegerDigits(4)
+                                            .setGroupingUsed(true)
+                                            .build()),
                     "0,012"),
-            test(DynamicInt32.constant(12).format(IntFormatter.with()), "12")
+            test(DynamicInt32.constant(12).format(new IntFormatter.Builder().build()), "12")
         };
         ImmutableList.Builder<Object[]> immutableListBuilder = new ImmutableList.Builder<>();
         for (DynamicTypeEvaluatorTest.TestCase<?> testCase : testCases) {
@@ -304,13 +338,25 @@
                 expectedValue);
     }
 
+    private static DynamicTypeEvaluatorTest.TestCase<Integer> test(
+            DynamicColor bindUnderTest, Integer expectedValue) {
+        return new DynamicTypeEvaluatorTest.TestCase<>(
+                bindUnderTest.toDynamicColorProto().toString(),
+                (evaluator, cb) ->
+                        evaluator
+                                .bind(bindUnderTest, new MainThreadExecutor(), cb)
+                                .startEvaluation(),
+                expectedValue);
+    }
+
     private static DynamicTypeEvaluatorTest.TestCase<Instant> test(
             DynamicInstant bindUnderTest, Instant instant) {
         return new DynamicTypeEvaluatorTest.TestCase<>(
                 bindUnderTest.toDynamicInstantProto().toString(),
-                (evaluator, cb) -> {
-                    evaluator.bind(bindUnderTest, new MainThreadExecutor(), cb).startEvaluation();
-                },
+                (evaluator, cb) ->
+                        evaluator
+                                .bind(bindUnderTest, new MainThreadExecutor(), cb)
+                                .startEvaluation(),
                 instant);
     }
 
@@ -336,6 +382,26 @@
                 expectedValue);
     }
 
+    private static DynamicTypeEvaluatorTest.TestCase<Integer> testForInvalidValue(
+            DynamicInt32 bindUnderTest) {
+        return new DynamicTypeEvaluatorTest.TestCase<>(
+                bindUnderTest.toDynamicInt32Proto().toString(),
+                (evaluator, cb) ->
+                        evaluator
+                                .bind(bindUnderTest, new MainThreadExecutor(), cb)
+                                .startEvaluation());
+    }
+
+    private static DynamicTypeEvaluatorTest.TestCase<Float> testForInvalidValue(
+            DynamicFloat bindUnderTest) {
+        return new DynamicTypeEvaluatorTest.TestCase<>(
+                bindUnderTest.toDynamicFloatProto().toString(),
+                (evaluator, cb) ->
+                        evaluator
+                                .bind(bindUnderTest, new MainThreadExecutor(), cb)
+                                .startEvaluation());
+    }
+
     private static class TestCase<T> {
         private final String mName;
         private final BiConsumer<DynamicTypeEvaluator, DynamicTypeValueReceiver<T>>
@@ -351,8 +417,18 @@
             this.mExpectedValue = expectedValue;
         }
 
+        /** Creates a test case for an expression which expects to result in invalid value. */
+        TestCase(
+                String name,
+                BiConsumer<DynamicTypeEvaluator, DynamicTypeValueReceiver<T>> expressionEvaluator) {
+            this.mName = name;
+            this.mExpressionEvaluator = expressionEvaluator;
+            this.mExpectedValue = null;
+        }
+
         public void runTest(DynamicTypeEvaluator evaluator) {
             List<T> results = new ArrayList<>();
+            AtomicInteger invalidatedCalls = new AtomicInteger(0);
 
             DynamicTypeValueReceiver<T> callback =
                     new DynamicTypeValueReceiver<T>() {
@@ -362,16 +438,25 @@
                         }
 
                         @Override
-                        public void onInvalidated() {}
+                        public void onInvalidated() {
+                            invalidatedCalls.incrementAndGet();
+                        }
                     };
 
             this.mExpressionEvaluator.accept(evaluator, callback);
+            shadowOf(Looper.getMainLooper()).idle();
 
-            assertThat(results).hasSize(1);
-            assertThat(results).containsExactly(mExpectedValue);
+            if (mExpectedValue != null) {
+                // Test expects an actual value.
+                assertThat(results).hasSize(1);
+                assertThat(results).containsExactly(mExpectedValue);
+            } else {
+                // Test expects an invalid value.
+                assertThat(results).isEmpty();
+                assertThat(invalidatedCalls.get()).isEqualTo(1);
+            }
         }
 
-        @NonNull
         @Override
         public String toString() {
             return mName + " = " + mExpectedValue;
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index f66fa06..81a51e5 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -99,8 +99,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
   }
@@ -115,6 +114,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
   }
 
@@ -176,11 +176,19 @@
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter maxFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter with();
+    method @IntRange(from=0) public int getMaxFractionDigits();
+    method @IntRange(from=0) public int getMinFractionDigits();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicFloat.FloatFormatter.Builder {
+    ctor public DynamicBuilders.DynamicFloat.FloatFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMaxFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -241,9 +249,15 @@
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter with();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicInt32.IntFormatter.Builder {
+    ctor public DynamicBuilders.DynamicInt32.IntFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
index bee2cf9..a95e751 100644
--- a/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout-expression/api/public_plus_experimental_current.txt
@@ -99,8 +99,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
   }
@@ -115,6 +114,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
   }
 
@@ -176,11 +176,19 @@
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter maxFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter with();
+    method @IntRange(from=0) public int getMaxFractionDigits();
+    method @IntRange(from=0) public int getMinFractionDigits();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicFloat.FloatFormatter.Builder {
+    ctor public DynamicBuilders.DynamicFloat.FloatFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMaxFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -241,9 +249,15 @@
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter with();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicInt32.IntFormatter.Builder {
+    ctor public DynamicBuilders.DynamicInt32.IntFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index f66fa06..81a51e5 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -99,8 +99,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool constant(boolean);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool fromState(String);
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isFalse();
-    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool isTrue();
+    method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool negate();
     method public default androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool or(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicBoolByteArray();
   }
@@ -115,6 +114,7 @@
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor constant(@ColorInt int);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromByteArray(byte[]);
     method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor fromState(String);
+    method public static androidx.wear.protolayout.expression.ConditionScopes.ConditionScope<androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor!,java.lang.Integer!> onCondition(androidx.wear.protolayout.expression.DynamicBuilders.DynamicBool);
     method public default byte[] toDynamicColorByteArray();
   }
 
@@ -176,11 +176,19 @@
   }
 
   public static class DynamicBuilders.DynamicFloat.FloatFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter maxFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minFractionDigits(@IntRange(from=0) int);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter with();
+    method @IntRange(from=0) public int getMaxFractionDigits();
+    method @IntRange(from=0) public int getMinFractionDigits();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicFloat.FloatFormatter.Builder {
+    ctor public DynamicBuilders.DynamicFloat.FloatFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMaxFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinFractionDigits(@IntRange(from=0) int);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat.FloatFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicInstant extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
@@ -241,9 +249,15 @@
   }
 
   public static class DynamicBuilders.DynamicInt32.IntFormatter {
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter groupingUsed(boolean);
-    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter minIntegerDigits(@IntRange(from=0) int);
-    method public static androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter with();
+    method @IntRange(from=0) public int getMinIntegerDigits();
+    method public boolean isGroupingUsed();
+  }
+
+  public static final class DynamicBuilders.DynamicInt32.IntFormatter.Builder {
+    ctor public DynamicBuilders.DynamicInt32.IntFormatter.Builder();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter build();
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setGroupingUsed(boolean);
+    method public androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32.IntFormatter.Builder setMinIntegerDigits(@IntRange(from=0) int);
   }
 
   public static interface DynamicBuilders.DynamicString extends androidx.wear.protolayout.expression.DynamicBuilders.DynamicType {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 299d7b9..05d9c21 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -150,7 +150,10 @@
     static final int ARITHMETIC_OP_TYPE_MODULO = 5;
 
     /**
-     * Rounding mode to use when converting a float to an int32.
+     * Rounding mode to use when converting a float to an int32. If the value is larger than {@link
+     * Integer#MAX_VALUE} or smaller than {@link Integer#MIN_VALUE}, the result of this operation
+     * will be invalid and will have an invalid value delivered via
+     * {@link DynamicTypeValueReceiver<T>#onInvalidate()}.
      *
      * @since 1.2
      */
@@ -2219,8 +2222,8 @@
 
         /**
          * Returns a {@link DynamicString} that contains the formatted value of this {@link
-         * DynamicInt32} (with default formatting parameters). As an example, in the English locale,
-         * the following is equal to {@code DynamicString.constant("12")}
+         * DynamicInt32} (with default formatting parameters). As an example, for locale en_US, the
+         * following is equal to {@code DynamicString.constant("12")}
          *
          * <pre>
          *   DynamicInt32.constant(12).format()
@@ -2230,65 +2233,115 @@
          */
         @NonNull
         default DynamicString format() {
-            return IntFormatter.with().buildForInput(this);
+            return new IntFormatter.Builder().build().getInt32FormatOp(this);
         }
 
         /**
          * Returns a {@link DynamicString} that contains the formatted value of this {@link
-         * DynamicInt32}. As an example, in the English locale, the following is equal to {@code
+         * DynamicInt32}. As an example, for locale en_US, the following is equal to {@code
          * DynamicString.constant("0,012")}
          *
          * <pre>
          *   DynamicInt32.constant(12)
          *            .format(
-         *                IntFormatter.with().minIntegerDigits(4).groupingUsed(true));
+         *                new IntFormatter.Builder()
+         *                                .setMinIntegerDigits(4)
+         *                                .setGroupingUsed(true)
+         *                                .build());
          * </pre>
          *
-         * The resulted {@link DynamicString} is subject to being truncated if it's too long.
-         *
          * @param formatter The formatting parameter.
          */
         @NonNull
         default DynamicString format(@NonNull IntFormatter formatter) {
-            return formatter.buildForInput(this);
+            return formatter.getInt32FormatOp(this);
         }
 
         /** Allows formatting {@link DynamicInt32} into a {@link DynamicString}. */
         class IntFormatter {
-            private final Int32FormatOp.Builder builder;
+            private final Int32FormatOp.Builder mInt32FormatOpBuilder;
+            private final Int32FormatOp mInt32FormatOp;
 
-            private IntFormatter() {
-                builder = new Int32FormatOp.Builder();
-            }
-
-            /** Creates an instance of {@link IntFormatter} with default configuration. */
-            @NonNull
-            public static IntFormatter with() {
-                return new IntFormatter();
-            }
-
-            /**
-             * Sets minimum number of integer digits for the formatter. Defaults to one if not
-             * specified.
-             */
-            @NonNull
-            public IntFormatter minIntegerDigits(@IntRange(from = 0) int minIntegerDigits) {
-                builder.setMinIntegerDigits(minIntegerDigits);
-                return this;
-            }
-
-            /**
-             * Sets whether grouping is used for the formatter. Defaults to false if not specified.
-             */
-            @NonNull
-            public IntFormatter groupingUsed(boolean groupingUsed) {
-                builder.setGroupingUsed(groupingUsed);
-                return this;
+            IntFormatter(@NonNull Int32FormatOp.Builder int32FormatOpBuilder) {
+                mInt32FormatOpBuilder = int32FormatOpBuilder;
+                mInt32FormatOp = int32FormatOpBuilder.build();
             }
 
             @NonNull
-            Int32FormatOp buildForInput(@NonNull DynamicInt32 dynamicInt32) {
-                return builder.setInput(dynamicInt32).build();
+            Int32FormatOp getInt32FormatOp(@NonNull DynamicInt32 dynamicInt32) {
+                return mInt32FormatOpBuilder.setInput(dynamicInt32).build();
+            }
+
+            /** Returns the minimum number of digits allowed in the integer portion of a number. */
+            @IntRange(from = 0)
+            public int getMinIntegerDigits() {
+                return mInt32FormatOp.getMinIntegerDigits();
+            }
+
+            /** Returns whether digit grouping is used or not. */
+            public boolean isGroupingUsed() {
+                return mInt32FormatOp.getGroupingUsed();
+            }
+
+            /** Builder to create {@link IntFormatter} objects. */
+            public static final class Builder {
+                private static final int MAX_INTEGER_PART_LENGTH = 15;
+                final Int32FormatOp.Builder mBuilder;
+
+                public Builder() {
+                    mBuilder = new Int32FormatOp.Builder();
+                }
+
+                /**
+                 * Sets minimum number of integer digits for the formatter. Defaults to one if not
+                 * specified. If minIntegerDigits is zero and the -1 < input < 1, the Integer
+                 * part will not appear.
+                 */
+                @NonNull
+                public Builder setMinIntegerDigits(@IntRange(from = 0) int minIntegerDigits) {
+                    mBuilder.setMinIntegerDigits(minIntegerDigits);
+                    return this;
+                }
+
+                /**
+                 * Sets whether grouping is used for the formatter. Defaults to false if not
+                 * specified. If grouping is used, digits will be grouped into digit groups using a
+                 * separator. Digit group size and used separator can vary in different
+                 * countries/regions. As an example, for locale en_US, the following is equal to
+                 * {@code * DynamicString.constant("1,234")}
+                 *
+                 * <pre>
+                 *   DynamicInt32.constant(1234)
+                 *       .format(
+                 *           new IntFormatter.Builder()
+                 *                           .setGroupingUsed(true).build());
+                 * </pre>
+                 */
+                @NonNull
+                public Builder setGroupingUsed(boolean groupingUsed) {
+                    mBuilder.setGroupingUsed(groupingUsed);
+                    return this;
+                }
+
+                /** Builds an instance with values accumulated in this Builder. */
+                @NonNull
+                public IntFormatter build() {
+                    throwIfExceedingMaxValue(
+                            "MinIntegerDigits",
+                            mBuilder.build().getMinIntegerDigits(),
+                            MAX_INTEGER_PART_LENGTH);
+                    return new IntFormatter(mBuilder);
+                }
+
+                private static void throwIfExceedingMaxValue(
+                        String paramName, int value, int maxValue) {
+                    if (value > maxValue) {
+                        throw new IllegalArgumentException(
+                                String.format(
+                                        "%s (%d) is too large. Maximum value for %s is %d",
+                                        paramName, value, paramName, maxValue));
+                    }
+                }
             }
         }
 
@@ -2374,8 +2427,8 @@
 
         /**
          * Gets minimum integer digits. Sign and grouping characters are not considered when
-         * applying minIntegerDigits constraint. If not defined, defaults to one. For example,in the
-         * English locale, applying minIntegerDigit=4 to 12 would yield "0012".
+         * applying minIntegerDigits constraint. If not defined, defaults to one. For example, for
+         * locale en_US, applying minIntegerDigit=4 to 12 would yield "0012".
          *
          * @since 1.2
          */
@@ -2386,8 +2439,8 @@
 
         /**
          * Gets digit grouping used. Grouping size and grouping character depend on the current
-         * locale. If not defined, defaults to false. For example, in the English locale, using
-         * grouping with 1234 would yield "1,234".
+         * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
+         * with 1234 would yield "1,234".
          *
          * @since 1.2
          */
@@ -2434,7 +2487,7 @@
 
         /** Builder for {@link Int32FormatOp}. */
         public static final class Builder implements DynamicString.Builder {
-            private final DynamicProto.Int32FormatOp.Builder mImpl =
+            final DynamicProto.Int32FormatOp.Builder mImpl =
                     DynamicProto.Int32FormatOp.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(196209833);
 
@@ -2455,8 +2508,8 @@
 
             /**
              * Sets minimum integer digits. Sign and grouping characters are not considered when
-             * applying minIntegerDigits constraint. If not defined, defaults to one. For example,in
-             * the English locale, applying minIntegerDigit=4 to 12 would yield "0012".
+             * applying minIntegerDigits constraint. If not defined, defaults to one. For example,
+             * for locale en_US, applying minIntegerDigit=4 to 12 would yield "0012".
              *
              * @since 1.2
              */
@@ -2469,7 +2522,7 @@
 
             /**
              * Sets digit grouping used. Grouping size and grouping character depend on the current
-             * locale. If not defined, defaults to false. For example, in the English locale, using
+             * locale. If not defined, defaults to false. For example, for locale en_US, using
              * grouping with 1234 would yield "1,234".
              *
              * @since 1.2
@@ -2898,8 +2951,8 @@
 
         /**
          * Gets minimum integer digits. Sign and grouping characters are not considered when
-         * applying minIntegerDigits constraint. If not defined, defaults to one. For example, in
-         * the English locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
+         * applying minIntegerDigits constraint. If not defined, defaults to one. For example, for
+         * locale en_US, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
          *
          * @since 1.2
          */
@@ -2910,8 +2963,8 @@
 
         /**
          * Gets digit grouping used. Grouping size and grouping character depend on the current
-         * locale. If not defined, defaults to false. For example, in the English locale, using
-         * grouping with 1234.56 would yield "1,234.56".
+         * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
+         * with 1234.56 would yield "1,234.56".
          *
          * @since 1.2
          */
@@ -3014,7 +3067,7 @@
             /**
              * Sets minimum integer digits. Sign and grouping characters are not considered when
              * applying minIntegerDigits constraint. If not defined, defaults to one. For example,
-             * in the English locale, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
+             * for locale en_US, applying minIntegerDigit=4 to 12.34 would yield "0012.34".
              *
              * @since 1.2
              */
@@ -3027,7 +3080,7 @@
 
             /**
              * Sets digit grouping used. Grouping size and grouping character depend on the current
-             * locale. If not defined, defaults to false. For example, in the English locale, using
+             * locale. If not defined, defaults to false. For example, for locale en_US, using
              * grouping with 1234.56 would yield "1,234.56".
              *
              * @since 1.2
@@ -3051,7 +3104,7 @@
     /**
      * Interface defining a dynamic string type.
      *
-     * <p> {@link DynamicString} string value is subject to being truncated if it's too long.
+     * <p>{@link DynamicString} string value is subject to being truncated if it's too long.
      *
      * @since 1.2
      */
@@ -3802,7 +3855,13 @@
             return toDynamicFloatProto().toByteArray();
         }
 
-        /** Creates a constant-valued {@link DynamicFloat}. */
+        /**
+         * Creates a constant-valued {@link DynamicFloat}.
+         *
+         * <p>If {@code Float.isNan(constant)} is true, the value will be invalid. And any
+         * expression that uses this {@link DynamicFloat} will have an invalid result (which will be
+         * delivered through {@link DynamicTypeValueReceiver<T>#onInvalidate()}.
+         */
         @NonNull
         static DynamicFloat constant(float constant) {
             return new FixedFloat.Builder().setValue(constant).build();
@@ -3907,6 +3966,11 @@
         /**
          * Returns a {@link DynamicInt32} which holds the largest integer value that is smaller than
          * or equal to this {@link DynamicFloat}, i.e. {@code int result = (int) Math.floor(this)}
+         *
+         * <p>If the float value is larger than {@link Integer#MAX_VALUE} or smaller than {@link
+         * Integer#MIN_VALUE}, the result of this operation will be invalid and any expression that
+         * uses the {@link DynamicInt32} will have an invalid result (which will be delivered
+         * through {@link DynamicTypeValueReceiver<T>#onInvalidate()}.
          */
         @NonNull
         default DynamicInt32 asInt() {
@@ -4467,8 +4531,8 @@
 
         /**
          * Returns a {@link DynamicString} that contains the formatted value of this {@link
-         * DynamicFloat} (with default formatting parameters). As an example, in the English locale,
-         * the following is equal to {@code DynamicString.constant("12.346")}
+         * DynamicFloat} (with default formatting parameters). As an example, for locale en_US, the
+         * following is equal to {@code DynamicString.constant("12.346")}
          *
          * <pre>
          *   DynamicFloat.constant(12.34567f).format();
@@ -4478,19 +4542,19 @@
          */
         @NonNull
         default DynamicString format() {
-            return FloatFormatter.with().buildForInput(this);
+            return new FloatFormatter.Builder().build().getFloatFormatOp(this);
         }
 
         /**
          * Returns a {@link DynamicString} that contains the formatted value of this {@link
-         * DynamicFloat}. As an example, in the English locale, the following is equal to {@code
+         * DynamicFloat}. As an example, for locale en_US, the following is equal to {@code
          * DynamicString.constant("0,012.34")}
          *
          * <pre>
          *   DynamicFloat.constant(12.345f)
          *       .format(
-         *           FloatFormatter.with().maxFractionDigits(2).minIntegerDigits(4)
-         *                         .groupingUsed(true));
+         *           new FloatFormatter.Builder().setMaxFractionDigits(2).setMinIntegerDigits(4)
+         *                             .setGroupingUsed(true).build());
          * </pre>
          *
          * The resulted {@link DynamicString} is subject to being truncated if it's too long.
@@ -4499,67 +4563,138 @@
          */
         @NonNull
         default DynamicString format(@NonNull FloatFormatter formatter) {
-            return formatter.buildForInput(this);
+            return formatter.getFloatFormatOp(this);
         }
 
         /** Allows formatting {@link DynamicFloat} into a {@link DynamicString}. */
         class FloatFormatter {
-            private final FloatFormatOp.Builder builder;
+            private final FloatFormatOp.Builder mFloatFormatOpBuilder;
+            private final FloatFormatOp mFloatFormatOp;
 
-            private FloatFormatter() {
-                builder = new FloatFormatOp.Builder();
-            }
-
-            /** Creates an instance of {@link FloatFormatter} with default configuration. */
-            @NonNull
-            public static FloatFormatter with() {
-                return new FloatFormatter();
-            }
-
-            /**
-             * Sets minimum number of fraction digits for the formatter. Defaults to zero if not
-             * specified. minimumFractionDigits must be <= maximumFractionDigits. If the condition
-             * is not satisfied, then minimumFractionDigits will be used for both fields.
-             */
-            @NonNull
-            public FloatFormatter minFractionDigits(@IntRange(from = 0) int minFractionDigits) {
-                builder.setMinFractionDigits(minFractionDigits);
-                return this;
-            }
-
-            /**
-             * Sets maximum number of fraction digits for the formatter. Defaults to three if not
-             * specified. minimumFractionDigits must be <= maximumFractionDigits. If the condition
-             * is not satisfied, then minimumFractionDigits will be used for both fields.
-             */
-            @NonNull
-            public FloatFormatter maxFractionDigits(@IntRange(from = 0) int maxFractionDigits) {
-                builder.setMaxFractionDigits(maxFractionDigits);
-                return this;
-            }
-
-            /**
-             * Sets minimum number of integer digits for the formatter. Defaults to one if not
-             * specified.
-             */
-            @NonNull
-            public FloatFormatter minIntegerDigits(@IntRange(from = 0) int minIntegerDigits) {
-                builder.setMinIntegerDigits(minIntegerDigits);
-                return this;
-            }
-
-            /**
-             * Sets whether grouping is used for the formatter. Defaults to false if not specified.
-             */
-            @NonNull
-            public FloatFormatter groupingUsed(boolean groupingUsed) {
-                builder.setGroupingUsed(groupingUsed);
-                return this;
+            FloatFormatter(FloatFormatOp.Builder floatFormatOpBuilder) {
+                mFloatFormatOpBuilder = floatFormatOpBuilder;
+                mFloatFormatOp = floatFormatOpBuilder.build();
             }
 
             @NonNull
-            FloatFormatOp buildForInput(@NonNull DynamicFloat dynamicFloat) {
-                return builder.setInput(dynamicFloat).build();
+            FloatFormatOp getFloatFormatOp(@NonNull DynamicFloat dynamicFloat) {
+                return mFloatFormatOpBuilder.setInput(dynamicFloat).build();
+            }
+
+            /** Returns the minimum number of digits allowed in the fraction portion of a number. */
+            @IntRange(from = 0)
+            public int getMinFractionDigits() {
+                return mFloatFormatOp.getMinFractionDigits();
+            }
+
+            /** Returns the maximum number of digits allowed in the fraction portion of a number. */
+            @IntRange(from = 0)
+            public int getMaxFractionDigits() {
+                return mFloatFormatOp.getMaxFractionDigits();
+            }
+
+            /** Returns the minimum number of digits allowed in the integer portion of a number. */
+            @IntRange(from = 0)
+            public int getMinIntegerDigits() {
+                return mFloatFormatOp.getMinIntegerDigits();
+            }
+
+            /** Returns whether digit grouping is used or not. */
+            public boolean isGroupingUsed() {
+                return mFloatFormatOp.getGroupingUsed();
+            }
+
+            /** Builder to create {@link FloatFormatter} objects. */
+            public static final class Builder {
+                private static final int MAX_INTEGER_PART_LENGTH = 15;
+                private static final int MAX_FRACTION_PART_LENGTH = 15;
+                final FloatFormatOp.Builder mBuilder;
+
+                public Builder() {
+                    mBuilder = new FloatFormatOp.Builder();
+                }
+
+                /**
+                 * Sets minimum number of fraction digits for the formatter. Defaults to zero if not
+                 * specified. minimumFractionDigits must be <= maximumFractionDigits. If the
+                 * condition is not satisfied, then minimumFractionDigits will be used for both
+                 * fields.
+                 */
+                @NonNull
+                public Builder setMinFractionDigits(@IntRange(from = 0) int minFractionDigits) {
+                    mBuilder.setMinFractionDigits(minFractionDigits);
+                    return this;
+                }
+
+                /**
+                 * Sets maximum number of fraction digits for the formatter. Defaults to three if
+                 * not specified. minimumFractionDigits must be <= maximumFractionDigits. If the
+                 * condition is not satisfied, then minimumFractionDigits will be used for both
+                 * fields.
+                 */
+                @NonNull
+                public Builder setMaxFractionDigits(@IntRange(from = 0) int maxFractionDigits) {
+                    mBuilder.setMaxFractionDigits(maxFractionDigits);
+                    return this;
+                }
+
+                /**
+                 * Sets minimum number of integer digits for the formatter. Defaults to one if not
+                 * specified. If minIntegerDigits is zero and the -1 < input < 1, the Integer
+                 * part will not appear.
+                 */
+                @NonNull
+                public Builder setMinIntegerDigits(@IntRange(from = 0) int minIntegerDigits) {
+                    mBuilder.setMinIntegerDigits(minIntegerDigits);
+                    return this;
+                }
+
+                /**
+                 * Sets whether grouping is used for the formatter. Defaults to false if not
+                 * specified. If grouping is used, digits will be grouped into digit groups using a
+                 * separator. Digit group size and used separator can vary in different
+                 * countries/regions. As an example, for locale en_US, the following is equal to
+                 * {@code * DynamicString.constant("1,234")}
+                 *
+                 * <pre>
+                 *   DynamicFloat.constant(1234)
+                 *       .format(
+                 *           new FloatFormatter.Builder()
+                 *                           .setGroupingUsed(true).build());
+                 * </pre>
+                 */
+                @NonNull
+                public Builder setGroupingUsed(boolean groupingUsed) {
+                    mBuilder.setGroupingUsed(groupingUsed);
+                    return this;
+                }
+
+                /** Builds an instance with values accumulated in this Builder. */
+                @NonNull
+                public FloatFormatter build() {
+                    FloatFormatOp op = mBuilder.build();
+                    throwIfExceedingMaxValue(
+                            "MinFractionDigits",
+                            op.getMinFractionDigits(),
+                            MAX_FRACTION_PART_LENGTH);
+                    throwIfExceedingMaxValue(
+                            "MaxFractionDigits",
+                            op.getMaxFractionDigits(),
+                            MAX_FRACTION_PART_LENGTH);
+                    throwIfExceedingMaxValue(
+                            "MinIntegerDigits", op.getMinIntegerDigits(), MAX_INTEGER_PART_LENGTH);
+                    return new FloatFormatter(mBuilder);
+                }
+
+                private static void throwIfExceedingMaxValue(
+                        String paramName, int value, int maxValue) {
+                    if (value > maxValue) {
+                        throw new IllegalArgumentException(
+                                String.format(
+                                        "%s (%d) is too large. Maximum value for %s is %d",
+                                        paramName, value, paramName, maxValue));
+                    }
+                }
             }
         }
 
@@ -5245,18 +5380,12 @@
             return new StateBoolSource.Builder().setSourceKey(stateKey).build();
         }
 
-        /** Returns a {@link DynamicBool} that has the same value as this {@link DynamicBool}. */
-        @NonNull
-        default DynamicBool isTrue() {
-            return this;
-        }
-
         /**
          * Returns a {@link DynamicBool} that has the opposite value of this {@link DynamicBool}.
          * i.e. {code result = !this}
          */
         @NonNull
-        default DynamicBool isFalse() {
+        default DynamicBool negate() {
             return new NotBoolOp.Builder().setInput(this).build();
         }
 
@@ -5682,6 +5811,170 @@
     }
 
     /**
+     * A conditional operator which yields a color depending on the boolean operand. This
+     * implements:
+     *
+     * <pre>{@code
+     * color result = condition ? value_if_true : value_if_false
+     * }</pre>
+     *
+     * @since 1.2
+     */
+    static final class ConditionalColorOp implements DynamicColor {
+        private final DynamicProto.ConditionalColorOp mImpl;
+        @Nullable private final Fingerprint mFingerprint;
+
+        ConditionalColorOp(
+                DynamicProto.ConditionalColorOp impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        /**
+         * Gets the condition to use.
+         *
+         * @since 1.2
+         */
+        @Nullable
+        public DynamicBool getCondition() {
+            if (mImpl.hasCondition()) {
+                return DynamicBuilders.dynamicBoolFromProto(mImpl.getCondition());
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Gets the color to yield if condition is true.
+         *
+         * @since 1.2
+         */
+        @Nullable
+        public DynamicColor getValueIfTrue() {
+            if (mImpl.hasValueIfTrue()) {
+                return DynamicBuilders.dynamicColorFromProto(mImpl.getValueIfTrue());
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Gets the color to yield if condition is false.
+         *
+         * @since 1.2
+         */
+        @Nullable
+        public DynamicColor getValueIfFalse() {
+            if (mImpl.hasValueIfFalse()) {
+                return DynamicBuilders.dynamicColorFromProto(mImpl.getValueIfFalse());
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Nullable
+        public Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public static ConditionalColorOp fromProto(
+                @NonNull DynamicProto.ConditionalColorOp proto, @Nullable Fingerprint fingerprint) {
+            return new ConditionalColorOp(proto, fingerprint);
+        }
+
+        @NonNull
+        static ConditionalColorOp fromProto(@NonNull DynamicProto.ConditionalColorOp proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        DynamicProto.ConditionalColorOp toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public DynamicProto.DynamicColor toDynamicColorProto() {
+            return DynamicProto.DynamicColor.newBuilder().setConditionalOp(mImpl).build();
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "ConditionalColorOp{"
+                    + "condition="
+                    + getCondition()
+                    + ", valueIfTrue="
+                    + getValueIfTrue()
+                    + ", valueIfFalse="
+                    + getValueIfFalse()
+                    + "}";
+        }
+
+        /** Builder for {@link ConditionalColorOp}. */
+        public static final class Builder implements DynamicColor.Builder {
+            private final DynamicProto.ConditionalColorOp.Builder mImpl =
+                    DynamicProto.ConditionalColorOp.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(-1961850082);
+
+            public Builder() {}
+
+            /**
+             * Sets the condition to use.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            public Builder setCondition(@NonNull DynamicBool condition) {
+                mImpl.setCondition(condition.toDynamicBoolProto());
+                mFingerprint.recordPropertyUpdate(
+                        1, checkNotNull(condition.getFingerprint()).aggregateValueAsInt());
+                return this;
+            }
+
+            /**
+             * Sets the color to yield if condition is true.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            public Builder setValueIfTrue(@NonNull DynamicColor valueIfTrue) {
+                mImpl.setValueIfTrue(valueIfTrue.toDynamicColorProto());
+                mFingerprint.recordPropertyUpdate(
+                        2, checkNotNull(valueIfTrue.getFingerprint()).aggregateValueAsInt());
+                return this;
+            }
+
+            /**
+             * Sets the color to yield if condition is false.
+             *
+             * @since 1.2
+             */
+            @NonNull
+            public Builder setValueIfFalse(@NonNull DynamicColor valueIfFalse) {
+                mImpl.setValueIfFalse(valueIfFalse.toDynamicColorProto());
+                mFingerprint.recordPropertyUpdate(
+                        3, checkNotNull(valueIfFalse.getFingerprint()).aggregateValueAsInt());
+                return this;
+            }
+
+            @Override
+            @NonNull
+            public ConditionalColorOp build() {
+                return new ConditionalColorOp(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
+    /**
      * Interface defining a dynamic color type.
      *
      * @since 1.2
@@ -5817,6 +6110,24 @@
             return new AnimatableDynamicColor.Builder().setInput(this).build();
         }
 
+        /**
+         * Bind the value of this {@link DynamicColor} to the result of a conditional expression.
+         * This will use the value given in either {@link ConditionScope#use} or {@link
+         * ConditionScopes.IfTrueScope#elseUse} depending on the value yielded from {@code
+         * condition}.
+         */
+        @NonNull
+        static ConditionScope<DynamicColor, Integer> onCondition(@NonNull DynamicBool condition) {
+            return new ConditionScopes.ConditionScope<>(
+                    (trueValue, falseValue) ->
+                            new ConditionalColorOp.Builder()
+                                    .setCondition(condition)
+                                    .setValueIfTrue(trueValue)
+                                    .setValueIfFalse(falseValue)
+                                    .build(),
+                    DynamicColor::constant);
+        }
+
         /** Get the fingerprint for this object or null if unknown. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Nullable
@@ -5851,6 +6162,9 @@
         if (proto.hasAnimatableDynamic()) {
             return AnimatableDynamicColor.fromProto(proto.getAnimatableDynamic());
         }
+        if (proto.hasConditionalOp()) {
+            return ConditionalColorOp.fromProto(proto.getConditionalOp());
+        }
         throw new IllegalStateException("Proto was not a recognised instance of DynamicColor");
     }
 
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index 8809e61..f9829cd 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -230,7 +230,8 @@
     }
 
     /**
-     * Gets the value.
+     * Gets the value. Note that a NaN value is considered invalid and any expression with this node
+     * will have an invalid value delivered via {@link DynamicTypeValueReceiver<T>#onInvalidate()}.
      *
      * @since 1.2
      */
@@ -284,8 +285,11 @@
 
       public Builder() {}
 
+
       /**
-       * Sets the value.
+       * Sets the value. Note that a NaN value is considered invalid and any expression with this
+       * node will have an invalid value delivered via
+       * {@link DynamicTypeValueReceiver<T>#onInvalidate()}.
        *
        * @since 1.2
        */
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
index 7c51d3e..b9bf7e3 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicBoolTest.java
@@ -98,14 +98,13 @@
   public void negateOpBool() {
     DynamicBool firstBool = DynamicBool.constant(true);
 
-    assertThat(firstBool.isTrue().toDynamicBoolProto()).isEqualTo(firstBool.toDynamicBoolProto());
-    assertThat(firstBool.isFalse().toDynamicBoolProto().getNotOp().getInput())
+    assertThat(firstBool.negate().toDynamicBoolProto().getNotOp().getInput())
         .isEqualTo(firstBool.toDynamicBoolProto());
   }
 
   @Test
   public void logicalToString() {
-    assertThat(DynamicBool.constant(true).isFalse().toString())
+    assertThat(DynamicBool.constant(true).negate().toString())
         .isEqualTo("NotBoolOp{input=FixedBool{value=true}}");
   }
 
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
index b178ff8..d00b7f3 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicFloatTest.java
@@ -17,7 +17,9 @@
 package androidx.wear.protolayout.expression;
 
 import static androidx.wear.protolayout.expression.AnimationParameterBuilders.REPEAT_MODE_REVERSE;
+
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertThrows;
 
 import androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters;
@@ -26,207 +28,225 @@
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicFloatTest {
-  private static final String STATE_KEY = "state-key";
-  private static final float CONSTANT_VALUE = 42.42f;
-  private static final AnimationSpec SPEC =
-      new AnimationSpec.Builder()
-          .setAnimationParameters(
-                  new AnimationParameters.Builder()
-                          .setDurationMillis(2)
-                          .setDelayMillis(1)
-                          .build())
-          .setRepeatable(
-              new AnimationParameterBuilders.Repeatable.Builder()
-                  .setRepeatMode(REPEAT_MODE_REVERSE)
-                  .setIterations(10)
-                  .build())
-          .build();
+    private static final String STATE_KEY = "state-key";
+    private static final float CONSTANT_VALUE = 42.42f;
+    private static final AnimationSpec SPEC =
+            new AnimationSpec.Builder()
+                    .setAnimationParameters(
+                            new AnimationParameters.Builder()
+                                    .setDurationMillis(2)
+                                    .setDelayMillis(1)
+                                    .build())
+                    .setRepeatable(
+                            new AnimationParameterBuilders.Repeatable.Builder()
+                                    .setRepeatMode(REPEAT_MODE_REVERSE)
+                                    .setIterations(10)
+                                    .build())
+                    .build();
 
-  @Test
-  public void constantFloat() {
-    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    @Test
+    public void constantFloat() {
+        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-    assertThat(constantFloat.toDynamicFloatProto().getFixed().getValue())
-        .isWithin(0.0001f)
-        .of(CONSTANT_VALUE);
-  }
+        assertThat(constantFloat.toDynamicFloatProto().getFixed().getValue())
+                .isWithin(0.0001f)
+                .of(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void constantToString() {
-    assertThat(DynamicFloat.constant(1f).toString()).isEqualTo("FixedFloat{value=1.0}");
-  }
+    @Test
+    public void constantToString() {
+        assertThat(DynamicFloat.constant(1f).toString()).isEqualTo("FixedFloat{value=1.0}");
+    }
 
-  @Test
-  public void stateEntryValueFloat() {
-    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+    @Test
+    public void stateEntryValueFloat() {
+        DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
 
-    assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey())
-        .isEqualTo(STATE_KEY);
-  }
+        assertThat(stateFloat.toDynamicFloatProto().getStateSource().getSourceKey())
+                .isEqualTo(STATE_KEY);
+    }
 
-  @Test
-  public void stateToString() {
-    assertThat(DynamicFloat.fromState("key").toString())
-        .isEqualTo("StateFloatSource{sourceKey=key}");
-  }
+    @Test
+    public void stateToString() {
+        assertThat(DynamicFloat.fromState("key").toString())
+                .isEqualTo("StateFloatSource{sourceKey=key}");
+    }
 
-  @Test
-  public void constantFloat_asInt() {
-    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    @Test
+    public void constantFloat_asInt() {
+        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-    DynamicInt32 dynamicInt32 = constantFloat.asInt();
+        DynamicInt32 dynamicInt32 = constantFloat.asInt();
 
-    assertThat(dynamicInt32.toDynamicInt32Proto().getFloatToInt().getInput().getFixed().getValue())
-        .isWithin(0.0001f)
-        .of(CONSTANT_VALUE);
-  }
+        assertThat(
+                        dynamicInt32
+                                .toDynamicInt32Proto()
+                                .getFloatToInt()
+                                .getInput()
+                                .getFixed()
+                                .getValue())
+                .isWithin(0.0001f)
+                .of(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void constantFloat_asIntToString() {
-    assertThat(DynamicFloat.constant(1f).asInt().toString())
-        .isEqualTo("FloatToInt32Op{input=FixedFloat{value=1.0}, roundMode=1}");
-  }
+    @Test
+    public void constantFloat_asIntToString() {
+        assertThat(DynamicFloat.constant(1f).asInt().toString())
+                .isEqualTo("FloatToInt32Op{input=FixedFloat{value=1.0}, roundMode=1}");
+    }
 
-  @Test
-  public void formatFloat_defaultParameters() {
-    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+    @Test
+    public void formatFloat_defaultParameters() {
+        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
 
-    DynamicString defaultFormat = constantFloat.format();
+        DynamicString defaultFormat = constantFloat.format();
 
-    DynamicProto.FloatFormatOp floatFormatOp =
-        defaultFormat.toDynamicStringProto().getFloatFormatOp();
-    assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
-    assertThat(floatFormatOp.getGroupingUsed()).isFalse();
-    assertThat(floatFormatOp.hasMaxFractionDigits()).isFalse();
-    assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(0);
-    assertThat(floatFormatOp.hasMinIntegerDigits()).isFalse();
-  }
+        DynamicProto.FloatFormatOp floatFormatOp =
+                defaultFormat.toDynamicStringProto().getFloatFormatOp();
+        assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
+        assertThat(floatFormatOp.getGroupingUsed()).isFalse();
+        assertThat(floatFormatOp.hasMaxFractionDigits()).isFalse();
+        assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(0);
+        assertThat(floatFormatOp.hasMinIntegerDigits()).isFalse();
+    }
 
-  @Test
-  public void formatFloat_customFormatter() {
-    DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
-    boolean groupingUsed = true;
-    int minFractionDigits = 1;
-    int maxFractionDigits = 2;
-    int minIntegerDigits = 3;
-    DynamicFloat.FloatFormatter floatFormatter =
-        DynamicFloat.FloatFormatter.with()
-            .minFractionDigits(minFractionDigits)
-            .maxFractionDigits(maxFractionDigits)
-            .minIntegerDigits(minIntegerDigits)
-            .groupingUsed(groupingUsed);
+    @Test
+    public void formatFloat_customFormatter() {
+        DynamicFloat constantFloat = DynamicFloat.constant(CONSTANT_VALUE);
+        boolean groupingUsed = true;
+        int minFractionDigits = 1;
+        int maxFractionDigits = 2;
+        int minIntegerDigits = 3;
+        DynamicFloat.FloatFormatter floatFormatter =
+                new DynamicFloat.FloatFormatter.Builder()
+                        .setMinFractionDigits(minFractionDigits)
+                        .setMaxFractionDigits(maxFractionDigits)
+                        .setMinIntegerDigits(minIntegerDigits)
+                        .setGroupingUsed(groupingUsed)
+                        .build();
 
-    DynamicString customFormat = constantFloat.format(floatFormatter);
+        DynamicString customFormat = constantFloat.format(floatFormatter);
 
-    DynamicProto.FloatFormatOp floatFormatOp =
-        customFormat.toDynamicStringProto().getFloatFormatOp();
-    assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
-    assertThat(floatFormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
-    assertThat(floatFormatOp.getMaxFractionDigits()).isEqualTo(maxFractionDigits);
-    assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(minFractionDigits);
-    assertThat(floatFormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
-  }
+        DynamicProto.FloatFormatOp floatFormatOp =
+                customFormat.toDynamicStringProto().getFloatFormatOp();
+        assertThat(floatFormatOp.getInput()).isEqualTo(constantFloat.toDynamicFloatProto());
+        assertThat(floatFormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
+        assertThat(floatFormatOp.getMaxFractionDigits()).isEqualTo(maxFractionDigits);
+        assertThat(floatFormatOp.getMinFractionDigits()).isEqualTo(minFractionDigits);
+        assertThat(floatFormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
+    }
 
-  @Test
-  public void formatToString() {
-    assertThat(
-            DynamicFloat.constant(1f)
-                .format(
-                    DynamicFloat.FloatFormatter.with()
-                        .maxFractionDigits(2)
-                        .minFractionDigits(3)
-                        .minIntegerDigits(4)
-                        .groupingUsed(true))
-                .toString())
-        .isEqualTo(
-            "FloatFormatOp{input=FixedFloat{value=1.0}, maxFractionDigits=2, "
-                + "minFractionDigits=3, minIntegerDigits=4, groupingUsed=true}");
-  }
+    @Test
+    public void formatToString() {
+        assertThat(
+                        DynamicFloat.constant(1f)
+                                .format(
+                                        new DynamicFloat.FloatFormatter.Builder()
+                                                .setMaxFractionDigits(2)
+                                                .setMinFractionDigits(3)
+                                                .setMinIntegerDigits(4)
+                                                .setGroupingUsed(true)
+                                                .build())
+                                .toString())
+                .isEqualTo(
+                        "FloatFormatOp{input=FixedFloat{value=1.0}, maxFractionDigits=2, "
+                                + "minFractionDigits=3, minIntegerDigits=4, groupingUsed=true}");
+    }
 
-  @Test
-  public void rangeAnimatedFloat() {
-    float startFloat = 100f;
-    float endFloat = 200f;
+    @Test
+    public void rangeAnimatedFloat() {
+        float startFloat = 100f;
+        float endFloat = 200f;
 
-    DynamicFloat animatedFloat = DynamicFloat.animate(startFloat, endFloat);
-    DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
+        DynamicFloat animatedFloat = DynamicFloat.animate(startFloat, endFloat);
+        DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(startFloat, endFloat, SPEC);
 
-    assertThat(animatedFloat.toDynamicFloatProto().getAnimatableFixed().hasAnimationSpec())
-        .isFalse();
-    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getFromValue())
-        .isEqualTo(startFloat);
-    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getToValue())
-        .isEqualTo(endFloat);
-    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getAnimationSpec())
-        .isEqualTo(SPEC.toProto());
-  }
+        assertThat(animatedFloat.toDynamicFloatProto().getAnimatableFixed().hasAnimationSpec())
+                .isFalse();
+        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getFromValue())
+                .isEqualTo(startFloat);
+        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableFixed().getToValue())
+                .isEqualTo(endFloat);
+        assertThat(
+                        animatedFloatWithSpec
+                                .toDynamicFloatProto()
+                                .getAnimatableFixed()
+                                .getAnimationSpec())
+                .isEqualTo(SPEC.toProto());
+    }
 
-  @Test
-  public void rangeAnimatedToString() {
-    assertThat(
-            DynamicFloat.animate(
-                    /* start= */ 1f,
-                    /* end= */ 2f,
-                    new AnimationSpec.Builder().build())
-                .toString())
-        .isEqualTo(
-            "AnimatableFixedFloat{fromValue=1.0, toValue=2.0, animationSpec=AnimationSpec{"
-                + "animationParameters=null, repeatable=null}}");
-  }
+    @Test
+    public void rangeAnimatedToString() {
+        assertThat(
+                        DynamicFloat.animate(
+                                        /* start= */ 1f,
+                                        /* end= */ 2f,
+                                        new AnimationSpec.Builder().build())
+                                .toString())
+                .isEqualTo(
+                        "AnimatableFixedFloat{fromValue=1.0, toValue=2.0,"
+                                + " animationSpec=AnimationSpec{animationParameters=null,"
+                                + " repeatable=null}}");
+    }
 
-  @Test
-  public void stateAnimatedFloat() {
-    DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
+    @Test
+    public void stateAnimatedFloat() {
+        DynamicFloat stateFloat = DynamicFloat.fromState(STATE_KEY);
 
-    DynamicFloat animatedFloat = DynamicFloat.animate(STATE_KEY);
-    DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(STATE_KEY, SPEC);
+        DynamicFloat animatedFloat = DynamicFloat.animate(STATE_KEY);
+        DynamicFloat animatedFloatWithSpec = DynamicFloat.animate(STATE_KEY, SPEC);
 
-    assertThat(animatedFloat.toDynamicFloatProto().getAnimatableDynamic().hasAnimationSpec())
-        .isFalse();
-    assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableDynamic().getInput())
-        .isEqualTo(stateFloat.toDynamicFloatProto());
-    assertThat(
-            animatedFloatWithSpec.toDynamicFloatProto().getAnimatableDynamic().getAnimationSpec()
-    ).isEqualTo(SPEC.toProto());
-    assertThat(animatedFloat.toDynamicFloatProto())
-        .isEqualTo(stateFloat.animate().toDynamicFloatProto());
-  }
+        assertThat(animatedFloat.toDynamicFloatProto().getAnimatableDynamic().hasAnimationSpec())
+                .isFalse();
+        assertThat(animatedFloatWithSpec.toDynamicFloatProto().getAnimatableDynamic().getInput())
+                .isEqualTo(stateFloat.toDynamicFloatProto());
+        assertThat(
+                        animatedFloatWithSpec
+                                .toDynamicFloatProto()
+                                .getAnimatableDynamic()
+                                .getAnimationSpec())
+                .isEqualTo(SPEC.toProto());
+        assertThat(animatedFloat.toDynamicFloatProto())
+                .isEqualTo(stateFloat.animate().toDynamicFloatProto());
+    }
 
-  @Test
-  public void stateAnimatedToString() {
-    assertThat(
-            DynamicFloat.animate(
-                    /* stateKey= */ "key",
-                    new AnimationSpec.Builder()
-                            .setAnimationParameters(
-                                    new AnimationParameters.Builder()
-                                            .setDelayMillis(1)
-                                            .build())
-                            .build())
-                .toString())
-        .isEqualTo(
-            "AnimatableDynamicFloat{"
-                + "input=StateFloatSource{sourceKey=key}, animationSpec=AnimationSpec{"
-                + "animationParameters=AnimationParameters{durationMillis=0, easing=null, "
-                + "delayMillis=1}, repeatable=null}}");
-  }
+    @Test
+    public void stateAnimatedToString() {
+        assertThat(
+                        DynamicFloat.animate(
+                                        /* stateKey= */ "key",
+                                        new AnimationSpec.Builder()
+                                                .setAnimationParameters(
+                                                        new AnimationParameters.Builder()
+                                                                .setDelayMillis(1)
+                                                                .build())
+                                                .build())
+                                .toString())
+                .isEqualTo(
+                        "AnimatableDynamicFloat{input=StateFloatSource{sourceKey=key},"
+                                + " animationSpec=AnimationSpec{animationParameters"
+                                + "=AnimationParameters{durationMillis=0,"
+                                + " easing=null, delayMillis=1}, repeatable=null}}");
+    }
 
-  @Test
-  public void validProto() {
-    DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
-    DynamicFloat to = DynamicFloat.fromByteArray(from.toDynamicFloatByteArray());
+    @Test
+    public void validProto() {
+        DynamicFloat from = DynamicFloat.constant(CONSTANT_VALUE);
+        DynamicFloat to = DynamicFloat.fromByteArray(from.toDynamicFloatByteArray());
 
-    assertThat(to.toDynamicFloatProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
-  }
+        assertThat(to.toDynamicFloatProto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void invalidProto() {
-    assertThrows(IllegalArgumentException.class, () -> DynamicFloat.fromByteArray(new byte[] {1}));
-  }
+    @Test
+    public void invalidProto() {
+        assertThrows(
+                IllegalArgumentException.class, () -> DynamicFloat.fromByteArray(new byte[] {1}));
+    }
 }
diff --git a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
index 416e35b..f87e5b5 100644
--- a/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
+++ b/wear/protolayout/protolayout-expression/src/test/java/androidx/wear/protolayout/expression/DynamicInt32Test.java
@@ -17,120 +17,130 @@
 package androidx.wear.protolayout.expression;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertThrows;
 
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.proto.DynamicProto;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
 
 @RunWith(RobolectricTestRunner.class)
 public final class DynamicInt32Test {
-  private static final String STATE_KEY = "state-key";
-  private static final int CONSTANT_VALUE = 42;
+    private static final String STATE_KEY = "state-key";
+    private static final int CONSTANT_VALUE = 42;
 
-  @Test
-  public void constantInt32() {
-    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    @Test
+    public void constantInt32() {
+        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-    assertThat(constantInt32.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
-  }
+        assertThat(constantInt32.toDynamicInt32Proto().getFixed().getValue())
+                .isEqualTo(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void constantToString() {
-    assertThat(DynamicInt32.constant(1).toString()).isEqualTo("FixedInt32{value=1}");
-  }
+    @Test
+    public void constantToString() {
+        assertThat(DynamicInt32.constant(1).toString()).isEqualTo("FixedInt32{value=1}");
+    }
 
-  @Test
-  public void stateEntryValueInt32() {
-    DynamicInt32 stateInt32 = DynamicInt32.fromState(STATE_KEY);
+    @Test
+    public void stateEntryValueInt32() {
+        DynamicInt32 stateInt32 = DynamicInt32.fromState(STATE_KEY);
 
-    assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey())
-        .isEqualTo(STATE_KEY);
-  }
+        assertThat(stateInt32.toDynamicInt32Proto().getStateSource().getSourceKey())
+                .isEqualTo(STATE_KEY);
+    }
 
-  @Test
-  public void stateToString() {
-    assertThat(DynamicInt32.fromState("key").toString())
-        .isEqualTo("StateInt32Source{sourceKey=key}");
-  }
+    @Test
+    public void stateToString() {
+        assertThat(DynamicInt32.fromState("key").toString())
+                .isEqualTo("StateInt32Source{sourceKey=key}");
+    }
 
-  @Test
-  public void constantInt32_asFloat() {
-    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    @Test
+    public void constantInt32_asFloat() {
+        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-    DynamicFloat dynamicFloat = constantInt32.asFloat();
+        DynamicFloat dynamicFloat = constantInt32.asFloat();
 
-    assertThat(
-            dynamicFloat
-                .toDynamicFloatProto()
-                .getInt32ToFloatOperation()
-                .getInput()
-                .getFixed()
-                .getValue())
-        .isEqualTo(CONSTANT_VALUE);
-  }
+        assertThat(
+                        dynamicFloat
+                                .toDynamicFloatProto()
+                                .getInt32ToFloatOperation()
+                                .getInput()
+                                .getFixed()
+                                .getValue())
+                .isEqualTo(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void constantInt32_asFloatToString() {
-    assertThat(DynamicInt32.constant(1).asFloat().toString())
-        .isEqualTo("Int32ToFloatOp{input=FixedInt32{value=1}}");
-  }
+    @Test
+    public void constantInt32_asFloatToString() {
+        assertThat(DynamicInt32.constant(1).asFloat().toString())
+                .isEqualTo("Int32ToFloatOp{input=FixedInt32{value=1}}");
+    }
 
-  @Test
-  public void formatInt32_defaultParameters() {
-    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+    @Test
+    public void formatInt32_defaultParameters() {
+        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
 
-    DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
+        DynamicBuilders.DynamicString defaultFormat = constantInt32.format();
 
-    DynamicProto.Int32FormatOp int32FormatOp =
-        defaultFormat.toDynamicStringProto().getInt32FormatOp();
-    assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
-    assertThat(int32FormatOp.getGroupingUsed()).isFalse();
-    assertThat(int32FormatOp.hasMinIntegerDigits()).isFalse();
-  }
+        DynamicProto.Int32FormatOp int32FormatOp =
+                defaultFormat.toDynamicStringProto().getInt32FormatOp();
+        assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
+        assertThat(int32FormatOp.getGroupingUsed()).isFalse();
+        assertThat(int32FormatOp.hasMinIntegerDigits()).isFalse();
+    }
 
-  @Test
-  public void formatInt32_customFormatter() {
-    DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
-    boolean groupingUsed = true;
-    int minIntegerDigits = 3;
-    DynamicInt32.IntFormatter intFormatter =
-        DynamicInt32.IntFormatter.with()
-            .minIntegerDigits(minIntegerDigits)
-            .groupingUsed(groupingUsed);
+    @Test
+    public void formatInt32_customFormatter() {
+        DynamicInt32 constantInt32 = DynamicInt32.constant(CONSTANT_VALUE);
+        boolean groupingUsed = true;
+        int minIntegerDigits = 3;
+        DynamicInt32.IntFormatter intFormatter =
+                new DynamicInt32.IntFormatter.Builder()
+                        .setMinIntegerDigits(minIntegerDigits)
+                        .setGroupingUsed(groupingUsed)
+                        .build();
 
-    DynamicBuilders.DynamicString customFormat = constantInt32.format(intFormatter);
+        DynamicBuilders.DynamicString customFormat = constantInt32.format(intFormatter);
 
-    DynamicProto.Int32FormatOp int32FormatOp =
-        customFormat.toDynamicStringProto().getInt32FormatOp();
-    assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
-    assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
-    assertThat(int32FormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
-  }
+        DynamicProto.Int32FormatOp int32FormatOp =
+                customFormat.toDynamicStringProto().getInt32FormatOp();
+        assertThat(int32FormatOp.getInput()).isEqualTo(constantInt32.toDynamicInt32Proto());
+        assertThat(int32FormatOp.getGroupingUsed()).isEqualTo(groupingUsed);
+        assertThat(int32FormatOp.getMinIntegerDigits()).isEqualTo(minIntegerDigits);
+    }
 
-  @Test
-  public void formatToString() {
-    assertThat(
-            DynamicInt32.constant(1)
-                .format(DynamicInt32.IntFormatter.with().minIntegerDigits(2).groupingUsed(true))
-                .toString())
-        .isEqualTo(
-            "Int32FormatOp{input=FixedInt32{value=1}, minIntegerDigits=2, groupingUsed=true}");
-  }
+    @Test
+    public void formatToString() {
+        assertThat(
+                        DynamicInt32.constant(1)
+                                .format(
+                                        new DynamicInt32.IntFormatter.Builder()
+                                                .setMinIntegerDigits(2)
+                                                .setGroupingUsed(true)
+                                                .build())
+                                .toString())
+                .isEqualTo(
+                        "Int32FormatOp{input=FixedInt32{value=1}, minIntegerDigits=2,"
+                            + " groupingUsed=true}");
+    }
 
-  @Test
-  public void validProto() {
-    DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
-    DynamicInt32 to = DynamicInt32.fromByteArray(from.toDynamicInt32ByteArray());
+    @Test
+    public void validProto() {
+        DynamicInt32 from = DynamicInt32.constant(CONSTANT_VALUE);
+        DynamicInt32 to = DynamicInt32.fromByteArray(from.toDynamicInt32ByteArray());
 
-    assertThat(to.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
-  }
+        assertThat(to.toDynamicInt32Proto().getFixed().getValue()).isEqualTo(CONSTANT_VALUE);
+    }
 
-  @Test
-  public void invalidProto() {
-    assertThrows(IllegalArgumentException.class, () -> DynamicInt32.fromByteArray(new byte[] {1}));
-  }
+    @Test
+    public void invalidProto() {
+        assertThrows(
+                IllegalArgumentException.class, () -> DynamicInt32.fromByteArray(new byte[] {1}));
+    }
 }
diff --git a/wear/protolayout/protolayout-material/build.gradle b/wear/protolayout/protolayout-material/build.gradle
index 8d0c69d7..042384d 100644
--- a/wear/protolayout/protolayout-material/build.gradle
+++ b/wear/protolayout/protolayout-material/build.gradle
@@ -25,9 +25,8 @@
 dependencies {
     annotationProcessor(libs.nullaway)
     api("androidx.annotation:annotation:1.2.0")
-    implementation(project(":wear:protolayout:protolayout"))
+    api(project(":wear:protolayout:protolayout"))
     implementation(project(":wear:protolayout:protolayout-proto"))
-    implementation(project(":wear:protolayout:protolayout-expression"))
     implementation("androidx.annotation:annotation-experimental:1.3.0")
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.testCore)
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
index 67d6ff8..23ab73a 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/dynamic.proto
@@ -507,6 +507,20 @@
   AnimationSpec animation_spec = 3;
 }
 
+// A conditional operator which yields a color depending on the boolean
+// operand. This implements:
+// ```color result = condition ? value_if_true : value_if_false```
+message ConditionalColorOp {
+    // The condition to use.
+  DynamicBool condition = 1;
+
+  // The color to yield if condition is true.
+  DynamicColor value_if_true = 2;
+
+  // The color to yield if condition is false.
+  DynamicColor value_if_false = 3;
+}
+
 // A dynamic color type.
 message DynamicColor {
   oneof inner {
@@ -514,6 +528,7 @@
     StateColorSource state_source = 2;
     AnimatableFixedColor animatable_fixed = 3;
     AnimatableDynamicColor animatable_dynamic = 4;
+    ConditionalColorOp conditional_op = 5;
   }
 }
 
diff --git a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
index c9e3f38..fe23db4 100644
--- a/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
+++ b/wear/protolayout/protolayout-proto/src/main/proto/layout.proto
@@ -142,6 +142,15 @@
   TextOverflow value = 1;
 }
 
+// Parameters for Marquee animation. Only applies for TEXT_OVERFLOW_MARQUEE.
+message MarqueeParameters {
+  oneof optional_iterations {
+    // The number of times to repeat the Marquee animation. Set to -1 to repeat
+    // indefinitely. Defaults to repeat indefinitely.
+    int32 iterations = 1;
+  }
+}
+
 // An Android platform specific text style configuration options for styling and
 // compatibility.
 message AndroidTextStyle {
@@ -180,6 +189,10 @@
   // be truncated. If not defined, defaults to TEXT_OVERFLOW_TRUNCATE.
   TextOverflowProp overflow = 6;
 
+  // Parameters for Marquee animation. Only applies when overflow is
+  // TEXT_OVERFLOW_MARQUEE.
+  MarqueeParameters marquee_parameters = 9;
+
   // The explicit height between lines of text. This is equivalent to the
   // vertical distance between subsequent baselines. If not specified, defaults
   // the font's recommended interline spacing.
@@ -387,6 +400,10 @@
   // TEXT_OVERFLOW_TRUNCATE.
   TextOverflowProp overflow = 5;
 
+  // Parameters for Marquee animation. Only applies when overflow is
+  // TEXT_OVERFLOW_MARQUEE.
+  MarqueeParameters marquee_parameters = 8;
+
   // Extra spacing to add between each line. This will apply to all
   // spans regardless of their font size. This is in addition to original
   // line heights. Note that this won't add any additional space before the
diff --git a/wear/protolayout/protolayout-renderer/build.gradle b/wear/protolayout/protolayout-renderer/build.gradle
index e7ea4bc1..f449584 100644
--- a/wear/protolayout/protolayout-renderer/build.gradle
+++ b/wear/protolayout/protolayout-renderer/build.gradle
@@ -31,7 +31,7 @@
 
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
     implementation(project(":wear:protolayout:protolayout-expression"))
-    implementation(project(":wear:protolayout:protolayout-expression-pipeline"))
+    api(project(":wear:protolayout:protolayout-expression-pipeline"))
     implementation "androidx.concurrent:concurrent-futures:1.1.0"
     implementation("androidx.core:core:1.7.0")
     implementation("androidx.vectordrawable:vectordrawable-seekable:1.0.0-beta01")
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index bb1546e2..625a0d7 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 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.
@@ -126,6 +126,7 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.Image;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.protolayout.proto.LayoutElementProto.MarqueeParameters;
 import androidx.wear.protolayout.proto.LayoutElementProto.Row;
 import androidx.wear.protolayout.proto.LayoutElementProto.Spacer;
 import androidx.wear.protolayout.proto.LayoutElementProto.Span;
@@ -2105,11 +2106,16 @@
                         .applyPendingChildLayoutParams(layoutParams));
     }
 
-    private static void applyTextOverflow(TextView textView, TextOverflowProp overflow) {
+    private static void applyTextOverflow(
+            TextView textView, TextOverflowProp overflow, MarqueeParameters marqueeParameters) {
         textView.setEllipsize(textTruncationToEllipsize(overflow));
         if (overflow.getValue() == TextOverflow.TEXT_OVERFLOW_MARQUEE
                 && textView.getMaxLines() == 1) {
-            textView.setMarqueeRepeatLimit(-1); // Repeat indefinitely.
+            int marqueeIterations =
+                    marqueeParameters.hasIterations()
+                            ? marqueeParameters.getIterations()
+                            : -1; // Defaults to repeat indefinitely (-1).
+            textView.setMarqueeRepeatLimit(marqueeIterations);
             textView.setSelected(true);
             textView.setSingleLine();
             textView.setHorizontalFadingEdgeEnabled(true);
@@ -2158,7 +2164,7 @@
         } else {
             textView.setMaxLines(TEXT_MAX_LINES_DEFAULT);
         }
-        applyTextOverflow(textView, text.getOverflow());
+        applyTextOverflow(textView, text.getOverflow(), text.getMarqueeParameters());
 
         // Setting colours **must** go after setting the Text Appearance, otherwise it will get
         // immediately overridden.
@@ -2966,7 +2972,7 @@
         } else {
             tv.setMaxLines(TEXT_MAX_LINES_DEFAULT);
         }
-        applyTextOverflow(tv, spannable.getOverflow());
+        applyTextOverflow(tv, spannable.getOverflow(), spannable.getMarqueeParameters());
 
         if (spannable.hasLineHeight()) {
             // We use a Span here instead of just calling TextViewCompat#setLineHeight.
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index 3aa3e5a..5a20188 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -131,6 +131,7 @@
 import androidx.wear.protolayout.proto.LayoutElementProto.Image;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.LayoutElementProto.LayoutElement;
+import androidx.wear.protolayout.proto.LayoutElementProto.MarqueeParameters;
 import androidx.wear.protolayout.proto.LayoutElementProto.Row;
 import androidx.wear.protolayout.proto.LayoutElementProto.Spacer;
 import androidx.wear.protolayout.proto.LayoutElementProto.Span;
@@ -1805,6 +1806,7 @@
         expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
         expect.that(tv.isSelected()).isTrue();
         expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
+        expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value.
         if (VERSION.SDK_INT >= VERSION_CODES.Q) {
             expect.that(tv.isSingleLine()).isTrue();
         }
@@ -1837,6 +1839,37 @@
     }
 
     @Test
+    public void inflate_textView_marqueeAnimation_repeatLimit() {
+        String textContents = "Marquee Animation";
+        int marqueeIterations = 5;
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setText(
+                                Text.newBuilder()
+                                        .setText(string(textContents))
+                                        .setMaxLines(Int32Prop.newBuilder().setValue(1))
+                                        .setOverflow(
+                                                TextOverflowProp.newBuilder()
+                                                        .setValue(
+                                                                TextOverflow.TEXT_OVERFLOW_MARQUEE)
+                                                        .build())
+                                        .setMarqueeParameters(
+                                                MarqueeParameters.newBuilder()
+                                                        .setIterations(marqueeIterations)))
+                        .build();
+
+        FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+        TextView tv = (TextView) rootLayout.getChildAt(0);
+        expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
+        expect.that(tv.isSelected()).isTrue();
+        expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
+        expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations);
+        if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+            expect.that(tv.isSingleLine()).isTrue();
+        }
+    }
+
+    @Test
     public void inflate_spannable_marqueeAnimation() {
         String text = "Marquee Animation";
         LayoutElement root =
@@ -1860,6 +1893,41 @@
         expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
         expect.that(tv.isSelected()).isTrue();
         expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
+        expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(-1); // Default value.
+        if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+            expect.that(tv.isSingleLine()).isTrue();
+        }
+    }
+
+    @Test
+    public void inflate_spannable_marqueeAnimation_repeatLimit() {
+        String text = "Marquee Animation";
+        int marqueeIterations = 5;
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setSpannable(
+                                Spannable.newBuilder()
+                                        .addSpans(
+                                                Span.newBuilder()
+                                                        .setText(
+                                                                SpanText.newBuilder()
+                                                                        .setText(string(text))))
+                                        .setOverflow(
+                                                TextOverflowProp.newBuilder()
+                                                        .setValue(
+                                                                TextOverflow.TEXT_OVERFLOW_MARQUEE))
+                                        .setMarqueeParameters(
+                                                MarqueeParameters.newBuilder()
+                                                        .setIterations(marqueeIterations))
+                                        .setMaxLines(Int32Prop.newBuilder().setValue(1)))
+                        .build();
+
+        FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
+        TextView tv = (TextView) rootLayout.getChildAt(0);
+        expect.that(tv.getEllipsize()).isEqualTo(TruncateAt.MARQUEE);
+        expect.that(tv.isSelected()).isTrue();
+        expect.that(tv.isHorizontalFadingEdgeEnabled()).isTrue();
+        expect.that(tv.getMarqueeRepeatLimit()).isEqualTo(marqueeIterations);
         if (VERSION.SDK_INT >= VERSION_CODES.Q) {
             expect.that(tv.isSingleLine()).isTrue();
         }
diff --git a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
index 2c913e6..56d09d9 100644
--- a/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
+++ b/wear/protolayout/protolayout/api/public_plus_experimental_current.txt
@@ -351,6 +351,7 @@
     field public static final int TEXT_ALIGN_START = 1; // 0x1
     field public static final int TEXT_ALIGN_UNDEFINED = 0; // 0x0
     field public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2; // 0x2
+    field @androidx.wear.protolayout.expression.ProtoLayoutExperimental public static final int TEXT_OVERFLOW_MARQUEE = 3; // 0x3
     field public static final int TEXT_OVERFLOW_TRUNCATE = 1; // 0x1
     field public static final int TEXT_OVERFLOW_UNDEFINED = 0; // 0x0
     field public static final int VERTICAL_ALIGN_BOTTOM = 3; // 0x3
@@ -734,6 +735,7 @@
 
   public static final class LayoutElementBuilders.Spannable implements androidx.wear.protolayout.LayoutElementBuilders.LayoutElement {
     method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method @IntRange(from=0xffffffff) @androidx.wear.protolayout.expression.ProtoLayoutExperimental public int getMarqueeIterations();
     method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
     method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
     method public androidx.wear.protolayout.LayoutElementBuilders.HorizontalAlignmentProp? getMultilineAlignment();
@@ -746,6 +748,7 @@
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder addSpan(androidx.wear.protolayout.LayoutElementBuilders.Span);
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable build();
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMarqueeIterations(@IntRange(from=0xffffffff) int);
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setMaxLines(@IntRange(from=1) int);
     method public androidx.wear.protolayout.LayoutElementBuilders.Spannable.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
@@ -760,6 +763,7 @@
     method public androidx.wear.protolayout.LayoutElementBuilders.FontStyle? getFontStyle();
     method public androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint? getLayoutConstraintsForDynamicText();
     method public androidx.wear.protolayout.DimensionBuilders.SpProp? getLineHeight();
+    method @IntRange(from=0xffffffff) @androidx.wear.protolayout.expression.ProtoLayoutExperimental public int getMarqueeIterations();
     method public androidx.wear.protolayout.TypeBuilders.Int32Prop? getMaxLines();
     method public androidx.wear.protolayout.ModifiersBuilders.Modifiers? getModifiers();
     method public androidx.wear.protolayout.LayoutElementBuilders.TextAlignmentProp? getMultilineAlignment();
@@ -774,6 +778,7 @@
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setFontStyle(androidx.wear.protolayout.LayoutElementBuilders.FontStyle);
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setLayoutConstraintsForDynamicText(androidx.wear.protolayout.TypeBuilders.StringLayoutConstraint);
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setLineHeight(androidx.wear.protolayout.DimensionBuilders.SpProp);
+    method @androidx.wear.protolayout.expression.ProtoLayoutExperimental public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMarqueeIterations(@IntRange(from=0xffffffff) int);
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(androidx.wear.protolayout.TypeBuilders.Int32Prop);
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setMaxLines(@IntRange(from=1) int);
     method public androidx.wear.protolayout.LayoutElementBuilders.Text.Builder setModifiers(androidx.wear.protolayout.ModifiersBuilders.Modifiers);
diff --git a/wear/protolayout/protolayout/build.gradle b/wear/protolayout/protolayout/build.gradle
index bc93416..27a9828 100644
--- a/wear/protolayout/protolayout/build.gradle
+++ b/wear/protolayout/protolayout/build.gradle
@@ -27,7 +27,7 @@
 
     implementation("androidx.annotation:annotation-experimental:1.3.0")
     implementation(project(path: ":wear:protolayout:protolayout-proto", configuration: "shadow"))
-    implementation(project(":wear:protolayout:protolayout-expression"))
+    api(project(":wear:protolayout:protolayout-expression"))
 
     compileOnly(libs.kotlinStdlib) // For annotation-experimental
 
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
index 7e98f63a..ddfea03 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/LayoutElementBuilders.java
@@ -24,6 +24,7 @@
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
 import androidx.wear.protolayout.ColorBuilders.ColorProp;
@@ -131,28 +132,55 @@
      */
     public static final int SPAN_VERTICAL_ALIGN_TEXT_BASELINE = 2;
 
-    /** How text that will not fit inside the bounds of a {@link Text} element will be handled. */
+    /**
+     * How text that will not fit inside the bounds of a {@link Text} element will be handled.
+     *
+     * @since 1.0
+     */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @IntDef({TEXT_OVERFLOW_UNDEFINED, TEXT_OVERFLOW_TRUNCATE, TEXT_OVERFLOW_ELLIPSIZE_END})
+    @IntDef({
+        TEXT_OVERFLOW_UNDEFINED,
+        TEXT_OVERFLOW_TRUNCATE,
+        TEXT_OVERFLOW_ELLIPSIZE_END,
+        TEXT_OVERFLOW_MARQUEE
+    })
     @Retention(RetentionPolicy.SOURCE)
+    @OptIn(markerClass = ProtoLayoutExperimental.class)
     public @interface TextOverflow {}
 
-    /** Overflow behavior is undefined. */
+    /**
+     * Overflow behavior is undefined.
+     *
+     * @since 1.0
+     */
     public static final int TEXT_OVERFLOW_UNDEFINED = 0;
 
     /**
      * Truncate the text to fit inside of the {@link Text} element's bounds. If text is truncated,
      * it will be truncated on a word boundary.
+     *
+     * @since 1.0
      */
     public static final int TEXT_OVERFLOW_TRUNCATE = 1;
 
     /**
      * Truncate the text to fit in the {@link Text} element's bounds, but add an ellipsis (i.e. ...)
      * to the end of the text if it has been truncated.
+     *
+     * @since 1.0
      */
     public static final int TEXT_OVERFLOW_ELLIPSIZE_END = 2;
 
     /**
+     * Enable marquee animation for texts that don't fit inside the {@link Text} element. This is
+     * only applicable for single line texts; if the text has multiple lines, the behavior is
+     * equivalent to TEXT_OVERFLOW_TRUNCATE.
+     *
+     * @since 1.2
+     */
+    @ProtoLayoutExperimental public static final int TEXT_OVERFLOW_MARQUEE = 3;
+
+    /**
      * How content which does not match the dimensions of its bounds (e.g. an image resource being
      * drawn inside an {@link Image}) will be resized to fit its bounds.
      */
@@ -639,7 +667,11 @@
         }
     }
 
-    /** An extensible {@code TextOverflow} property. */
+    /**
+     * An extensible {@code TextOverflow} property.
+     *
+     * @since 1.0
+     */
     public static final class TextOverflowProp {
         private final LayoutElementProto.TextOverflowProp mImpl;
         @Nullable private final Fingerprint mFingerprint;
@@ -650,7 +682,11 @@
             this.mFingerprint = fingerprint;
         }
 
-        /** Gets the value. Intended for testing purposes only. */
+        /**
+         * Gets the value.
+         *
+         * @since 1.0
+         */
         @TextOverflow
         public int getValue() {
             return mImpl.getValue().getNumber();
@@ -663,25 +699,46 @@
             return mFingerprint;
         }
 
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
-        static TextOverflowProp fromProto(@NonNull LayoutElementProto.TextOverflowProp proto) {
-            return new TextOverflowProp(proto, null);
+        public static TextOverflowProp fromProto(
+                @NonNull LayoutElementProto.TextOverflowProp proto,
+                @Nullable Fingerprint fingerprint) {
+            return new TextOverflowProp(proto, fingerprint);
         }
 
         @NonNull
-        LayoutElementProto.TextOverflowProp toProto() {
+        static TextOverflowProp fromProto(@NonNull LayoutElementProto.TextOverflowProp proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public LayoutElementProto.TextOverflowProp toProto() {
             return mImpl;
         }
 
+        @Override
+        @NonNull
+        public String toString() {
+            return "TextOverflowProp{" + "value=" + getValue() + "}";
+        }
+
         /** Builder for {@link TextOverflowProp} */
         public static final class Builder {
             private final LayoutElementProto.TextOverflowProp.Builder mImpl =
                     LayoutElementProto.TextOverflowProp.newBuilder();
-            private final Fingerprint mFingerprint = new Fingerprint(1183432233);
+            private final Fingerprint mFingerprint = new Fingerprint(-1542057565);
 
             public Builder() {}
 
-            /** Sets the value. */
+            /**
+             * Sets the value.
+             *
+             * @since 1.0
+             */
             @NonNull
             public Builder setValue(@TextOverflow int value) {
                 mImpl.setValue(LayoutElementProto.TextOverflow.forNumber(value));
@@ -698,6 +755,97 @@
     }
 
     /**
+     * Parameters for Marquee animation. Only applies for TEXT_OVERFLOW_MARQUEE.
+     *
+     * @since 1.2
+     */
+    @ProtoLayoutExperimental
+    static final class MarqueeParameters {
+        private final LayoutElementProto.MarqueeParameters mImpl;
+        @Nullable private final Fingerprint mFingerprint;
+
+        MarqueeParameters(
+                LayoutElementProto.MarqueeParameters impl, @Nullable Fingerprint fingerprint) {
+            this.mImpl = impl;
+            this.mFingerprint = fingerprint;
+        }
+
+        /**
+         * Gets the number of times to repeat the Marquee animation. Set to -1 to repeat
+         * indefinitely. Defaults to repeat indefinitely.
+         *
+         * @since 1.2
+         */
+        @ProtoLayoutExperimental
+        public int getIterations() {
+            return mImpl.getIterations();
+        }
+
+        /** Get the fingerprint for this object, or null if unknown. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Nullable
+        public Fingerprint getFingerprint() {
+            return mFingerprint;
+        }
+
+        /** Creates a new wrapper instance from the proto. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public static MarqueeParameters fromProto(
+                @NonNull LayoutElementProto.MarqueeParameters proto,
+                @Nullable Fingerprint fingerprint) {
+            return new MarqueeParameters(proto, fingerprint);
+        }
+
+        @NonNull
+        static MarqueeParameters fromProto(@NonNull LayoutElementProto.MarqueeParameters proto) {
+            return fromProto(proto, null);
+        }
+
+        /** Returns the internal proto instance. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public LayoutElementProto.MarqueeParameters toProto() {
+            return mImpl;
+        }
+
+        @Override
+        @NonNull
+        public String toString() {
+            return "MarqueeParameters{" + "iterations=" + getIterations() + "}";
+        }
+
+        /** Builder for {@link MarqueeParameters} */
+        public static final class Builder {
+            private final LayoutElementProto.MarqueeParameters.Builder mImpl =
+                    LayoutElementProto.MarqueeParameters.newBuilder();
+            private final Fingerprint mFingerprint = new Fingerprint(1405971293);
+
+            public Builder() {}
+
+            /**
+             * Sets the number of times to repeat the Marquee animation. Set to -1 to repeat
+             * indefinitely. Defaults to repeat indefinitely.
+             *
+             * @since 1.2
+             */
+            @ProtoLayoutExperimental
+            @NonNull
+            public Builder setIterations(int iterations) {
+                mImpl.setIterations(iterations);
+                mFingerprint.recordPropertyUpdate(1, iterations);
+                return this;
+            }
+
+            /** Builds an instance from accumulated values. */
+            @NonNull
+            public MarqueeParameters build() {
+                return new MarqueeParameters(mImpl.build(), mFingerprint);
+            }
+        }
+    }
+
+    /**
      * An Android platform specific text style configuration options for styling and compatibility.
      *
      * @since 1.2
@@ -924,6 +1072,18 @@
             }
         }
 
+        /**
+         * Gets the number of times to repeat the Marquee animation. Only applies when overflow is
+         * TEXT_OVERFLOW_MARQUEE. Set to -1 to repeat indefinitely. Defaults to repeat indefinitely.
+         *
+         * @since 1.2
+         */
+        @ProtoLayoutExperimental
+        @IntRange(from = -1)
+        public int getMarqueeIterations() {
+            return mImpl.getMarqueeParameters().getIterations();
+        }
+
         @Override
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Nullable
@@ -1138,6 +1298,23 @@
                 return this;
             }
 
+            /**
+             * Sets the number of times to repeat the Marquee animation. Only applies when overflow
+             * is TEXT_OVERFLOW_MARQUEE. Set to -1 to repeat indefinitely. Defaults to repeat
+             * indefinitely.
+             *
+             * @since 1.2
+             */
+            @ProtoLayoutExperimental
+            @NonNull
+            public Builder setMarqueeIterations(@IntRange(from = -1) int marqueeIterations) {
+                mImpl.setMarqueeParameters(
+                        LayoutElementProto.MarqueeParameters.newBuilder()
+                                .setIterations(marqueeIterations));
+                mFingerprint.recordPropertyUpdate(9, marqueeIterations);
+                return this;
+            }
+
             @Override
             @NonNull
             public Text build() {
@@ -2466,6 +2643,18 @@
             }
         }
 
+        /**
+         * Gets the number of times to repeat the Marquee animation. Only applies when overflow is
+         * TEXT_OVERFLOW_MARQUEE. Set to -1 to repeat indefinitely. Defaults to repeat indefinitely.
+         *
+         * @since 1.2
+         */
+        @ProtoLayoutExperimental
+        @IntRange(from = -1)
+        public int getMarqueeIterations() {
+            return mImpl.getMarqueeParameters().getIterations();
+        }
+
         @Override
         @RestrictTo(Scope.LIBRARY_GROUP)
         @Nullable
@@ -2615,6 +2804,23 @@
                 return this;
             }
 
+            /**
+             * Sets the number of times to repeat the Marquee animation. Only applies when overflow
+             * is TEXT_OVERFLOW_MARQUEE. Set to -1 to repeat indefinitely. Defaults to repeat
+             * indefinitely.
+             *
+             * @since 1.2
+             */
+            @ProtoLayoutExperimental
+            @NonNull
+            public Builder setMarqueeIterations(@IntRange(from = -1) int marqueeIterations) {
+                mImpl.setMarqueeParameters(
+                        LayoutElementProto.MarqueeParameters.newBuilder()
+                                .setIterations(marqueeIterations));
+                mFingerprint.recordPropertyUpdate(8, marqueeIterations);
+                return this;
+            }
+
             @Override
             @NonNull
             public Spannable build() {
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
index 600f72d..9b854fd 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/ModifiersBuilders.java
@@ -196,7 +196,7 @@
     public static final int SEMANTICS_ROLE_NONE = 0;
 
     /**
-     * The element is an {@link androidx.wear.protolayout.LayoutElementBuilders.Image}.
+     * The element is an image.
      *
      * @since 1.2
      */
diff --git a/wear/tiles/tiles-material/build.gradle b/wear/tiles/tiles-material/build.gradle
index c696e99..ac5345a 100644
--- a/wear/tiles/tiles-material/build.gradle
+++ b/wear/tiles/tiles-material/build.gradle
@@ -26,7 +26,7 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.2.0")
-    implementation(project(":wear:tiles:tiles"))
+    api(project(":wear:tiles:tiles"))
     implementation(project(":wear:tiles:tiles-proto"))
 
     androidTestImplementation(libs.junit)
diff --git a/wear/tiles/tiles-renderer/build.gradle b/wear/tiles/tiles-renderer/build.gradle
index 1d8a312..f7841d1 100644
--- a/wear/tiles/tiles-renderer/build.gradle
+++ b/wear/tiles/tiles-renderer/build.gradle
@@ -35,7 +35,7 @@
     implementation("androidx.core:core:1.7.0")
     implementation "androidx.wear:wear:1.2.0"
 
-    implementation(project(":wear:protolayout:protolayout"))
+    api(project(":wear:protolayout:protolayout"))
     implementation(project(":wear:protolayout:protolayout-expression-pipeline"))
     implementation(project(":wear:protolayout:protolayout-renderer"))
     implementation(project(":wear:tiles:tiles"))
diff --git a/window/window-demos/demo/build.gradle b/window/window-demos/demo/build.gradle
index 49f7d60..65adb00 100644
--- a/window/window-demos/demo/build.gradle
+++ b/window/window-demos/demo/build.gradle
@@ -52,6 +52,8 @@
     implementation("androidx.core:core-ktx:1.3.2")
     implementation("androidx.activity:activity:1.2.0")
     implementation("androidx.recyclerview:recyclerview:1.2.1")
+    // TODO(b/262583150): force tracing 1.1.0 since its required by androidTest
+    implementation("androidx.tracing:tracing:1.1.0")
     api(libs.constraintLayout)
     // TODO(b/152245564) Conflicting dependencies cause IDE errors.
     implementation("androidx.lifecycle:lifecycle-viewmodel:2.4.0-alpha02")