Merge "Fix documentation issues in wear-watchface" into androidx-main
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
index aaf6abe..44e9b90 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/AnchoredDraggable.kt
@@ -374,7 +374,8 @@
 ) : DragGestureNode(
     canDrag = AlwaysDrag,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     open suspend fun AnchoredDragScope.anchoredDrag(
@@ -389,9 +390,6 @@
         state.anchoredDrag(MutatePriority.Default) { anchoredDrag(forEachDelta) }
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -429,7 +427,8 @@
         update(
             enabled = enabled,
             interactionSource = interactionSource,
-            isResetPointerInputHandling = resetPointerInputHandling,
+            shouldResetPointerInputHandling = resetPointerInputHandling,
+            orientationLock = orientation
         )
     }
 
@@ -742,7 +741,8 @@
     @Deprecated(
         message = "Use the progress function to query the progress between two specified " +
             "anchors.",
-        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)"))
+        replaceWith = ReplaceWith("progress(state.settledValue, state.targetValue)")
+    )
     @get:FloatRange(from = 0.0, to = 1.0)
     val progress: Float by derivedStateOf(structuralEqualityPolicy()) {
         val a = anchors.positionOf(settledValue)
@@ -1296,7 +1296,7 @@
     }
 }
 
-private fun<K> ObjectFloatMap<K>.minValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.minValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var minValue = Float.POSITIVE_INFINITY
     forEachValue { value ->
@@ -1307,7 +1307,7 @@
     return minValue
 }
 
-private fun<K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
+private fun <K> ObjectFloatMap<K>.maxValueOrNaN(): Float {
     if (size == 1) return Float.NaN
     var maxValue = Float.NEGATIVE_INFINITY
     forEachValue { value ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
index 4d7c7fc..86835e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/DragGestureDetector.kt
@@ -45,7 +45,7 @@
 import androidx.compose.ui.util.fastAny
 import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
-import kotlin.math.abs
+import kotlin.math.absoluteValue
 import kotlin.math.sign
 import kotlinx.coroutines.CancellationException
 
@@ -79,7 +79,7 @@
         pointerId,
         PointerType.Touch,
         onPointerSlopReached = onTouchSlopReached,
-        pointerDirectionConfig = BidirectionalPointerDirectionConfig,
+        orientation = null,
     )
 }
 
@@ -170,33 +170,113 @@
     onDragEnd: () -> Unit = { },
     onDragCancel: () -> Unit = { },
     onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
+) = detectDragGestures(
+    onDragStart = { _, offset -> onDragStart(offset) },
+    onDragEnd = { onDragEnd.invoke() },
+    onDragCancel = onDragCancel,
+    shouldAwaitTouchSlop = { true },
+    orientationLock = null,
+    onDrag = onDrag
+)
+
+/**
+ * A Gesture detector that waits for pointer down and touch slop in the direction specified by
+ * [orientationLock] and then calls [onDrag] for each drag event.
+ * It follows the touch slop detection of [awaitTouchSlopOrCancellation] but will consume the
+ * position change automatically once the touch slop has been crossed, the amount of drag over
+ * the touch slop is reported as the first drag event [onDrag] after the slop is crossed.
+ * If [shouldAwaitTouchSlop] returns true the touch slop recognition phase will be ignored
+ * and the drag gesture will be recognized immediately.The first [onDrag] in this case will report
+ * an [Offset.Zero].
+ *
+ * [onDragStart] is called when the touch slop has been passed and includes an [Offset] representing
+ * the last known pointer position relative to the containing element as well as  the initial
+ * down event that triggered this gesture detection cycle. The [Offset] can be outside
+ * the actual bounds of the element itself meaning the numbers can be negative or larger than the
+ * element bounds if the touch target is smaller than the
+ * [ViewConfiguration.minimumTouchTargetSize].
+ *
+ * [onDragEnd] is called after all pointers are up with the event change of the up event
+ * and [onDragCancel] is called if another gesture has consumed pointer input,
+ * canceling this gesture.
+ *
+ * @param onDragStart A lambda to be called when the drag gesture starts, it contains information
+ * about the triggering [PointerInputChange] and post slop delta.
+ * @param onDragEnd A lambda to be called when the gesture ends. It contains information about the
+ * up [PointerInputChange] that finished the gesture.
+ * @param onDragCancel A lambda to be called when the gesture is cancelled either by an error or
+ * when it was consumed.
+ * @param shouldAwaitTouchSlop Indicates if touch slop detection should be skipped.
+ * @param orientationLock Optionally locks detection to this orientation, this means, when this is
+ * provided, touch slop detection and drag event detection will be conditioned to the given
+ * orientation axis. [onDrag] will still dispatch events on with information in both axis, but
+ * if orientation lock is provided, only events that happen on the given orientation will be
+ * considered. If no value is provided (i.e. null) touch slop and drag detection will happen on
+ * an "any" orientation basis, that is, touch slop will be detected if crossed in either direction
+ * and drag events will be dispatched if present in either direction.
+ * @param onDrag A lambda to be called for each delta event in the gesture. It contains information
+ * about the [PointerInputChange] and the movement offset.
+ *
+ * Example Usage:
+ * @sample androidx.compose.foundation.samples.DetectDragGesturesSample
+ *
+ * @see detectVerticalDragGestures
+ * @see detectHorizontalDragGestures
+ * @see detectDragGesturesAfterLongPress to detect gestures after long press
+ */
+internal suspend fun PointerInputScope.detectDragGestures(
+    onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit,
+    onDragEnd: (change: PointerInputChange) -> Unit,
+    onDragCancel: () -> Unit,
+    shouldAwaitTouchSlop: () -> Boolean,
+    orientationLock: Orientation?,
+    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
 ) {
     awaitEachGesture {
+        val initialDown =
+            awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
+        val awaitTouchSlop = shouldAwaitTouchSlop()
+
+        if (!awaitTouchSlop) {
+            initialDown.consume()
+        }
         val down = awaitFirstDown(requireUnconsumed = false)
         var drag: PointerInputChange?
         var overSlop = Offset.Zero
-        do {
-            drag = awaitPointerSlopOrCancellation(
-                down.id,
-                down.type,
-                pointerDirectionConfig = BidirectionalPointerDirectionConfig
-            ) { change, over ->
-                change.consume()
-                overSlop = over
-            }
-        } while (drag != null && !drag.isConsumed)
-        if (drag != null) {
-            onDragStart.invoke(drag.position)
-            onDrag(drag, overSlop)
-            if (
-                !drag(drag.id) {
-                    onDrag(it, it.positionChange())
-                    it.consume()
+        var initialDelta = Offset.Zero
+
+        if (awaitTouchSlop) {
+            do {
+                drag = awaitPointerSlopOrCancellation(
+                    down.id,
+                    down.type,
+                    orientation = orientationLock
+                ) { change, over ->
+                    change.consume()
+                    overSlop = over
                 }
-            ) {
+            } while (drag != null && !drag.isConsumed)
+            initialDelta = drag?.position ?: Offset.Zero
+        } else {
+            drag = initialDown
+        }
+
+        if (drag != null) {
+            onDragStart.invoke(initialDown, initialDelta)
+            onDrag(drag, overSlop)
+            val upEvent = drag(
+                pointerId = drag.id,
+                onDrag = {
+                    onDrag(it, it.positionChange())
+                },
+                orientation = orientationLock,
+                motionConsumed = {
+                    it.isConsumed
+                })
+            if (upEvent == null) {
                 onDragCancel()
             } else {
-                onDragEnd()
+                onDragEnd(upEvent)
             }
         }
     }
@@ -288,7 +368,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 internal suspend fun AwaitPointerEventScope.awaitVerticalPointerSlopOrCancellation(
@@ -299,7 +379,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.y) },
-    pointerDirectionConfig = VerticalPointerDirectionConfig
+    orientation = Orientation.Vertical
 )
 
 /**
@@ -324,7 +404,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().y != 0f },
+    orientation = Orientation.Vertical,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -439,7 +519,7 @@
     pointerId = pointerId,
     pointerType = PointerType.Touch,
     onPointerSlopReached = { change, overSlop -> onTouchSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 internal suspend fun AwaitPointerEventScope.awaitHorizontalPointerSlopOrCancellation(
@@ -450,7 +530,7 @@
     pointerId = pointerId,
     pointerType = pointerType,
     onPointerSlopReached = { change, overSlop -> onPointerSlopReached(change, overSlop.x) },
-    pointerDirectionConfig = HorizontalPointerDirectionConfig
+    orientation = Orientation.Horizontal
 )
 
 /**
@@ -472,7 +552,7 @@
 ): Boolean = drag(
     pointerId = pointerId,
     onDrag = onDrag,
-    hasDragged = { it.positionChangeIgnoreConsumed().x != 0f },
+    orientation = Orientation.Horizontal,
     motionConsumed = { it.isConsumed }
 ) != null
 
@@ -563,9 +643,12 @@
 
 /**
  * Continues to read drag events until all pointers are up or the drag event is canceled.
- * The initial pointer to use for driving the drag is [pointerId]. [hasDragged]
- * passes the result whether a change was detected from the drag function or not. [onDrag] is called
- * whenever the pointer moves and [hasDragged] returns non-zero.
+ * The initial pointer to use for driving the drag is [pointerId]. [onDrag] is called
+ * whenever the pointer moves. The up event is returned at the end of the drag gesture.
+ *
+ * @param pointerId The pointer where that is driving the gesture.
+ * @param onDrag Callback for every new drag event.
+ * @param motionConsumed If the PointerInputChange should be considered as consumed.
  *
  * @return The last pointer input event change when gesture ended with all pointers up
  * and null when the gesture was canceled.
@@ -573,7 +656,7 @@
 internal suspend inline fun AwaitPointerEventScope.drag(
     pointerId: PointerId,
     onDrag: (PointerInputChange) -> Unit,
-    hasDragged: (PointerInputChange) -> Boolean,
+    orientation: Orientation?,
     motionConsumed: (PointerInputChange) -> Boolean
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -581,7 +664,15 @@
     }
     var pointer = pointerId
     while (true) {
-        val change = awaitDragOrUp(pointer, hasDragged) ?: return null
+        val change = awaitDragOrUp(pointer) {
+            val positionChange = it.positionChangeIgnoreConsumed()
+            val motionChange = if (orientation == null) {
+                positionChange.getDistance()
+            } else {
+                if (orientation == Orientation.Vertical) positionChange.y else positionChange.x
+            }
+            motionChange != 0.0f
+        } ?: return null
 
         if (motionConsumed(change)) {
             return null
@@ -629,16 +720,14 @@
 }
 
 /**
- * Waits for drag motion along one axis when [pointerDirectionConfig] is
- * [HorizontalPointerDirectionConfig] or [VerticalPointerDirectionConfig], and drag motion along
- * any axis when using [BidirectionalPointerDirectionConfig]. It passes [pointerId] as the pointer
- * to examine. If [pointerId] is raised, another pointer from those that are down will be chosen to
+ * Waits for drag motion and uses [orientation] to detect the direction of  touch slop detection.
+ * It passes [pointerId] as the pointer to examine. If [pointerId] is raised, another pointer from
+ * those that are down will be chosen to
  * lead the gesture, and if none are down, `null` is returned. If [pointerId] is not down when
  * [awaitPointerSlopOrCancellation] is called, then `null` is returned.
  *
  * When pointer slop is detected, [onPointerSlopReached] is called with the change and the distance
- * beyond the pointer slop. [PointerDirectionConfig.calculateDeltaChange] should return the position
- * change in the direction of the drag axis. If [onPointerSlopReached] does not consume the
+ * beyond the pointer slop. If [onPointerSlopReached] does not consume the
  * position change, pointer slop will not have been considered detected and the detection will
  * continue or, if it is consumed, the [PointerInputChange] that was consumed will be returned.
  *
@@ -650,10 +739,10 @@
  * `null` if all pointers are raised or the position change was consumed by another gesture
  * detector.
  */
-internal suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
+private suspend inline fun AwaitPointerEventScope.awaitPointerSlopOrCancellation(
     pointerId: PointerId,
     pointerType: PointerType,
-    pointerDirectionConfig: PointerDirectionConfig,
+    orientation: Orientation?,
     onPointerSlopReached: (PointerInputChange, Offset) -> Unit,
 ): PointerInputChange? {
     if (currentEvent.isPointerUp(pointerId)) {
@@ -661,8 +750,7 @@
     }
     val touchSlop = viewConfiguration.pointerSlop(pointerType)
     var pointer: PointerId = pointerId
-    var totalPositionChange = Offset.Zero
-
+    val touchSlopDetector = TouchSlopDetector(orientation)
     while (true) {
         val event = awaitPointerEvent()
         val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null
@@ -677,29 +765,8 @@
                 pointer = otherDown.id
             }
         } else {
-            val currentPosition = dragEvent.position
-            val previousPosition = dragEvent.previousPosition
-
-            val positionChange = currentPosition - previousPosition
-
-            totalPositionChange += positionChange
-
-            val inDirection = pointerDirectionConfig.calculateDeltaChange(
-                totalPositionChange
-            )
-
-            if (inDirection < touchSlop) {
-                // verify that nothing else consumed the drag event
-                awaitPointerEvent(PointerEventPass.Final)
-                if (dragEvent.isConsumed) {
-                    return null
-                }
-            } else {
-                val postSlopOffset = pointerDirectionConfig.calculatePostSlopOffset(
-                    totalPositionChange,
-                    touchSlop
-                )
-
+            val postSlopOffset = touchSlopDetector.addPointerInputChange(dragEvent, touchSlop)
+            if (postSlopOffset != null) {
                 onPointerSlopReached(
                     dragEvent,
                     postSlopOffset
@@ -707,7 +774,13 @@
                 if (dragEvent.isConsumed) {
                     return dragEvent
                 } else {
-                    totalPositionChange = Offset.Zero
+                    touchSlopDetector.reset()
+                }
+            } else {
+                // verify that nothing else consumed the drag event
+                awaitPointerEvent(PointerEventPass.Final)
+                if (dragEvent.isConsumed) {
+                    return null
                 }
             }
         }
@@ -715,70 +788,77 @@
 }
 
 /**
- * Configures the calculations to get the change amount depending on the dragging type.
- * [calculatePostSlopOffset] will return the post offset slop when the touchSlop is reached.
+ * Detects if touch slop has been crossed after adding a series of [PointerInputChange].
+ * For every new [PointerInputChange] one should add it to this detector using
+ * [addPointerInputChange]. If the position change causes the touch slop to be crossed,
+ * [addPointerInputChange] will return true.
  */
-internal interface PointerDirectionConfig {
-    fun calculateDeltaChange(offset: Offset): Float
-    fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset
-}
+private class TouchSlopDetector(val orientation: Orientation? = null) {
 
-/**
- * Used for monitoring changes on X axis.
- */
-internal val HorizontalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.x)
+    fun Offset.mainAxis() = if (orientation == Orientation.Horizontal) x else y
+    fun Offset.crossAxis() = if (orientation == Orientation.Horizontal) y else x
 
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
+    /**
+     * The accumulation of drag deltas in this detector.
+     */
+    private var totalPositionChange: Offset = Offset.Zero
+
+    /**
+     * Adds [dragEvent] to this detector. If the accumulated position changes crosses the touch
+     * slop provided by [touchSlop], this method will return the post slop offset, that is the
+     * total accumulated delta change minus the touch slop value, otherwise this should return null.
+     */
+    fun addPointerInputChange(
+        dragEvent: PointerInputChange,
         touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.x -
-            (sign(totalPositionChange.x) * touchSlop)
-        return Offset(finalMainPositionChange, totalPositionChange.y)
+    ): Offset? {
+        val currentPosition = dragEvent.position
+        val previousPosition = dragEvent.previousPosition
+        val positionChange = currentPosition - previousPosition
+        totalPositionChange += positionChange
+
+        val inDirection = if (orientation == null) {
+            totalPositionChange.getDistance()
+        } else {
+            totalPositionChange.mainAxis().absoluteValue
+        }
+
+        val hasCrossedSlop = inDirection >= touchSlop
+
+        return if (hasCrossedSlop) {
+            calculatePostSlopOffset(touchSlop)
+        } else {
+            null
+        }
+    }
+
+    /**
+     * Resets the accumulator associated with this detector.
+     */
+    fun reset() {
+        totalPositionChange = Offset.Zero
+    }
+
+    private fun calculatePostSlopOffset(touchSlop: Float): Offset {
+        return if (orientation == null) {
+            val touchSlopOffset =
+                totalPositionChange / totalPositionChange.getDistance() * touchSlop
+            // update postSlopOffset
+            totalPositionChange - touchSlopOffset
+        } else {
+            val finalMainAxisChange = totalPositionChange.mainAxis() -
+                (sign(totalPositionChange.mainAxis()) * touchSlop)
+            val finalCrossAxisChange = totalPositionChange.crossAxis()
+            if (orientation == Orientation.Horizontal) {
+                Offset(finalMainAxisChange, finalCrossAxisChange)
+            } else {
+                Offset(finalCrossAxisChange, finalMainAxisChange)
+            }
+        }
     }
 }
 
 /**
- * Used for monitoring changes on Y axis.
- */
-internal val VerticalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = abs(offset.y)
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val finalMainPositionChange = totalPositionChange.y -
-            (sign(totalPositionChange.y) * touchSlop)
-        return Offset(totalPositionChange.x, finalMainPositionChange)
-    }
-}
-
-/**
- * Used for monitoring changes on both X and Y axes.
- */
-internal val BidirectionalPointerDirectionConfig = object : PointerDirectionConfig {
-    override fun calculateDeltaChange(offset: Offset): Float = offset.getDistance()
-
-    override fun calculatePostSlopOffset(
-        totalPositionChange: Offset,
-        touchSlop: Float
-    ): Offset {
-        val touchSlopOffset =
-            totalPositionChange / calculateDeltaChange(totalPositionChange) * touchSlop
-        return totalPositionChange - touchSlopOffset
-    }
-}
-
-internal fun Orientation.toPointerDirectionConfig(): PointerDirectionConfig =
-    if (this == Orientation.Vertical) VerticalPointerDirectionConfig
-    else HorizontalPointerDirectionConfig
-
-/**
  * Waits for a long press by examining [pointerId].
  *
  * If that [pointerId] is raised (that is, the user lifts their finger), but another
@@ -839,7 +919,7 @@
                         // should technically never happen as we checked it above
                         finished = true
                     }
-                // Pointer (id) stayed down.
+                    // Pointer (id) stayed down.
                 } else {
                     longPress = event.changes.fastFirstOrNull { it.id == currentDown.id }
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 5db29b1..92f3e6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -31,16 +31,11 @@
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.AwaitPointerEventScope
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.PointerId
 import androidx.compose.ui.input.pointer.PointerInputChange
 import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode
-import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.input.pointer.positionChange
-import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.input.pointer.util.addPointerInputChange
 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
@@ -56,7 +51,6 @@
 import kotlin.math.sign
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
@@ -301,9 +295,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ) {
 
     override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) {
@@ -314,8 +309,6 @@
         }
     }
 
-    override val pointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@DraggableNode.onDragStarted(this, startedPosition)
 
@@ -357,6 +350,7 @@
             canDrag,
             enabled,
             interactionSource,
+            orientation,
             resetPointerInputHandling
         )
     }
@@ -372,6 +366,7 @@
     canDrag: (PointerInputChange) -> Boolean,
     enabled: Boolean,
     interactionSource: MutableInteractionSource?,
+    private var orientationLock: Orientation?
 ) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode {
 
     protected var canDrag = canDrag
@@ -397,13 +392,6 @@
     abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit)
 
     /**
-     * Returns the pointerDirectionConfig which specifies the main and cross axis deltas. This is
-     * important when observing the delta change for Draggable, as we want to observe the change
-     * in the main axis only.
-     */
-    abstract val pointerDirectionConfig: PointerDirectionConfig
-
-    /**
      * Passes the action needed when a drag starts. This gives the ability to pass the desired
      * behavior from other nodes implementing AbstractDraggableNode
      */
@@ -478,62 +466,63 @@
             // re-create tracker when pointer input block restarts. This lazily creates the tracker
             // only when it is need.
             val velocityTracker = VelocityTracker()
+            val onDragStart: (change: PointerInputChange, initialDelta: Offset) -> Unit =
+                { startEvent, initialDelta ->
+                    if (canDrag.invoke(startEvent)) {
+                        if (!isListeningForEvents) {
+                            if (channel == null) {
+                                channel = Channel(capacity = Channel.UNLIMITED)
+                            }
+                            startListeningForEvents()
+                        }
+                        val overSlopOffset = initialDelta
+                        val xSign = sign(startEvent.position.x)
+                        val ySign = sign(startEvent.position.y)
+                        val adjustedStart = startEvent.position -
+                            Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
+
+                        channel?.trySend(DragStarted(adjustedStart))
+                    }
+                }
+
+            val onDragEnd: (change: PointerInputChange) -> Unit = { upEvent ->
+                velocityTracker.addPointerInputChange(upEvent)
+                val maximumVelocity = currentValueOf(LocalViewConfiguration)
+                    .maximumFlingVelocity
+                val velocity = velocityTracker.calculateVelocity(
+                    Velocity(maximumVelocity, maximumVelocity)
+                )
+                velocityTracker.resetTracking()
+                channel?.trySend(DragStopped(velocity))
+            }
+
+            val onDragCancel: () -> Unit = {
+                channel?.trySend(DragCancelled)
+            }
+
+            val shouldAwaitTouchSlop: () -> Boolean = {
+                !startDragImmediately()
+            }
+
+            val onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit =
+                { change, delta ->
+                    velocityTracker.addPointerInputChange(change)
+                    channel?.trySend(DragDelta(delta))
+                }
+
             coroutineScope {
                 try {
-                    awaitPointerEventScope {
-                        while (isActive) {
-                            awaitDownAndSlop(
-                                _canDrag,
-                                ::startDragImmediately,
-                                velocityTracker,
-                                pointerDirectionConfig
-                            )?.let {
-                                /**
-                                 * The gesture crossed the touch slop, events are now relevant
-                                 * and should be propagated
-                                 */
-                                if (!isListeningForEvents) {
-                                    if (channel == null) {
-                                        channel = Channel(capacity = Channel.UNLIMITED)
-                                    }
-                                    startListeningForEvents()
-                                }
-                                var isDragSuccessful = false
-                                try {
-                                    isDragSuccessful = awaitDrag(
-                                        it.first,
-                                        it.second,
-                                        velocityTracker,
-                                        channel
-                                    ) { event ->
-                                        pointerDirectionConfig.calculateDeltaChange(
-                                            event.positionChangeIgnoreConsumed()
-                                        ) != 0f
-                                    }
-                                } catch (cancellation: CancellationException) {
-                                    isDragSuccessful = false
-                                    if (!isActive) throw cancellation
-                                } finally {
-                                    val maximumVelocity = currentValueOf(LocalViewConfiguration)
-                                        .maximumFlingVelocity
-                                    val event = if (isDragSuccessful) {
-                                        val velocity = velocityTracker.calculateVelocity(
-                                            Velocity(maximumVelocity, maximumVelocity)
-                                        )
-                                        velocityTracker.resetTracking()
-                                        DragStopped(velocity)
-                                    } else {
-                                        DragCancelled
-                                    }
-                                    channel?.trySend(event)
-                                }
-                            }
-                        }
-                    }
-                } catch (exception: CancellationException) {
-                    if (!isActive) {
-                        throw exception
-                    }
+                    detectDragGestures(
+                        orientationLock = orientationLock,
+                        onDragStart = onDragStart,
+                        onDragEnd = onDragEnd,
+                        onDragCancel = onDragCancel,
+                        shouldAwaitTouchSlop = shouldAwaitTouchSlop,
+                        onDrag = onDrag
+                    )
+                } catch (cancellation: CancellationException) {
+                    channel?.trySend(DragCancelled)
+                    if (!isActive) throw cancellation
                 }
             }
         }
@@ -580,9 +569,10 @@
         canDrag: (PointerInputChange) -> Boolean = this.canDrag,
         enabled: Boolean = this.enabled,
         interactionSource: MutableInteractionSource? = this.interactionSource,
-        isResetPointerInputHandling: Boolean = false
+        orientationLock: Orientation? = this.orientationLock,
+        shouldResetPointerInputHandling: Boolean = false
     ) {
-        var resetPointerInputHandling = isResetPointerInputHandling
+        var resetPointerInputHandling = shouldResetPointerInputHandling
 
         this.canDrag = canDrag
         if (this.enabled != enabled) {
@@ -599,91 +589,17 @@
             this.interactionSource = interactionSource
         }
 
+        if (this.orientationLock != orientationLock) {
+            this.orientationLock = orientationLock
+            resetPointerInputHandling = true
+        }
+
         if (resetPointerInputHandling) {
             pointerInputNode?.resetPointerInputHandler()
         }
     }
 }
 
-private suspend fun AwaitPointerEventScope.awaitDownAndSlop(
-    canDrag: (PointerInputChange) -> Boolean,
-    startDragImmediately: () -> Boolean,
-    velocityTracker: VelocityTracker,
-    pointerDirectionConfig: PointerDirectionConfig
-): Pair<PointerInputChange, Offset>? {
-    val initialDown =
-        awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
-    return if (!canDrag(initialDown)) {
-        null
-    } else if (startDragImmediately()) {
-        initialDown.consume()
-        velocityTracker.addPointerInputChange(initialDown)
-        // since we start immediately we don't wait for slop and the initial delta is 0
-        initialDown to Offset.Zero
-    } else {
-        val down = awaitFirstDown(requireUnconsumed = false)
-        velocityTracker.addPointerInputChange(down)
-        var initialDelta = Offset.Zero
-        val postPointerSlop = { event: PointerInputChange, offset: Offset ->
-            velocityTracker.addPointerInputChange(event)
-            event.consume()
-            initialDelta = offset
-        }
-
-        val afterSlopResult = awaitPointerSlopOrCancellation(
-            down.id,
-            down.type,
-            pointerDirectionConfig = pointerDirectionConfig,
-            onPointerSlopReached = postPointerSlop
-        )
-
-        if (afterSlopResult != null) afterSlopResult to initialDelta else null
-    }
-}
-
-private suspend fun AwaitPointerEventScope.awaitDrag(
-    startEvent: PointerInputChange,
-    initialDelta: Offset,
-    velocityTracker: VelocityTracker,
-    channel: SendChannel<DragEvent>?,
-    hasDragged: (PointerInputChange) -> Boolean,
-): Boolean {
-
-    val overSlopOffset = initialDelta
-    val xSign = sign(startEvent.position.x)
-    val ySign = sign(startEvent.position.y)
-    val adjustedStart = startEvent.position -
-        Offset(overSlopOffset.x * xSign, overSlopOffset.y * ySign)
-    channel?.trySend(DragStarted(adjustedStart))
-
-    channel?.trySend(DragDelta(initialDelta))
-
-    return onDragOrUp(hasDragged, startEvent.id) { event ->
-        // Velocity tracker takes all events, even UP
-        velocityTracker.addPointerInputChange(event)
-
-        // Dispatch only MOVE events
-        if (!event.changedToUpIgnoreConsumed()) {
-            val delta = event.positionChange()
-            event.consume()
-            channel?.trySend(DragDelta(delta))
-        }
-    }
-}
-
-private suspend fun AwaitPointerEventScope.onDragOrUp(
-    hasDragged: (PointerInputChange) -> Boolean,
-    pointerId: PointerId,
-    onDrag: (PointerInputChange) -> Unit
-): Boolean {
-    return drag(
-        pointerId = pointerId,
-        onDrag = onDrag,
-        hasDragged = hasDragged,
-        motionConsumed = { it.isConsumed }
-    )?.let(onDrag) != null
-}
-
 private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState {
 
     private val dragScope: DragScope = object : DragScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
index f9c9571..0c4b4c4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable2D.kt
@@ -267,9 +267,10 @@
     private var onDragStopped: suspend CoroutineScope.(velocity: Velocity) -> Unit,
     private var reverseDirection: Boolean
 ) : DragGestureNode(
-    canDrag,
-    enabled,
-    interactionSource
+    canDrag = canDrag,
+    enabled = enabled,
+    interactionSource = interactionSource,
+    orientationLock = null
 ) {
 
     override suspend fun drag(
@@ -282,8 +283,6 @@
         }
     }
 
-    override val pointerDirectionConfig = BidirectionalPointerDirectionConfig
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) =
         this@Draggable2DNode.onDragStarted(this, startedPosition)
 
@@ -317,10 +316,11 @@
         this.startDragImmediately = startDragImmediately
 
         update(
-            canDrag,
-            enabled,
-            interactionSource,
-            resetPointerInputHandling
+            canDrag = canDrag,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = null,
+            shouldResetPointerInputHandling = resetPointerInputHandling
         )
     }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index cf62744a..bcfaf97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -273,7 +273,8 @@
 ) : DragGestureNode(
     canDrag = CanDragCalculation,
     enabled = enabled,
-    interactionSource = interactionSource
+    interactionSource = interactionSource,
+    orientationLock = orientation
 ), ObserverModifierNode, CompositionLocalConsumerModifierNode,
     FocusPropertiesModifierNode, KeyInputModifierNode {
 
@@ -333,9 +334,6 @@
         scrollingLogic.dispatchDragEvents(forEachDelta)
     }
 
-    override val pointerDirectionConfig: PointerDirectionConfig
-        get() = scrollingLogic.pointerDirectionConfig()
-
     override suspend fun CoroutineScope.onDragStarted(startedPosition: Offset) {}
 
     override suspend fun CoroutineScope.onDragStopped(velocity: Velocity) {
@@ -386,7 +384,13 @@
         this.flingBehavior = flingBehavior
 
         // update DragGestureNode
-        update(CanDragCalculation, enabled, interactionSource, resetPointerInputHandling)
+        update(
+            canDrag = CanDragCalculation,
+            enabled = enabled,
+            interactionSource = interactionSource,
+            orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal,
+            shouldResetPointerInputHandling = resetPointerInputHandling
+        )
     }
 
     override fun onAttach() {
@@ -777,8 +781,6 @@
         return resetPointerInputHandling
     }
 
-    fun pointerDirectionConfig(): PointerDirectionConfig = orientation.toPointerDirectionConfig()
-
     fun isVertical(): Boolean = orientation == Vertical
 }
 
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
index 825a3ee..3ab85ce 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedColumn.kt
@@ -44,6 +44,7 @@
  * than the curved column, either at the [CurvedAlignment.Angular.Start] of the layout,
  * at the [CurvedAlignment.Angular.End], or [CurvedAlignment.Angular.Center].
  * If unspecified or null, they can choose for themselves.
+ * @param contentBuilder Scope used to provide the content for this column.
  */
 public fun CurvedScope.curvedColumn(
     modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
index f4fa3ca..4f5daea 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/CurvedRow.kt
@@ -42,6 +42,7 @@
  * and if those needs to be reversed in a Rtl layout.
  * If not specified, it will be inherited from the enclosing [curvedRow] or [CurvedLayout]
  * See [CurvedDirection.Angular].
+ * @param contentBuilder Scope used to provide the content for this row.
  */
 public fun CurvedScope.curvedRow(
     modifier: CurvedModifier = CurvedModifier,
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
index e344c9f..2087c5c3 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/SwipeableV2.kt
@@ -677,17 +677,19 @@
 @RestrictTo(LIBRARY_GROUP)
 public object SwipeableV2Defaults {
     /**
-     * The default animation used by [SwipeableV2State].
+     * The default animation that will be used to animate to a new state.
      */
     val AnimationSpec = SpringSpec<Float>()
 
     /**
-     * The default velocity threshold (1.8 dp per millisecond) used by [rememberSwipeableV2State].
+     * The default velocity threshold (in dp per second) that the end velocity has to
+     * exceed in order to animate to the next state.
      */
     val VelocityThreshold: Dp = 125.dp
 
     /**
-     * The default positional threshold (56 dp) used by [rememberSwipeableV2State]
+     * The default positional threshold used when calculating the target state while a swipe is in
+     * progress and when settling after the swipe ends.
      */
     val PositionalThreshold: Density.(totalDistance: Float) -> Float =
         fixedPositionalThreshold(56.dp)
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
index 1fdbc11..114fd30 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/SelectionControls.kt
@@ -310,10 +310,9 @@
     // Canvas internally uses Spacer.drawBehind.
     // Using Spacer.drawWithCache to optimize the stroke allocations.
     Spacer(
+        // NB We must set the semantic role to Role.RadioButton in the parent Button,
+        // not here in the selection control - see b/330869742
         modifier = modifier
-            .semantics {
-                this.role = Role.RadioButton
-            }
             .maybeSelectable(
                 onClick, enabled, selected, interactionSource, ripple, width, height
             )
diff --git a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
index 792bd00..3b0332a 100644
--- a/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
+++ b/wear/compose/compose-material-core/src/main/java/androidx/wear/compose/materialcore/ToggleButton.kt
@@ -211,6 +211,9 @@
                         indication = ripple,
                         interactionSource = interactionSource
                     )
+                    // For a toggleable button, the role could be Checkbox or Switch,
+                    // so we cannot set the semantics here. Instead,
+                    // we set them in the toggle control
                 } else {
                     Modifier.selectable(
                         enabled = enabled,
@@ -218,7 +221,12 @@
                         onClick = { onCheckedChange(true) },
                         indication = ripple,
                         interactionSource = interactionSource
-                    )
+                    ).semantics {
+                        // For a selectable button, the role is always RadioButton.
+                        // See also b/330869742 for issue with setting the RadioButton role
+                        // within the selection control.
+                        role = Role.RadioButton
+                    }
                 }
             )
             .padding(contentPadding),
@@ -375,6 +383,12 @@
                     indication = ripple,
                     interactionSource = checkedInteractionSource
                 )
+                .semantics {
+                    // For a selectable button, the role is always RadioButton.
+                    // See also b/330869742 for issue with setting the RadioButton role
+                    // within the selection control.
+                    role = Role.RadioButton
+                }
             }
 
         Box(
diff --git a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
index 2fde27b..c3d34d8 100644
--- a/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
+++ b/wear/compose/compose-material/src/androidTest/kotlin/androidx/wear/compose/material/SelectableChipTest.kt
@@ -136,6 +136,28 @@
     }
 
     @Test
+    fun selectable_chip_has_role_radiobutton() {
+        rule.setContentWithTheme {
+            SelectableChip(
+                selected = true,
+                onClick = {},
+                enabled = false,
+                label = { Text("Label") },
+                selectionControl = { TestImage() },
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.Role,
+                    Role.RadioButton
+                )
+            )
+    }
+
+    @Test
     fun split_chip_has_clickaction_when_disabled() {
         rule.setContentWithTheme {
             SplitSelectableChip(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
index f5c3ef0..3997c14 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/RadioButtonTest.kt
@@ -82,6 +82,23 @@
     }
 
     @Test
+    fun radio_button_has_role_radiobutton() {
+        rule.setContentWithTheme {
+            RadioButtonWithDefaults(
+                modifier = Modifier.testTag(TEST_TAG)
+            )
+        }
+
+        rule.onNodeWithTag(TEST_TAG)
+            .assert(
+                SemanticsMatcher.expectValue(
+                    SemanticsProperties.Role,
+                    Role.RadioButton
+                )
+            )
+    }
+
+    @Test
     fun radio_button_samples_build() {
         rule.setContentWithTheme {
             RadioButton()
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
index 600e737..a8ac0da 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/SelectionControlsTest.kt
@@ -22,10 +22,6 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.SemanticsProperties
-import androidx.compose.ui.test.SemanticsMatcher
-import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertIsEnabled
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.captureToImage
@@ -67,25 +63,6 @@
     }
 
     @Test
-    fun radio_control_has_role_radiobutton() {
-        rule.setContentWithTheme {
-            with(SelectionControlScope(isEnabled = true, isSelected = true)) {
-                Radio(
-                    modifier = Modifier.testTag(TEST_TAG)
-                )
-            }
-        }
-
-        rule.onNodeWithTag(TEST_TAG)
-            .assert(
-                SemanticsMatcher.expectValue(
-                    SemanticsProperties.Role,
-                    Role.RadioButton
-                )
-            )
-    }
-
-    @Test
     fun radio_control_is_correctly_enabled() {
         rule.setContentWithTheme {
             with(SelectionControlScope(isEnabled = true, isSelected = true)) {
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
index 70dbab71..b4a6f53 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/RadioButton.kt
@@ -144,7 +144,13 @@
                 indication = rippleOrFallbackImplementation(),
                 interactionSource = interactionSource
             )
-            .padding(contentPadding),
+            .padding(contentPadding)
+            .semantics {
+                // For a selectable button, the role is always RadioButton.
+                // See also b/330869742 for issue with setting the RadioButton role
+                // within the selection control.
+                role = Role.RadioButton
+            },
         verticalAlignment = Alignment.CenterVertically
     ) {
         if (icon != null) {
@@ -329,7 +335,13 @@
                 .width(SPLIT_WIDTH)
                 .wrapContentHeight(align = Alignment.CenterVertically)
                 .wrapContentWidth(align = Alignment.End)
-                .then(endPadding),
+                .then(endPadding)
+                .semantics {
+                    // For a selectable button, the role is always RadioButton.
+                    // See also b/330869742 for issue with setting the RadioButton role
+                    // within the selection control.
+                    role = Role.RadioButton
+                },
         ) {
             val scope = remember(enabled, selected) { SelectionControlScope(enabled, selected) }
             selectionControl(scope)
diff --git a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
index 7981cf2..5b295fc 100644
--- a/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
+++ b/wear/compose/compose-ui-tooling/src/main/java/androidx/wear/compose/ui/tooling/preview/WearPreviewFontScales.kt
@@ -34,7 +34,7 @@
  * note, the above list is not exhaustive. It previews the composables on a small round Wear device.
  *
  * @sample androidx.wear.compose.material.samples.TitleCardWithImagePreview
- * @see [Preview.fontScale]
+ * @see Preview.fontScale
  */
 @Preview(
     device = WearDevices.SMALL_ROUND,
diff --git a/wear/compose/integration-tests/demos/build.gradle b/wear/compose/integration-tests/demos/build.gradle
index 28d0392..d68e962 100644
--- a/wear/compose/integration-tests/demos/build.gradle
+++ b/wear/compose/integration-tests/demos/build.gradle
@@ -25,8 +25,8 @@
     defaultConfig {
         applicationId "androidx.wear.compose.integration.demos"
         minSdk 25
-        versionCode 23
-        versionName "1.23"
+        versionCode 24
+        versionName "1.24"
     }
 
     buildTypes {
diff --git a/wear/protolayout/protolayout-expression/api/current.txt b/wear/protolayout/protolayout-expression/api/current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/current.txt
+++ b/wear/protolayout/protolayout-expression/api/current.txt
@@ -14,7 +14,7 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationParameters.Builder {
-    ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationSpec.Builder {
-    ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
-    method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
   }
 
   public static final class AnimationParameterBuilders.Repeatable.Builder {
-    ctor public AnimationParameterBuilders.Repeatable.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/api/restricted_current.txt b/wear/protolayout/protolayout-expression/api/restricted_current.txt
index bd9565b..756cbe0 100644
--- a/wear/protolayout/protolayout-expression/api/restricted_current.txt
+++ b/wear/protolayout/protolayout-expression/api/restricted_current.txt
@@ -14,7 +14,7 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationParameters.Builder {
-    ctor public AnimationParameterBuilders.AnimationParameters.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationParameters.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDelayMillis(@IntRange(from=0) long);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters.Builder setDurationMillis(@IntRange(from=0) long);
@@ -27,10 +27,10 @@
   }
 
   public static final class AnimationParameterBuilders.AnimationSpec.Builder {
-    ctor public AnimationParameterBuilders.AnimationSpec.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.AnimationSpec.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setAnimationParameters(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
-    method public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
+    method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationSpec.Builder setRepeatable(androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable);
   }
 
   @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public static interface AnimationParameterBuilders.Easing {
@@ -53,7 +53,7 @@
   }
 
   public static final class AnimationParameterBuilders.Repeatable.Builder {
-    ctor public AnimationParameterBuilders.Repeatable.Builder();
+    ctor @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public AnimationParameterBuilders.Repeatable.Builder();
     method public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable build();
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setForwardRepeatOverride(androidx.wear.protolayout.expression.AnimationParameterBuilders.AnimationParameters);
     method @androidx.wear.protolayout.expression.RequiresSchemaVersion(major=1, minor=200) public androidx.wear.protolayout.expression.AnimationParameterBuilders.Repeatable.Builder setIterations(@IntRange(from=1) int);
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
index 44eb9a7..a800b2a 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/AnimationParameterBuilders.java
@@ -140,6 +140,7 @@
                     AnimationParameterProto.AnimationSpec.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-2136602843);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets animation parameters including duration, easing and repeat delay. */
@@ -158,6 +159,7 @@
              * Sets the repeatable mode to be used for specifying repetition parameters for the
              * animation. If not set, animation won't be repeated.
              */
+            @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setRepeatable(@NonNull Repeatable repeatable) {
                 mImpl.setRepeatable(repeatable.toProto());
@@ -261,6 +263,7 @@
                     AnimationParameterProto.AnimationParameters.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1301308590);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -491,7 +494,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         AnimationParameterProto.CubicBezierEasing toProto() {
             return mImpl;
@@ -525,6 +527,7 @@
                     AnimationParameterProto.CubicBezierEasing.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(856403705);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -704,6 +707,7 @@
                     AnimationParameterProto.Repeatable.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(2110475048);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
index 36e1b0b..5a088b9 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/ConditionScopes.java
@@ -48,12 +48,14 @@
         }
 
         /** Sets the value to use as the value when true in a conditional expression. */
-        public @NonNull IfTrueScope<T, RawT> use(T valueWhenTrue) {
+        @NonNull
+        public IfTrueScope<T, RawT> use(T valueWhenTrue) {
             return new IfTrueScope<>(valueWhenTrue, conditionBuilder, rawTypeMapper);
         }
 
         /** Sets the value to use as the value when true in a conditional expression. */
-        public @NonNull IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
+        @NonNull
+        public IfTrueScope<T, RawT> use(RawT valueWhenTrue) {
             return use(rawTypeMapper.apply(valueWhenTrue));
         }
     }
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
index 17d89ce..500985d 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicBuilders.java
@@ -708,7 +708,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -1255,7 +1255,7 @@
             /** Sets the value to start animating from. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableFixedInt32.Builder setFromValue(int fromValue) {
+            public Builder setFromValue(int fromValue) {
                 mImpl.setFromValue(fromValue);
                 mFingerprint.recordPropertyUpdate(1, fromValue);
                 return this;
@@ -1264,7 +1264,7 @@
             /** Sets the value to animate to. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableFixedInt32.Builder setToValue(int toValue) {
+            public Builder setToValue(int toValue) {
                 mImpl.setToValue(toValue);
                 mFingerprint.recordPropertyUpdate(2, toValue);
                 return this;
@@ -1398,7 +1398,7 @@
             /** Sets the value to watch, and animate when it changes. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
-            public AnimatableDynamicInt32.Builder setInput(@NonNull DynamicInt32 input) {
+            public Builder setInput(@NonNull DynamicInt32 input) {
                 mImpl.setInput(input.toDynamicInt32Proto());
                 mFingerprint.recordPropertyUpdate(
                         1, checkNotNull(input.getFingerprint()).aggregateValueAsInt());
@@ -2413,7 +2413,7 @@
 
             /** Returns whether digit grouping is used or not. */
             public boolean isGroupingUsed() {
-                return mInt32FormatOp.getGroupingUsed();
+                return mInt32FormatOp.isGroupingUsed();
             }
 
             /** Builder to create {@link IntFormatter} objects. */
@@ -2579,7 +2579,7 @@
          * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
          * with 1234 would yield "1,234".
          */
-        public boolean getGroupingUsed() {
+        public boolean isGroupingUsed() {
             return mImpl.getGroupingUsed();
         }
 
@@ -2638,13 +2638,13 @@
                     + ", minIntegerDigits="
                     + getMinIntegerDigits()
                     + ", groupingUsed="
-                    + getGroupingUsed()
+                    + isGroupingUsed()
                     + "}";
         }
 
         /** Builder for {@link Int32FormatOp}. */
         public static final class Builder implements DynamicString.Builder {
-            final DynamicProto.Int32FormatOp.Builder mImpl =
+            private final DynamicProto.Int32FormatOp.Builder mImpl =
                     DynamicProto.Int32FormatOp.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(196209833);
 
@@ -2792,7 +2792,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -3143,7 +3143,7 @@
          * locale. If not defined, defaults to false. For example, for locale en_US, using grouping
          * with 1234.56 would yield "1,234.56".
          */
-        public boolean getGroupingUsed() {
+        public boolean isGroupingUsed() {
             return mImpl.getGroupingUsed();
         }
 
@@ -3206,7 +3206,7 @@
                     + ", minIntegerDigits="
                     + getMinIntegerDigits()
                     + ", groupingUsed="
-                    + getGroupingUsed()
+                    + isGroupingUsed()
                     + "}";
         }
 
@@ -3736,7 +3736,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -5012,7 +5012,7 @@
 
             /** Returns whether digit grouping is used or not. */
             public boolean isGroupingUsed() {
-                return mFloatFormatOp.getGroupingUsed();
+                return mFloatFormatOp.isGroupingUsed();
             }
 
             /** Builder to create {@link FloatFormatter} objects. */
@@ -5228,8 +5228,8 @@
         @NonNull
         public DynamicProto.DynamicBool toDynamicBoolProto(boolean withFingerprint) {
             if (withFingerprint) {
-                return DynamicProto.DynamicBool.newBuilder().
-                        setStateSource(mImpl)
+                return DynamicProto.DynamicBool.newBuilder()
+                        .setStateSource(mImpl)
                         .setFingerprint(checkNotNull(mFingerprint).toProto())
                         .build();
             }
@@ -5265,7 +5265,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
@@ -6144,7 +6144,7 @@
                 return this;
             }
 
-            /** Sets the name space for the state key. */
+            /** Sets the namespace for the state key. */
             @RequiresSchemaVersion(major = 1, minor = 200)
             @NonNull
             public Builder setSourceNamespace(@NonNull String sourceNamespace) {
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
index 69c88e3..c3b5c45 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/DynamicDataBuilders.java
@@ -28,7 +28,6 @@
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInstant;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicInt32;
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicType;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedBool;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedColor;
 import androidx.wear.protolayout.expression.FixedValueBuilders.FixedDuration;
@@ -51,7 +50,7 @@
 
     /** Interface defining a dynamic data value. */
     @RequiresSchemaVersion(major = 1, minor = 200)
-    public interface DynamicDataValue<T extends DynamicType> {
+    public interface DynamicDataValue<T extends DynamicBuilders.DynamicType> {
         /** Get the protocol buffer representation of this object. */
         @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
@@ -310,7 +309,7 @@
 
         /** Builder to create {@link DynamicDataValue} objects. */
         @RestrictTo(Scope.LIBRARY_GROUP)
-        interface Builder<T extends DynamicType> {
+        interface Builder<T extends DynamicBuilders.DynamicType> {
 
             /** Builds an instance with values accumulated in this Builder. */
             @NonNull
diff --git a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
index ced6685..e16e059 100644
--- a/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
+++ b/wear/protolayout/protolayout-expression/src/main/java/androidx/wear/protolayout/expression/FixedValueBuilders.java
@@ -78,7 +78,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedInt32 toProto() {
             return mImpl;
@@ -144,6 +143,7 @@
             private final FixedProto.FixedInt32.Builder mImpl = FixedProto.FixedInt32.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(974881783);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -203,7 +203,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedString toProto() {
             return mImpl;
@@ -271,6 +270,7 @@
                     FixedProto.FixedString.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(1963352072);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -333,7 +333,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedFloat toProto() {
             return mImpl;
@@ -399,6 +398,7 @@
             private final FixedProto.FixedFloat.Builder mImpl = FixedProto.FixedFloat.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-144724541);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -461,7 +461,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedBool toProto() {
             return mImpl;
@@ -527,6 +526,7 @@
             private final FixedProto.FixedBool.Builder mImpl = FixedProto.FixedBool.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-665116398);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the value. */
@@ -587,7 +587,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedColor toProto() {
             return mImpl;
@@ -653,6 +652,7 @@
             private final FixedProto.FixedColor.Builder mImpl = FixedProto.FixedColor.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1895809356);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets the color value, in ARGB format. */
@@ -733,7 +733,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedInstant toProto() {
             return mImpl;
@@ -778,6 +777,7 @@
                     FixedProto.FixedInstant.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(-1986552556);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /**
@@ -860,7 +860,6 @@
         }
 
         /** Returns the internal proto instance. */
-        @RestrictTo(Scope.LIBRARY_GROUP)
         @NonNull
         FixedProto.FixedDuration toProto() {
             return mImpl;
@@ -905,6 +904,7 @@
                     FixedProto.FixedDuration.newBuilder();
             private final Fingerprint mFingerprint = new Fingerprint(9029504);
 
+            @RequiresSchemaVersion(major = 1, minor = 200)
             public Builder() {}
 
             /** Sets duration in seconds. */
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
new file mode 100644
index 0000000..d6a206c
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/Constants.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License = 0 Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing = 0 software
+ * distributed under the License is distributed on an "AS IS" BASIS = 0
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND = 0 either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.RestrictTo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Shared constants. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Constants {
+
+    private Constants() {}
+
+    /** The reason why an update was requested. */
+    @IntDef({
+        UPDATE_REQUEST_REASON_UNKNOWN,
+        UPDATE_REQUEST_REASON_SYSUI_CAROUSEL,
+        UPDATE_REQUEST_REASON_FRESHNESS,
+        UPDATE_REQUEST_REASON_USER_INTERACTION,
+        UPDATE_REQUEST_REASON_UPDATE_REQUESTER,
+        UPDATE_REQUEST_REASON_CACHE_INVALIDATION,
+        UPDATE_REQUEST_REASON_RETRY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface UpdateRequestReason {}
+
+    /** Unknown reason. */
+    public static final int UPDATE_REQUEST_REASON_UNKNOWN = 0;
+
+    /** Update triggered by SysUI Carousel. */
+    public static final int UPDATE_REQUEST_REASON_SYSUI_CAROUSEL = 1;
+
+    /** Update triggered by freshness. */
+    public static final int UPDATE_REQUEST_REASON_FRESHNESS = 2;
+
+    /** Update triggered by user interaction (e.g. clicking on the tile). */
+    public static final int UPDATE_REQUEST_REASON_USER_INTERACTION = 3;
+
+    /** Update triggered using update requester. */
+    public static final int UPDATE_REQUEST_REASON_UPDATE_REQUESTER = 4;
+
+    /** Update triggered due to clearing the cache. */
+    public static final int UPDATE_REQUEST_REASON_CACHE_INVALIDATION = 5;
+
+    /** Update triggered by retry policy. */
+    public static final int UPDATE_REQUEST_REASON_RETRY = 6;
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
new file mode 100644
index 0000000..f292c98
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/NoOpProviderStatsLogger.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+/** A No-Op implementation of {@link ProviderStatsLogger}. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class NoOpProviderStatsLogger implements ProviderStatsLogger {
+    private static final String TAG = "NoOpProviderStatsLogger";
+
+    /** Creates an instance of {@link NoOpProviderStatsLogger}. */
+    public NoOpProviderStatsLogger(@NonNull String reason) {
+        Log.i(TAG, "Instance used because " + reason);
+    }
+
+    /** No-op method. */
+    @Override
+    public void logLayoutSchemaVersion(int major, int minor) {}
+
+    /** No-op method. */
+    @Override
+    public void logStateStructure(@NonNull State state, boolean isInitialState) {}
+
+    /** No-op method. */
+    @Override
+    public void logIgnoredFailure(int failure) {}
+
+    /** No-op method. */
+    @Override
+    public void logInflationFailed(@InflationFailureReason int failureReason) {}
+
+    /** No-op method. */
+    @Override
+    @NonNull
+    public InflaterStatsLogger createInflaterStatsLogger() {
+        return new NoOpInflaterStatsLogger();
+    }
+
+    /** No-op method. */
+    @Override
+    public void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger) {}
+
+    /** No-op method. */
+    @Override
+    public void logTileRequestReason(@UpdateRequestReason int updateRequestReason) {}
+
+    /** A No-Op implementation of {@link InflaterStatsLogger}. */
+    public static class NoOpInflaterStatsLogger implements InflaterStatsLogger {
+
+        private NoOpInflaterStatsLogger() {}
+
+        @Override
+        public void logMutationChangedNodes(int changedNodesCount) {}
+
+        @Override
+        public void logTotalNodeCount(int totalNodesCount) {}
+
+        /** No-op method. */
+        @Override
+        public void logDrawableUsage(@NonNull Drawable drawable) {}
+
+        /** No-op method. */
+        @Override
+        public void logIgnoredFailure(@IgnoredFailure int failure) {}
+
+        /** No-op method. */
+        @Override
+        public void logInflationFailed(@InflationFailureReason int failureReason) {}
+    }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
new file mode 100644
index 0000000..96a6fb0
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/ProviderStatsLogger.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import android.graphics.drawable.Drawable;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.UiThread;
+import androidx.wear.protolayout.proto.StateProto.State;
+import androidx.wear.protolayout.renderer.common.Constants.UpdateRequestReason;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Logger used for collecting metrics. */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ProviderStatsLogger {
+
+    /** Failures that doesn't cause the inflation to fail. */
+    @IntDef({
+        IGNORED_FAILURE_UNKNOWN,
+        IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION,
+        IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED,
+        IGNORED_FAILURE_DIFFING_FAILURE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface IgnoredFailure {}
+
+    /** Unknown failure. */
+    int IGNORED_FAILURE_UNKNOWN = 0;
+
+    /** Failure applying the diff mutation. */
+    int IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION = 1;
+
+    /** Failure caused by exceeding animation quota. */
+    int IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED = 2;
+
+    /** Failure diffing the layout. */
+    int IGNORED_FAILURE_DIFFING_FAILURE = 3;
+
+    /** Failures that causes the inflation to fail. */
+    @IntDef({
+        INFLATION_FAILURE_REASON_UNKNOWN,
+        INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED,
+        INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface InflationFailureReason {}
+
+    /** Unknown failure. */
+    int INFLATION_FAILURE_REASON_UNKNOWN = 0;
+
+    /** Failure caused by exceeding maximum layout depth. */
+    int INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED = 1;
+
+    /** Failure caused by exceeding maximum expression node count. */
+    int INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED = 2;
+
+    /** Log the schema version of the received layout. */
+    void logLayoutSchemaVersion(int major, int minor);
+
+    /** Log Protolayout state structure. */
+    void logStateStructure(@NonNull State state, boolean isInitialState);
+
+    /** Log the occurrence of an ignored failure. */
+    @UiThread
+    void logIgnoredFailure(@IgnoredFailure int failure);
+
+    /** Log the reason for inflation failure. */
+    @UiThread
+    void logInflationFailed(@InflationFailureReason int failureReason);
+
+    /**
+     * Creates an {@link InflaterStatsLogger} and marks the start of inflation. The atoms will be
+     * logged to statsd only when {@link #logInflationFinished} is called.
+     */
+    @UiThread
+    @NonNull
+    InflaterStatsLogger createInflaterStatsLogger();
+
+    /** Makes the end of inflation and log the inflation results. */
+    @UiThread
+    void logInflationFinished(@NonNull InflaterStatsLogger inflaterStatsLogger);
+
+    /** Log tile request reason. */
+    void logTileRequestReason(@UpdateRequestReason int updateRequestReason);
+
+    /** Logger used for logging inflation stats. */
+    interface InflaterStatsLogger {
+        /** log the mutation changed nodes count for the ongoing inflation. */
+        @UiThread
+        void logMutationChangedNodes(int changedNodesCount);
+
+        /** Log the total nodes count for the ongoing inflation. */
+        @UiThread
+        void logTotalNodeCount(int totalNodesCount);
+
+        /**
+         * Log the usage of a drawable. This method should be called between {@link
+         * #createInflaterStatsLogger()} and {@link #logInflationFinished(InflaterStatsLogger)}.
+         */
+        @UiThread
+        void logDrawableUsage(@NonNull Drawable drawable);
+
+        /**
+         * Log the occurrence of an ignored failure. The usage of this method is not restricted to
+         * inflation start or end.
+         */
+        @UiThread
+        void logIgnoredFailure(@IgnoredFailure int failure);
+
+        /**
+         * Log the reason for inflation failure. This will make any future call {@link
+         * #logInflationFinished(InflaterStatsLogger)} a Noop.
+         */
+        @UiThread
+        void logInflationFailed(@InflationFailureReason int failureReason);
+    }
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
new file mode 100644
index 0000000..8424728
--- /dev/null
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/common/RenderingArtifact.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.protolayout.renderer.common;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.RestrictTo.Scope;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+
+/** Artifacts resulted from the layout rendering. */
+@RestrictTo(Scope.LIBRARY_GROUP)
+public interface RenderingArtifact {
+
+    /** Creates a {@link RenderingArtifact} instance. */
+    @NonNull
+    static RenderingArtifact create(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+        return new SuccessfulRenderingArtifact(inflaterStatsLogger);
+    }
+
+    /** Creates a {@link RenderingArtifact} instance for a skipped inflation. */
+    @NonNull
+    static RenderingArtifact skipped() {
+        return new SkippedRenderingArtifact();
+    }
+
+    /** Creates a {@link RenderingArtifact} instance for a failed inflation. */
+    @NonNull
+    static RenderingArtifact failed() {
+        return new FailedRenderingArtifact();
+    }
+
+    /** Artifacts resulted from a successful layout rendering. */
+    class SuccessfulRenderingArtifact implements RenderingArtifact {
+        @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
+
+        private SuccessfulRenderingArtifact(@NonNull InflaterStatsLogger inflaterStatsLogger) {
+            mInflaterStatsLogger = inflaterStatsLogger;
+        }
+
+        /**
+         * Returns the {@link ProviderStatsLogger.InflaterStatsLogger} used log inflation stats.
+         * This will return {@code null} if the inflation was skipped or failed.
+         */
+        @NonNull
+        public InflaterStatsLogger getInflaterStatsLogger() {
+            return mInflaterStatsLogger;
+        }
+    }
+
+    /** Artifacts resulted from a skipped layout rendering. */
+    class SkippedRenderingArtifact implements RenderingArtifact {}
+
+    /** Artifacts resulted from a failed layout rendering. */
+    class FailedRenderingArtifact implements RenderingArtifact {}
+}
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
index 08f0228..8f18ca8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstance.java
@@ -20,6 +20,13 @@
 import static android.widget.FrameLayout.LayoutParams.UNSPECIFIED_GRAVITY;
 
 import static androidx.core.util.Preconditions.checkNotNull;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED;
+import static androidx.wear.protolayout.renderer.common.ProviderStatsLogger.INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED;
+
+import static com.google.common.util.concurrent.Futures.immediateCancelledFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -41,6 +48,7 @@
 import androidx.wear.protolayout.expression.PlatformDataKey;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
 import androidx.wear.protolayout.expression.pipeline.PlatformDataProvider;
+import androidx.wear.protolayout.expression.pipeline.QuotaManager;
 import androidx.wear.protolayout.expression.pipeline.StateStore;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement;
 import androidx.wear.protolayout.proto.LayoutElementProto.ArcLayoutElement.InnerCase;
@@ -52,7 +60,11 @@
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
 import androidx.wear.protolayout.renderer.ProtoLayoutVisibilityState;
 import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -114,6 +126,7 @@
     @NonNull private final ListeningExecutorService mUiExecutorService;
     @NonNull private final ListeningExecutorService mBgExecutorService;
     @NonNull private final String mClickableIdExtra;
+    @NonNull private final ProviderStatsLogger mProviderStatsLogger;
     @Nullable private final LoggingUtils mLoggingUtils;
 
     @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
@@ -219,10 +232,11 @@
          */
         @UiThread
         @NonNull
-        ListenableFuture<Void> postInflate(
+        ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching);
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger);
     }
 
     /** Result of a {@link #renderOrComputeMutations} call when no changes are required. */
@@ -234,11 +248,12 @@
 
         @NonNull
         @Override
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
-            return Futures.immediateVoidFuture();
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
+            return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
         }
     }
 
@@ -251,11 +266,12 @@
 
         @NonNull
         @Override
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
-            return Futures.immediateVoidFuture();
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
+            return immediateFuture(RenderingArtifact.failed());
         }
     }
 
@@ -278,10 +294,11 @@
         @NonNull
         @Override
         @UiThread
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
             InflateResult inflateResult =
                     checkNotNull(
                             mNewInflateParentData.mInflateResult,
@@ -292,7 +309,7 @@
             attachParent.addView(
                     inflateResult.inflateParent, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
             inflateResult.updateDynamicDataPipeline(isReattaching);
-            return Futures.immediateVoidFuture();
+            return immediateFuture(RenderingArtifact.create(inflaterStatsLogger));
         }
     }
 
@@ -318,10 +335,11 @@
         @NonNull
         @Override
         @UiThread
-        public ListenableFuture<Void> postInflate(
+        public ListenableFuture<RenderingArtifact> postInflate(
                 @NonNull ViewGroup attachParent,
                 @Nullable ViewGroup prevInflateParent,
-                boolean isReattaching) {
+                boolean isReattaching,
+                InflaterStatsLogger inflaterStatsLogger) {
             return mInflater.applyMutation(checkNotNull(prevInflateParent), mMutation);
         }
     }
@@ -345,6 +363,7 @@
         @NonNull private final String mClickableIdExtra;
 
         @Nullable private final LoggingUtils mLoggingUtils;
+        @NonNull private final ProviderStatsLogger mProviderStatsLogger;
         private final boolean mAnimationEnabled;
         private final int mRunningAnimationsLimit;
 
@@ -366,6 +385,7 @@
                 @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
                 @NonNull String clickableIdExtra,
                 @Nullable LoggingUtils loggingUtils,
+                @NonNull ProviderStatsLogger providerStatsLogger,
                 boolean animationEnabled,
                 int runningAnimationsLimit,
                 boolean updatesEnabled,
@@ -384,6 +404,7 @@
             this.mExtensionViewProvider = extensionViewProvider;
             this.mClickableIdExtra = clickableIdExtra;
             this.mLoggingUtils = loggingUtils;
+            this.mProviderStatsLogger = providerStatsLogger;
             this.mAnimationEnabled = animationEnabled;
             this.mRunningAnimationsLimit = runningAnimationsLimit;
             this.mUpdatesEnabled = updatesEnabled;
@@ -468,6 +489,13 @@
             return mLoggingUtils;
         }
 
+        /** Returns the provider stats logger used for telemetry. */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        public ProviderStatsLogger getProviderStatsLogger() {
+            return mProviderStatsLogger;
+        }
+
         /** Returns whether animations are enabled. */
         @RestrictTo(Scope.LIBRARY)
         public boolean getAnimationEnabled() {
@@ -529,6 +557,7 @@
             @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider;
             @NonNull private final String mClickableIdExtra;
             @Nullable private LoggingUtils mLoggingUtils;
+            @Nullable private ProviderStatsLogger mProviderStatsLogger;
             private boolean mAnimationEnabled = true;
             private int mRunningAnimationsLimit = DEFAULT_MAX_CONCURRENT_RUNNING_ANIMATIONS;
 
@@ -632,6 +661,15 @@
                 return this;
             }
 
+            /** Sets the provider stats logger used for telemetry. */
+            @RestrictTo(Scope.LIBRARY_GROUP)
+            @NonNull
+            public Builder setProviderStatsLogger(
+                    @NonNull ProviderStatsLogger providerStatsLogger) {
+                this.mProviderStatsLogger = providerStatsLogger;
+                return this;
+            }
+
             /**
              * Sets whether animation are enabled. If disabled, none of the animation will be
              * played.
@@ -715,6 +753,12 @@
                 if (mRendererResources == null) {
                     this.mRendererResources = mUiContext.getResources();
                 }
+
+                if (mProviderStatsLogger == null) {
+                    mProviderStatsLogger =
+                            new NoOpProviderStatsLogger(
+                                    "ProviderStatsLogger not provided to " + TAG);
+                }
                 return new Config(
                         mUiContext,
                         mRendererResources,
@@ -728,6 +772,7 @@
                         mExtensionViewProvider,
                         mClickableIdExtra,
                         mLoggingUtils,
+                        mProviderStatsLogger,
                         mAnimationEnabled,
                         mRunningAnimationsLimit,
                         mUpdatesEnabled,
@@ -754,24 +799,51 @@
         this.mWasFullyVisibleBefore = false;
         this.mAllowLayoutChangingBindsWithoutDefault =
                 config.getAllowLayoutChangingBindsWithoutDefault();
+        this.mProviderStatsLogger = config.getProviderStatsLogger();
 
         StateStore stateStore = config.getStateStore();
-        if (stateStore != null) {
-            mDataPipeline =
-                    config.getAnimationEnabled()
-                            ? new ProtoLayoutDynamicDataPipeline(
-                                    config.getPlatformDataProviders(),
-                                    stateStore,
-                                    new FixedQuotaManagerImpl(
-                                            config.getRunningAnimationsLimit(), "animations"),
-                                    new FixedQuotaManagerImpl(
-                                            DYNAMIC_NODES_MAX_COUNT, "dynamic nodes"))
-                            : new ProtoLayoutDynamicDataPipeline(
-                                    config.getPlatformDataProviders(), stateStore);
-            mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
-        } else {
+        if (stateStore == null) {
             mDataPipeline = null;
+            return;
         }
+
+        if (config.getAnimationEnabled()) {
+            QuotaManager nodeQuotaManager =
+                    new FixedQuotaManagerImpl(DYNAMIC_NODES_MAX_COUNT, "dynamic nodes") {
+                        @Override
+                        public boolean tryAcquireQuota(int quota) {
+                            boolean success = super.tryAcquireQuota(quota);
+                            if (!success) {
+                                mProviderStatsLogger.logInflationFailed(
+                                        INFLATION_FAILURE_REASON_EXPRESSION_NODE_COUNT_EXCEEDED);
+                            }
+                            return success;
+                        }
+                    };
+            mDataPipeline =
+                    new ProtoLayoutDynamicDataPipeline(
+                            config.getPlatformDataProviders(),
+                            stateStore,
+                            new FixedQuotaManagerImpl(
+                                    config.getRunningAnimationsLimit(), "animations") {
+                                @Override
+                                public boolean tryAcquireQuota(int quota) {
+                                    boolean success = super.tryAcquireQuota(quota);
+                                    if (!success) {
+                                        mProviderStatsLogger.logIgnoredFailure(
+                                                IGNORED_FAILURE_ANIMATION_QUOTA_EXCEEDED);
+                                    }
+                                    return success;
+                                }
+                            },
+                            nodeQuotaManager);
+        } else {
+            mDataPipeline =
+                    new ProtoLayoutDynamicDataPipeline(
+                            config.getPlatformDataProviders(), stateStore);
+        }
+
+        mDataPipeline.setFullyVisible(config.getIsViewFullyVisible());
     }
 
     @WorkerThread
@@ -780,7 +852,8 @@
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @Nullable RenderedMetadata prevRenderedMetadata,
-            @NonNull ViewProperties parentViewProp) {
+            @NonNull ViewProperties parentViewProp,
+            @NonNull InflaterStatsLogger inflaterStatsLogger) {
         ResourceResolvers resolvers =
                 mResourceResolversProvider.getResourceResolvers(
                         mUiContext, resources, mUiExecutorService, mAnimationEnabled);
@@ -797,10 +870,10 @@
 
         if (sameFingerprint) {
             if (mPrevLayoutAlreadyFailingDepthCheck) {
-                throwExceptionForLayoutDepthCheckFailure();
+                handleLayoutDepthCheckFailure(inflaterStatsLogger);
             }
         } else {
-            checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH);
+            checkLayoutDepth(layout.getRoot(), MAX_LAYOUT_ELEMENT_DEPTH, inflaterStatsLogger);
         }
 
         mPrevLayoutAlreadyFailingDepthCheck = false;
@@ -815,6 +888,7 @@
                         .setClickableIdExtra(mClickableIdExtra)
                         .setAllowLayoutChangingBindsWithoutDefault(
                                 mAllowLayoutChangingBindsWithoutDefault)
+                        .setInflaterStatsLogger(inflaterStatsLogger)
                         .setApplyFontVariantBodyAsDefault(true);
         if (mDataPipeline != null) {
             inflaterConfigBuilder.setDynamicDataPipeline(mDataPipeline);
@@ -886,6 +960,18 @@
         return new InflateParentData(result);
     }
 
+    @UiThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public ListenableFuture<RenderingArtifact> renderLayoutAndAttach(
+            @NonNull Layout layout,
+            @NonNull ResourceProto.Resources resources,
+            @NonNull ViewGroup attachParent) {
+
+        return renderAndAttach(
+                layout, resources, attachParent, mProviderStatsLogger.createInflaterStatsLogger());
+    }
+
     /**
      * Render the layout for this layout and attach this layout instance to a {@code attachParent}
      * container. Note that this method may clear all of {@code attachParent}'s children before
@@ -911,6 +997,47 @@
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @NonNull ViewGroup attachParent) {
+        SettableFuture<Void> result = SettableFuture.create();
+        ListenableFuture<RenderingArtifact> future =
+                renderLayoutAndAttach(layout, resources, attachParent);
+        if (future.isDone()) {
+            if (future.isCancelled()) {
+                return immediateCancelledFuture();
+            }
+            return immediateFuture(null);
+        } else {
+            future.addListener(
+                    () -> {
+                        if (future.isCancelled()) {
+                            result.cancel(/* mayInterruptIfRunning= */ false);
+                        } else {
+                            try {
+                                RenderingArtifact ignored = future.get();
+                                result.set(null);
+                            } catch (ExecutionException
+                                    | InterruptedException
+                                    | CancellationException e) {
+                                Log.e(TAG, "Failed to render layout", e);
+                                result.setException(e);
+                            }
+                        }
+                    },
+                    mUiExecutorService);
+        }
+        return result;
+    }
+
+    @UiThread
+    @SuppressWarnings({
+        "ReferenceEquality",
+        "ExecutorTaskName",
+    }) // layout == prevLayout is intentional (and enough in this case)
+    @NonNull
+    private ListenableFuture<RenderingArtifact> renderAndAttach(
+            @NonNull Layout layout,
+            @NonNull ResourceProto.Resources resources,
+            @NonNull ViewGroup attachParent,
+            @NonNull InflaterStatsLogger inflaterStatsLogger) {
         if (mLoggingUtils != null && mLoggingUtils.canLogD(TAG)) {
             mLoggingUtils.logD(TAG, "Layout received in #renderAndAttach:\n %s", layout.toString());
             mLoggingUtils.logD(
@@ -930,7 +1057,7 @@
 
         if (layout == mPrevLayout && mInflateParent != null) {
             // Nothing to do.
-            return Futures.immediateVoidFuture();
+            return Futures.immediateFuture(RenderingArtifact.skipped());
         }
 
         boolean isReattaching = false;
@@ -1000,10 +1127,11 @@
                                             layout,
                                             resources,
                                             prevRenderedMetadata,
-                                            parentViewProp));
+                                            parentViewProp,
+                                            inflaterStatsLogger));
             mCanReattachWithoutRendering = false;
         }
-        SettableFuture<Void> result = SettableFuture.create();
+        SettableFuture<RenderingArtifact> result = SettableFuture.create();
         if (!checkNotNull(mRenderFuture).isDone()) {
             ListenableFuture<RenderResult> rendererFuture = mRenderFuture;
             mRenderFuture.addListener(
@@ -1023,7 +1151,8 @@
                                                 checkNotNull(rendererFuture).get(),
                                                 /* isReattaching= */ false,
                                                 layout,
-                                                resources));
+                                                resources,
+                                                inflaterStatsLogger));
                             } catch (ExecutionException
                                     | InterruptedException
                                     | CancellationException e) {
@@ -1048,7 +1177,8 @@
                                 mRenderFuture.get(),
                                 isReattaching,
                                 layout,
-                                resources));
+                                resources,
+                                inflaterStatsLogger));
             } catch (ExecutionException | InterruptedException | CancellationException e) {
                 Log.e(TAG, "Failed to render layout", e);
                 result.setException(e);
@@ -1064,6 +1194,12 @@
      */
     public void invalidateCache() {
         mPrevResourcesVersion = null;
+        // Cancel any ongoing rendering which might have a reference to older app resources.
+        if (mRenderFuture != null && !mRenderFuture.isDone()) {
+            mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+            mRenderFuture = null;
+            Log.w(TAG, "Cancelled ongoing rendering due to cache invalidation.");
+        }
     }
 
     @Nullable
@@ -1080,13 +1216,14 @@
     @UiThread
     @SuppressWarnings("ExecutorTaskName")
     @NonNull
-    private ListenableFuture<Void> postInflate(
+    private ListenableFuture<RenderingArtifact> postInflate(
             @NonNull ViewGroup attachParent,
             @Nullable ViewGroup prevInflateParent,
             @NonNull RenderResult renderResult,
             boolean isReattaching,
             @NonNull Layout layout,
-            @NonNull ResourceProto.Resources resources) {
+            @NonNull ResourceProto.Resources resources,
+            InflaterStatsLogger inflaterStatsLogger) {
         mCanReattachWithoutRendering = renderResult.canReattachWithoutRendering();
 
         if (renderResult instanceof InflatedIntoNewParentRenderResult) {
@@ -1101,9 +1238,10 @@
                             .inflateParent;
         }
 
-        ListenableFuture<Void> postInflateFuture =
-                renderResult.postInflate(attachParent, prevInflateParent, isReattaching);
-        SettableFuture<Void> result = SettableFuture.create();
+        ListenableFuture<RenderingArtifact> postInflateFuture =
+                renderResult.postInflate(
+                        attachParent, prevInflateParent, isReattaching, inflaterStatsLogger);
+        SettableFuture<RenderingArtifact> result = SettableFuture.create();
         if (!postInflateFuture.isDone()) {
             postInflateFuture.addListener(
                     () -> {
@@ -1114,20 +1252,24 @@
                                 | CancellationException e) {
                             result.setFuture(
                                     handlePostInflateFailure(
-                                            e, layout, resources, prevInflateParent, attachParent));
+                                            e,
+                                            layout,
+                                            resources,
+                                            prevInflateParent,
+                                            attachParent,
+                                            inflaterStatsLogger));
                         }
                     },
                     mUiExecutorService);
         } else {
             try {
-                postInflateFuture.get();
-                return Futures.immediateVoidFuture();
+                return immediateFuture(postInflateFuture.get());
             } catch (ExecutionException
                     | InterruptedException
                     | CancellationException
                     | ViewMutationException e) {
                 return handlePostInflateFailure(
-                        e, layout, resources, prevInflateParent, attachParent);
+                        e, layout, resources, prevInflateParent, attachParent, inflaterStatsLogger);
             }
         }
         return result;
@@ -1136,22 +1278,24 @@
     @UiThread
     @SuppressWarnings("ReferenceEquality") // layout == prevLayout is intentional
     @NonNull
-    private ListenableFuture<Void> handlePostInflateFailure(
+    private ListenableFuture<RenderingArtifact> handlePostInflateFailure(
             @NonNull Throwable error,
             @NonNull Layout layout,
             @NonNull ResourceProto.Resources resources,
             @Nullable ViewGroup prevInflateParent,
-            @NonNull ViewGroup parent) {
+            @NonNull ViewGroup parent,
+            InflaterStatsLogger inflaterStatsLogger) {
         // If a RuntimeError is thrown, it'll be wrapped in an UncheckedExecutionException
         Throwable e = error.getCause();
         if (e instanceof ViewMutationException) {
+            inflaterStatsLogger.logIgnoredFailure(IGNORED_FAILURE_APPLY_MUTATION_EXCEPTION);
             Log.w(TAG, "applyMutation failed." + e.getMessage());
             if (mPrevLayout == layout && parent == mAttachParent) {
                 Log.w(TAG, "Retrying full inflation.");
                 // Clear rendering metadata and prevLayout to force a full reinflation.
                 ProtoLayoutInflater.clearRenderedMetadata(checkNotNull(prevInflateParent));
                 mPrevLayout = null;
-                return renderAndAttach(layout, resources, parent);
+                return renderAndAttach(layout, resources, parent, inflaterStatsLogger);
             }
         } else {
             Log.e(TAG, "postInflate failed.", error);
@@ -1176,6 +1320,7 @@
     private void detachInternal() {
         if (mRenderFuture != null && !mRenderFuture.isDone()) {
             mRenderFuture.cancel(/* mayInterruptIfRunning= */ false);
+            mRenderFuture = null;
         }
         setLayoutVisibility(ProtoLayoutVisibilityState.VISIBILITY_STATE_INVISIBLE);
 
@@ -1227,9 +1372,12 @@
     }
 
     /** Returns true if the layout element depth doesn't exceed the given {@code allowedDepth}. */
-    private void checkLayoutDepth(LayoutElement layoutElement, int allowedDepth) {
+    private void checkLayoutDepth(
+            LayoutElement layoutElement,
+            int allowedDepth,
+            InflaterStatsLogger inflaterStatsLogger) {
         if (allowedDepth <= 0) {
-            throwExceptionForLayoutDepthCheckFailure();
+            handleLayoutDepthCheckFailure(inflaterStatsLogger);
         }
         List<LayoutElement> children = ImmutableList.of();
         switch (layoutElement.getInnerCase()) {
@@ -1245,28 +1393,32 @@
             case ARC:
                 List<ArcLayoutElement> arcElements = layoutElement.getArc().getContentsList();
                 if (!arcElements.isEmpty() && allowedDepth == 1) {
-                    throwExceptionForLayoutDepthCheckFailure();
+                    handleLayoutDepthCheckFailure(inflaterStatsLogger);
                 }
                 for (ArcLayoutElement element : arcElements) {
                     if (element.getInnerCase() == InnerCase.ADAPTER) {
-                        checkLayoutDepth(element.getAdapter().getContent(), allowedDepth - 1);
+                        checkLayoutDepth(
+                                element.getAdapter().getContent(),
+                                allowedDepth - 1,
+                                inflaterStatsLogger);
                     }
                 }
                 break;
             case SPANNABLE:
                 if (layoutElement.getSpannable().getSpansCount() > 0 && allowedDepth == 1) {
-                    throwExceptionForLayoutDepthCheckFailure();
+                    handleLayoutDepthCheckFailure(inflaterStatsLogger);
                 }
                 break;
             default:
                 // Other LayoutElements have depth of one.
         }
         for (LayoutElement child : children) {
-            checkLayoutDepth(child, allowedDepth - 1);
+            checkLayoutDepth(child, allowedDepth - 1, inflaterStatsLogger);
         }
     }
 
-    private void throwExceptionForLayoutDepthCheckFailure() {
+    private void handleLayoutDepthCheckFailure(InflaterStatsLogger inflaterStatsLogger) {
+        inflaterStatsLogger.logInflationFailed(INFLATION_FAILURE_REASON_LAYOUT_DEPTH_EXCEEDED);
         mPrevLayoutAlreadyFailingDepthCheck = true;
         throw new IllegalStateException(
                 "Layout depth exceeds maximum allowed depth: " + MAX_LAYOUT_ELEMENT_DEPTH);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
index 1ca9a23..4997dde 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflater.java
@@ -28,7 +28,7 @@
 import static androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.getParentNodePosId;
 
 import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
-import static com.google.common.util.concurrent.Futures.immediateVoidFuture;
+import static com.google.common.util.concurrent.Futures.immediateFuture;
 
 import static java.lang.Math.max;
 import static java.lang.Math.min;
@@ -180,9 +180,12 @@
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme.FontSet;
 import androidx.wear.protolayout.renderer.R;
 import androidx.wear.protolayout.renderer.common.LoggingUtils;
+import androidx.wear.protolayout.renderer.common.NoOpProviderStatsLogger;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.LayoutDiff;
 import androidx.wear.protolayout.renderer.common.ProtoLayoutDiffer.TreeNodeWithChange;
+import androidx.wear.protolayout.renderer.common.ProviderStatsLogger.InflaterStatsLogger;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.inflater.RenderedMetadata.LayoutInfo;
@@ -301,6 +304,7 @@
     final String mClickableIdExtra;
 
     @Nullable private final LoggingUtils mLoggingUtils;
+    @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
 
     @Nullable final Executor mLoadActionExecutor;
     final LoadActionListener mLoadActionListener;
@@ -528,6 +532,7 @@
         @NonNull private final String mClickableIdExtra;
 
         @Nullable private final LoggingUtils mLoggingUtils;
+        @NonNull private final InflaterStatsLogger mInflaterStatsLogger;
         @Nullable private final ProtoLayoutExtensionViewProvider mExtensionViewProvider;
         private final boolean mAnimationEnabled;
 
@@ -547,6 +552,7 @@
                 @Nullable ProtoLayoutExtensionViewProvider extensionViewProvider,
                 @NonNull String clickableIdExtra,
                 @Nullable LoggingUtils loggingUtils,
+                @NonNull InflaterStatsLogger inflaterStatsLogger,
                 boolean animationEnabled,
                 boolean allowLayoutChangingBindsWithoutDefault,
                 boolean applyFontVariantBodyAsDefault) {
@@ -562,6 +568,7 @@
             this.mAllowLayoutChangingBindsWithoutDefault = allowLayoutChangingBindsWithoutDefault;
             this.mClickableIdExtra = clickableIdExtra;
             this.mLoggingUtils = loggingUtils;
+            this.mInflaterStatsLogger = inflaterStatsLogger;
             this.mExtensionViewProvider = extensionViewProvider;
             this.mApplyFontVariantBodyAsDefault = applyFontVariantBodyAsDefault;
         }
@@ -638,6 +645,12 @@
             return mLoggingUtils;
         }
 
+        /** Stats logger used for telemetry. */
+        @NonNull
+        public InflaterStatsLogger getInflaterStatsLogger() {
+            return mInflaterStatsLogger;
+        }
+
         /** View provider for the renderer extension. */
         @Nullable
         public ProtoLayoutExtensionViewProvider getExtensionViewProvider() {
@@ -678,6 +691,7 @@
             @Nullable private String mClickableIdExtra;
 
             @Nullable private LoggingUtils mLoggingUtils;
+            @Nullable private InflaterStatsLogger mInflaterStatsLogger;
 
             @Nullable private ProtoLayoutExtensionViewProvider mExtensionViewProvider = null;
 
@@ -788,6 +802,14 @@
                 return this;
             }
 
+            /** Sets the stats logger used for telemetry. */
+            @NonNull
+            public Builder setInflaterStatsLogger(
+                    @NonNull InflaterStatsLogger inflaterStatsLogger) {
+                this.mInflaterStatsLogger = inflaterStatsLogger;
+                return this;
+            }
+
             /**
              * Sets whether a "layout changing" data bind can be applied without the
              * "value_for_layout" field being filled in, or being set to zero / empty. Defaults to
@@ -834,7 +856,11 @@
                 if (mClickableIdExtra == null) {
                     mClickableIdExtra = DEFAULT_CLICKABLE_ID_EXTRA;
                 }
-
+                if (mInflaterStatsLogger == null) {
+                    mInflaterStatsLogger =
+                            new NoOpProviderStatsLogger("No implementation was provided")
+                                    .createInflaterStatsLogger();
+                }
                 return new Config(
                         mUiContext,
                         mLayout,
@@ -847,6 +873,7 @@
                         mExtensionViewProvider,
                         checkNotNull(mClickableIdExtra),
                         mLoggingUtils,
+                        mInflaterStatsLogger,
                         mAnimationEnabled,
                         mAllowLayoutChangingBindsWithoutDefault,
                         mApplyFontVariantBodyAsDefault);
@@ -873,6 +900,7 @@
                 config.getAllowLayoutChangingBindsWithoutDefault();
         this.mClickableIdExtra = config.getClickableIdExtra();
         this.mLoggingUtils = config.getLoggingUtils();
+        this.mInflaterStatsLogger = config.getInflaterStatsLogger();
         this.mExtensionViewProvider = config.getExtensionViewProvider();
         this.mApplyFontVariantBodyAsDefault = config.getApplyFontVariantBodyAsDefault();
     }
@@ -1731,7 +1759,9 @@
 
         if (modifiers.hasTransformation()) {
             applyTransformation(
-                    wrapper == null ? view : wrapper, modifiers.getTransformation(), posId,
+                    wrapper == null ? view : wrapper,
+                    modifiers.getTransformation(),
+                    posId,
                     pipelineMaker);
         }
 
@@ -2565,8 +2595,7 @@
                     }
 
                     @Override
-                    public void onViewDetachedFromWindow(@NonNull View v) {
-                    }
+                    public void onViewDetachedFromWindow(@NonNull View v) {}
                 });
     }
 
@@ -3152,7 +3181,7 @@
      *     to the image view; otherwise returns null to indicate the failure of setting drawable.
      */
     @Nullable
-    private static Drawable setImageDrawable(
+    private Drawable setImageDrawable(
             ImageView imageView, Future<Drawable> drawableFuture, String protoResId) {
         try {
             return setImageDrawable(imageView, drawableFuture.get(), protoResId);
@@ -3169,8 +3198,10 @@
      *     null to indicate the failure of setting drawable.
      */
     @Nullable
-    private static Drawable setImageDrawable(
-            ImageView imageView, Drawable drawable, String protoResId) {
+    private Drawable setImageDrawable(ImageView imageView, Drawable drawable, String protoResId) {
+        if (drawable != null) {
+            mInflaterStatsLogger.logDrawableUsage(drawable);
+        }
         if (drawable instanceof BitmapDrawable
                 && ((BitmapDrawable) drawable).getBitmap().getByteCount()
                         > DEFAULT_MAX_BITMAP_RAW_SIZE) {
@@ -3300,7 +3331,7 @@
                     Log.w(
                             TAG,
                             "ArcLine length's value_for_layout is not a positive value. Element"
-                                + " won't be visible.");
+                                    + " won't be visible.");
                 }
                 sizeWrapper.setSweepAngleDegrees(sizeForLayout);
                 sizedLp.setAngularAlignment(
@@ -4079,8 +4110,9 @@
             Optional<ProtoLayoutDynamicDataPipeline.PipelineMaker> pipelineMaker) {
         if (dpProp.hasDynamicValue() && pipelineMaker.isPresent()) {
             try {
-                pipelineMaker.get().addPipelineFor(dpProp, dpProp.getValue(), posId,
-                        dynamicValueConsumer);
+                pipelineMaker
+                        .get()
+                        .addPipelineFor(dpProp, dpProp.getValue(), posId, dynamicValueConsumer);
             } catch (RuntimeException ex) {
                 Log.e(TAG, "Error building pipeline", ex);
                 staticValueConsumer.accept(dpProp.getValue());
@@ -4143,7 +4175,9 @@
                 pipelineMaker
                         .get()
                         .addPipelineFor(
-                                floatProp.getDynamicValue(), floatProp.getValue(), posId,
+                                floatProp.getDynamicValue(),
+                                floatProp.getValue(),
+                                posId,
                                 dynamicValueconsumer);
             } catch (RuntimeException ex) {
                 Log.e(TAG, "Error building pipeline", ex);
@@ -4548,7 +4582,7 @@
     /** Apply the mutation that was previously computed with {@link #computeMutation}. */
     @UiThread
     @NonNull
-    public ListenableFuture<Void> applyMutation(
+    public ListenableFuture<RenderingArtifact> applyMutation(
             @NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
         RenderedMetadata prevRenderedMetadata = getRenderedMetadata(prevInflatedParent);
         if (prevRenderedMetadata != null
@@ -4561,11 +4595,11 @@
         }
         if (groupMutation.isNoOp()) {
             // Nothing to do.
-            return immediateVoidFuture();
+            return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
         }
 
         if (groupMutation.mPipelineMaker.isPresent()) {
-            SettableFuture<Void> result = SettableFuture.create();
+            SettableFuture<RenderingArtifact> result = SettableFuture.create();
             groupMutation
                     .mPipelineMaker
                     .get()
@@ -4575,7 +4609,7 @@
                             () -> {
                                 try {
                                     applyMutationInternal(prevInflatedParent, groupMutation);
-                                    result.set(null);
+                                    result.set(RenderingArtifact.create(mInflaterStatsLogger));
                                 } catch (ViewMutationException ex) {
                                     result.setException(ex);
                                 }
@@ -4584,7 +4618,7 @@
         } else {
             try {
                 applyMutationInternal(prevInflatedParent, groupMutation);
-                return immediateVoidFuture();
+                return immediateFuture(RenderingArtifact.create(mInflaterStatsLogger));
             } catch (ViewMutationException ex) {
                 return immediateFailedFuture(ex);
             }
@@ -4593,6 +4627,7 @@
 
     private void applyMutationInternal(
             @NonNull ViewGroup prevInflatedParent, @NonNull ViewGroupMutation groupMutation) {
+        mInflaterStatsLogger.logMutationChangedNodes(groupMutation.mInflatedViews.size());
         for (InflatedView inflatedView : groupMutation.mInflatedViews) {
             String posId = inflatedView.getTag();
             if (posId == null) {
@@ -4620,15 +4655,21 @@
             }
             // Remove the touch delegate to the view to be updated
             if (immediateParent.getTouchDelegate() != null) {
-                ((TouchDelegateComposite) immediateParent.getTouchDelegate())
-                        .removeDelegate(viewToUpdate);
+                TouchDelegateComposite delegateComposite =
+                        (TouchDelegateComposite) immediateParent.getTouchDelegate();
+                delegateComposite.removeDelegate(viewToUpdate);
 
                 // Make sure to remove the touch delegate when the actual clickable view is wrapped,
                 // for example ImageView inside the RatioViewWrapper
                 if (viewToUpdate instanceof ViewGroup
                         && ((ViewGroup) viewToUpdate).getChildCount() > 0) {
-                    ((TouchDelegateComposite) immediateParent.getTouchDelegate())
-                            .removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+                    delegateComposite.removeDelegate(((ViewGroup) viewToUpdate).getChildAt(0));
+                }
+
+                // If no more touch delegate left in the composite, remove it completely from the
+                // parent
+                if (delegateComposite.isEmpty()) {
+                    immediateParent.setTouchDelegate(null);
                 }
             }
             immediateParent.removeViewAt(childIndex);
diff --git a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
index e6bf97c..6184ac8 100644
--- a/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
+++ b/wear/protolayout/protolayout-renderer/src/main/java/androidx/wear/protolayout/renderer/inflater/TouchDelegateComposite.java
@@ -83,6 +83,10 @@
         mDelegates.remove(delegateView);
     }
 
+    boolean isEmpty() {
+        return mDelegates.isEmpty();
+    }
+
     @Override
     public boolean onTouchEvent(@NonNull MotionEvent event) {
         boolean eventForwarded = false;
@@ -125,7 +129,7 @@
     @Override
     @NonNull
     public AccessibilityNodeInfo.TouchDelegateInfo getTouchDelegateInfo() {
-        if (VERSION.SDK_INT >= VERSION_CODES.Q) {
+        if (VERSION.SDK_INT >= VERSION_CODES.Q && !mDelegates.isEmpty()) {
             Map<Region, View> targetMap = new ArrayMap<>(mDelegates.size());
             for (Map.Entry<View, DelegateInfo> entry : mDelegates.entrySet()) {
                 AccessibilityNodeInfo.TouchDelegateInfo info =
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
index 6db2dfb..d095d58 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/impl/ProtoLayoutViewInstanceTest.java
@@ -53,6 +53,7 @@
 import androidx.wear.protolayout.expression.pipeline.StateStore;
 import androidx.wear.protolayout.proto.LayoutElementProto.Layout;
 import androidx.wear.protolayout.proto.ResourceProto.Resources;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
 import androidx.wear.protolayout.renderer.helper.TestDsl.LayoutNode;
 import androidx.wear.protolayout.renderer.impl.ProtoLayoutViewInstance.Config;
 
@@ -97,8 +98,8 @@
     @Test
     public void adaptiveUpdateRatesDisabled_attach_reinflatesCompletely() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -108,7 +109,7 @@
         assertThat(layout1).hasSize(1);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -120,8 +121,8 @@
     @Test
     public void adaptiveUpdateRatesEnabled_attach_appliesDiffOnly() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -131,7 +132,7 @@
         assertThat(layout1).hasSize(1);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -144,8 +145,8 @@
     @Test
     public void reattach_usesCachedLayoutForDiffUpdate() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -156,7 +157,7 @@
         mInstanceUnderTest.detach(mRootContainer);
 
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -172,8 +173,8 @@
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
 
         // First one that does the full layout update.
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -184,7 +185,7 @@
 
         // Second one that applies mutation only.
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, mRootContainer);
         // Detach so it can't apply update.
         mInstanceUnderTest.detach(mRootContainer);
@@ -200,9 +201,8 @@
 
         // Render the first layout.
         Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
-                        layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -227,9 +227,7 @@
         // not changed part of the layout was also changed in inflated View.
         Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
 
-        result =
-                mInstanceUnderTest.renderAndAttach(
-                        layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
 
         // Make sure future is computing result.
         assertThat(result.isDone()).isFalse();
@@ -252,8 +250,8 @@
 
         // Render the first layout.
         Layout layout1 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT2)));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -263,7 +261,7 @@
         assertThat(findViewsWithText(mRootContainer, TEXT2)).hasSize(1);
 
         Layout layout2 = layout(column(dynamicFixedText(TEXT1), dynamicFixedText(TEXT3)));
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
         // Make sure future is computing result.
         assertThat(result.isDone()).isFalse();
         shadowOf(Looper.getMainLooper()).idle();
@@ -279,13 +277,13 @@
     public void adaptiveUpdateRatesEnabled_ongoingRendering_skipsPreviousLayout() {
         FrameLayout container = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result1 =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result1 =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container);
         assertThat(result1.isDone()).isFalse();
 
-        ListenableFuture<Void> result2 =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result2 =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT3))), RESOURCES, container);
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -301,14 +299,14 @@
         FrameLayout container1 = new FrameLayout(mApplicationContext);
         FrameLayout container2 = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container1);
 
         assertThrows(
                 IllegalStateException.class,
                 () ->
-                        mInstanceUnderTest.renderAndAttach(
+                        mInstanceUnderTest.renderLayoutAndAttach(
                                 layout(column(text(TEXT1), text(TEXT2))), RESOURCES, container2));
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -321,12 +319,14 @@
         FrameLayout container1 = new FrameLayout(mApplicationContext);
         FrameLayout container2 = new FrameLayout(mApplicationContext);
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result1 =
-                mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container1);
+        ListenableFuture<RenderingArtifact> result1 =
+                mInstanceUnderTest.renderLayoutAndAttach(
+                        layout(text(TEXT1)), RESOURCES, container1);
         mInstanceUnderTest.detach(container1);
 
-        ListenableFuture<Void> result2 =
-                mInstanceUnderTest.renderAndAttach(layout(text(TEXT1)), RESOURCES, container2);
+        ListenableFuture<RenderingArtifact> result2 =
+                mInstanceUnderTest.renderLayoutAndAttach(
+                        layout(text(TEXT1)), RESOURCES, container2);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertThat(result1.isCancelled()).isTrue();
@@ -341,13 +341,13 @@
             throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
 
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
@@ -360,14 +360,14 @@
         Layout layout1 = layout(text(TEXT1));
         Layout layout2 = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
 
         // Make sure we have an UnchangedRenderResult
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -377,7 +377,7 @@
         assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
         shadowOf(Looper.getMainLooper()).idle();
 
-        result = mInstanceUnderTest.renderAndAttach(layout2, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, RESOURCES, mRootContainer);
 
         assertThat(result.isDone()).isTrue();
         assertNoException(result);
@@ -388,8 +388,8 @@
     public void fullInflationResultCanBeReused() throws Exception {
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
         Layout layout = layout(text(TEXT1));
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -397,7 +397,7 @@
         ListenableFuture<?> renderFuture = mInstanceUnderTest.mRenderFuture;
 
         mInstanceUnderTest.detach(mRootContainer);
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
@@ -409,15 +409,15 @@
             throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         List<View> textViews1 = findViewsWithText(mRootContainer, TEXT1);
         assertThat(textViews1).hasSize(1);
 
         mInstanceUnderTest.close();
-        result = mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
 
         assertThat(shadowOf(Looper.getMainLooper()).isIdle()).isFalse();
         shadowOf(Looper.getMainLooper()).idle();
@@ -431,8 +431,8 @@
     public void detach_clearsHostView() throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -449,14 +449,14 @@
         Layout layout2 = layout(text(TEXT1));
         Resources resources2 = Resources.newBuilder().setVersion("2").build();
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
         View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
 
-        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -472,15 +472,15 @@
         Layout layout2 = layout(text(TEXT1));
         Resources resources2 = Resources.newBuilder().setVersion("1").build();
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout1, resources1, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
         View view1 = findViewsWithText(mRootContainer, TEXT1).get(0);
 
         mInstanceUnderTest.invalidateCache();
-        result = mInstanceUnderTest.renderAndAttach(layout2, resources2, mRootContainer);
+        result = mInstanceUnderTest.renderLayoutAndAttach(layout2, resources2, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
 
         assertNoException(result);
@@ -490,13 +490,28 @@
     }
 
     @Test
+    public void invalidateCache_ongoingInflation_oldInflationGetsCancelled() throws Exception {
+        Layout layout1 = layout(text(TEXT1));
+        Resources resources1 = Resources.newBuilder().setVersion("1").build();
+        setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout1, resources1, mRootContainer);
+
+        mInstanceUnderTest.invalidateCache();
+        shadowOf(Looper.getMainLooper()).idle();
+
+        assertThat(result.isCancelled()).isTrue();
+        assertThat(findViewsWithText(mRootContainer, TEXT1)).isEmpty();
+    }
+
+    @Test
     public void adaptiveUpdateRatesEnabled_rootElementdiff_keepsElementCentered() throws Exception {
         int dimension = 50;
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
 
         // Full inflation.
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(
                                 column(
                                         props -> {
@@ -518,7 +533,7 @@
 
         // Diff update only for the root element.
         result =
-                mInstanceUnderTest.renderAndAttach(
+                mInstanceUnderTest.renderLayoutAndAttach(
                         layout(
                                 column(
                                         props -> {
@@ -546,8 +561,8 @@
     public void close_clearsHostView() throws Exception {
         Layout layout = layout(text(TEXT1));
         setupInstance(/* adaptiveUpdateRatesEnabled= */ true);
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
         assertThat(findViewsWithText(mRootContainer, TEXT1)).hasSize(1);
@@ -562,7 +577,9 @@
         setupInstance(/* adaptiveUpdateRatesEnabled= */ false);
         assertThrows(
                 ExecutionException.class,
-                () -> renderAndAttachLayout(layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
+                () ->
+                        renderLayoutAndAttachLayout(
+                                layout(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH + 1))));
     }
 
     @Test
@@ -573,8 +590,8 @@
         for (int i = 0; i < children.length; i++) {
             children[i] = recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1);
         }
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // MAX_LAYOUT_ELEMENT_DEPTH branches of depth MAX_LAYOUT_ELEMENT_DEPTH - 1.
                         // Total depth is MAX_LAYOUT_ELEMENT_DEPTH (if we count the head).
                         layout(box(children)), RESOURCES, mRootContainer);
@@ -591,7 +608,7 @@
         assertThrows(
                 ExecutionException.class,
                 () ->
-                        renderAndAttachLayout(
+                        renderLayoutAndAttachLayout(
                                 // Total number of views is = MAX_LAYOUT_ELEMENT_DEPTH  + 1 (span
                                 // text)
                                 layout(
@@ -599,8 +616,8 @@
                                                 MAX_LAYOUT_ELEMENT_DEPTH,
                                                 spannable(spanText("Hello"))))));
 
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // Total number of views is = (MAX_LAYOUT_ELEMENT_DEPTH -1)  + 1 (span text)
                         layout(
                                 recursiveBox(
@@ -620,12 +637,12 @@
         assertThrows(
                 ExecutionException.class,
                 () ->
-                        renderAndAttachLayout(
+                        renderLayoutAndAttachLayout(
                                 // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH)
                                 layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH))))));
 
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(
                         // Total number of views is = 1 (Arc) + (MAX_LAYOUT_ELEMENT_DEPTH - 1)
                         // = MAX_LAYOUT_ELEMENT_DEPTH
                         layout(arc(arcAdapter(recursiveBox(MAX_LAYOUT_ELEMENT_DEPTH - 1)))),
@@ -637,9 +654,9 @@
         assertThat(mRootContainer.getChildCount()).isEqualTo(1);
     }
 
-    private void renderAndAttachLayout(Layout layout) throws Exception {
-        ListenableFuture<Void> result =
-                mInstanceUnderTest.renderAndAttach(layout, RESOURCES, mRootContainer);
+    private void renderLayoutAndAttachLayout(Layout layout) throws Exception {
+        ListenableFuture<RenderingArtifact> result =
+                mInstanceUnderTest.renderLayoutAndAttach(layout, RESOURCES, mRootContainer);
         shadowOf(Looper.getMainLooper()).idle();
         assertNoException(result);
     }
@@ -690,7 +707,8 @@
         return views;
     }
 
-    private static void assertNoException(ListenableFuture<Void> result) throws Exception {
+    private static void assertNoException(ListenableFuture<RenderingArtifact> result)
+            throws Exception {
         // Assert that result hasn't thrown exception.
         result.get();
     }
diff --git a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
index da07985..1a46140 100644
--- a/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
+++ b/wear/protolayout/protolayout-renderer/src/test/java/androidx/wear/protolayout/renderer/inflater/ProtoLayoutInflaterTest.java
@@ -83,7 +83,6 @@
 import androidx.core.content.ContextCompat;
 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.expression.AppDataKey;
 import androidx.wear.protolayout.expression.DynamicBuilders;
 import androidx.wear.protolayout.expression.pipeline.FixedQuotaManagerImpl;
@@ -203,6 +202,8 @@
 import androidx.wear.protolayout.proto.TypesProto.StringProp;
 import androidx.wear.protolayout.protobuf.ByteString;
 import androidx.wear.protolayout.renderer.ProtoLayoutTheme;
+import androidx.wear.protolayout.renderer.common.RenderingArtifact;
+import androidx.wear.protolayout.renderer.common.SeekableAnimatedVectorDrawable;
 import androidx.wear.protolayout.renderer.dynamicdata.ProtoLayoutDynamicDataPipeline;
 import androidx.wear.protolayout.renderer.helper.TestFingerprinter;
 import androidx.wear.protolayout.renderer.inflater.ProtoLayoutInflater.InflateResult;
@@ -222,6 +223,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.Robolectric;
+import org.robolectric.annotation.Config;
 import org.robolectric.shadows.ShadowChoreographer;
 import org.robolectric.shadows.ShadowLooper;
 import org.robolectric.shadows.ShadowPackageManager;
@@ -543,37 +545,41 @@
         int width = 10;
         int height = 12;
         byte[] payload = "Hello World".getBytes(UTF_8);
+        LayoutElement extension =
+                LayoutElement.newBuilder()
+                        .setExtension(
+                                ExtensionLayoutElement.newBuilder()
+                                        .setExtensionId("foo")
+                                        .setPayload(ByteString.copyFrom(payload))
+                                        .setWidth(
+                                                ExtensionDimension.newBuilder()
+                                                        .setLinearDimension(dp(width))
+                                                        .build())
+                                        .setHeight(
+                                                ExtensionDimension.newBuilder()
+                                                        .setLinearDimension(dp(height))
+                                                        .build()))
+                        .build();
         LayoutElement root =
                 LayoutElement.newBuilder()
-                    .setBox(
-                        Box.newBuilder()
-                            // Outer box's width and height left at default value of "wrap"
-                            .addContents(
-                                LayoutElement.newBuilder()
-                                    .setExtension(
-                                        ExtensionLayoutElement.newBuilder()
-                                            .setExtensionId("foo")
-                                            .setPayload(ByteString.copyFrom(payload))
-                                            .setWidth(
-                                                ExtensionDimension.newBuilder()
-                                                    .setLinearDimension(dp(width))
-                                                    .build())
-                                            .setHeight(
-                                                ExtensionDimension.newBuilder()
-                                                    .setLinearDimension(dp(height))
-                                                    .build()))))
+                        .setBox(
+                                Box.newBuilder()
+                                        // Outer box's width and height left at default value of
+                                        // "wrap"
+                                        .addContents(extension))
                         .build();
 
         FrameLayout rootLayout =
                 renderer(
-                    newRendererConfigBuilder(fingerprintedLayout(root))
-                        .setExtensionViewProvider(
-                            (extensionPayload, id) -> {
-                                TextView returnedView = new TextView(getApplicationContext());
-                                returnedView.setText("testing");
+                                newRendererConfigBuilder(fingerprintedLayout(root))
+                                        .setExtensionViewProvider(
+                                                (extensionPayload, id) -> {
+                                                    TextView returnedView =
+                                                            new TextView(getApplicationContext());
+                                                    returnedView.setText("testing");
 
-                                return returnedView;
-                            }))
+                                                    return returnedView;
+                                                }))
                         .inflate();
 
         // Check that the outer box is displayed and it has a child.
@@ -976,6 +982,23 @@
 
         // A column with a row (Spacer + Spacer) and Spacer, everything has weighted expand
         // dimension.
+
+        Row rowWithSpacers =
+                Row.newBuilder()
+                        .setWidth(expand())
+                        .setHeight(
+                                ContainerDimension.newBuilder()
+                                        .setExpandedDimension(expandWithWeight(heightWeight1))
+                                        .build())
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setSpacer(
+                                                buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setSpacer(
+                                                buildExpandedSpacer(widthWeight2, DEFAULT_WEIGHT)))
+                        .build();
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setColumn(
@@ -983,26 +1006,13 @@
                                         .setWidth(expand())
                                         .setHeight(expand())
                                         .addContents(
-                                                LayoutElement.newBuilder()
-                                                        .setRow(
-                                                                Row.newBuilder()
-                                                                        .setWidth(expand())
-                                                                        .setHeight(
-                                                                                ContainerDimension.newBuilder()
-                                                                                        .setExpandedDimension(expandWithWeight(heightWeight1))
-                                                                                        .build())
-                                                                        .addContents(
-                                                                                LayoutElement.newBuilder()
-                                                                                        .setSpacer(
-                                                                                                buildExpandedSpacer(widthWeight1, DEFAULT_WEIGHT)))
-                                                                        .addContents(
-                                                                                LayoutElement.newBuilder()
-                                                                                        .setSpacer(
-                                                                                                buildExpandedSpacer(
-                                                                                                        widthWeight2, DEFAULT_WEIGHT)))))
+                                                LayoutElement.newBuilder().setRow(rowWithSpacers))
                                         .addContents(
                                                 LayoutElement.newBuilder()
-                                                        .setSpacer(buildExpandedSpacer(DEFAULT_WEIGHT, heightWeight2)))
+                                                        .setSpacer(
+                                                                buildExpandedSpacer(
+                                                                        DEFAULT_WEIGHT,
+                                                                        heightWeight2)))
                                         .build())
                         .build();
 
@@ -1034,9 +1044,9 @@
                                                         .setExpandedDimension(
                                                                 ExpandedDimensionProp
                                                                         .getDefaultInstance()))
-                                        .setWidth(SpacerDimension
-                                                .newBuilder()
-                                                .setLinearDimension(dp(width))))
+                                        .setWidth(
+                                                SpacerDimension.newBuilder()
+                                                        .setLinearDimension(dp(width))))
                         .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
@@ -1543,17 +1553,17 @@
                 ContainerDimension.newBuilder().setLinearDimension(dp(childSize)).build();
 
         LayoutElement childBox =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(childBoxSize)
-                                .setHeight(childBoxSize)
-                                .setModifiers(
-                                        Modifiers.newBuilder()
-                                                .setClickable(
-                                                        Clickable.newBuilder()
-                                                                .setId("foo")
-                                                                .setOnClick(
-                                                                        action))))
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(childBoxSize)
+                                        .setHeight(childBoxSize)
+                                        .setModifiers(
+                                                Modifiers.newBuilder()
+                                                        .setClickable(
+                                                                Clickable.newBuilder()
+                                                                        .setId("foo")
+                                                                        .setOnClick(action))))
                         .build();
 
         LayoutElement root =
@@ -1563,13 +1573,14 @@
                                         .setWidth(parentBoxSize)
                                         .setHeight(parentBoxSize)
                                         .addContents(childBox))
-                                        .build();
+                        .build();
 
         State.Builder receivedState = State.newBuilder();
         FrameLayout rootLayout =
                 renderer(
-                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
-                                .setLoadActionListener(receivedState::mergeFrom))
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
                         .inflate();
         shadowOf(Looper.getMainLooper()).idle();
 
@@ -1627,8 +1638,7 @@
                                         .setWidth(
                                                 SpacerDimension.newBuilder()
                                                         .setLinearDimension(dp(spacerSize))
-                                                        .build()
-                                        ));
+                                                        .build()));
 
         //           |--clickable area child box 1 (5 - 35)--|
         //                                          |---clickable area child box 2 (30-60)--|
@@ -1647,8 +1657,7 @@
                                                                                 dp(clickTargetSize))
                                                                         .setMinimumClickableHeight(
                                                                                 dp(clickTargetSize))
-                                                                        .setOnClick(
-                                                                                action)
+                                                                        .setOnClick(action)
                                                                         .setId("foo1"))))
                         .build();
 
@@ -1666,8 +1675,7 @@
                                                                                 dp(clickTargetSize))
                                                                         .setMinimumClickableHeight(
                                                                                 dp(clickTargetSize))
-                                                                        .setOnClick(
-                                                                                action)
+                                                                        .setOnClick(action)
                                                                         .setId("foo2"))))
                         .build();
 
@@ -1692,8 +1700,9 @@
         State.Builder receivedState = State.newBuilder();
         FrameLayout rootLayout =
                 renderer(
-                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
-                                .setLoadActionListener(receivedState::mergeFrom))
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
                         .inflate();
 
         ShadowLooper.runUiThreadTasks();
@@ -1812,8 +1821,8 @@
 
         // Compute the mutation
         ViewGroupMutation mutation =
-                renderer.computeMutation(getRenderedMetadata(rootLayout),
-                        fingerprintedLayout(root2));
+                renderer.computeMutation(
+                        getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
         assertThat(mutation).isNotNull();
         assertThat(mutation.isNoOp()).isFalse();
 
@@ -1848,8 +1857,9 @@
                         .setSpacer(
                                 Spacer.newBuilder()
                                         .setWidth(
-                                                SpacerDimension.newBuilder().setLinearDimension(
-                                                        dp(spacerSize)).build()));
+                                                SpacerDimension.newBuilder()
+                                                        .setLinearDimension(dp(spacerSize))
+                                                        .build()));
 
         int parentHeight = 45;
         int parentWidth = 125;
@@ -1916,13 +1926,13 @@
 
         // Compute the mutation
         ViewGroupMutation mutation =
-                renderer.computeMutation(getRenderedMetadata(rootLayout),
-                        fingerprintedLayout(root2));
+                renderer.computeMutation(
+                        getRenderedMetadata(rootLayout), fingerprintedLayout(root2));
         assertThat(mutation).isNotNull();
         assertThat(mutation.isNoOp()).isFalse();
 
         // Apply the mutation
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(rootLayout, mutation);
         shadowOf(getMainLooper()).idle();
         try {
@@ -1958,6 +1968,108 @@
     }
 
     @Test
+    @Config(minSdk = VERSION_CODES.Q)
+    public void inflateThenMutate_withClickableSizeChange_clickableModifier_extendClickTargetSize()
+    {
+        Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+        int parentSize = 50;
+        ContainerDimension parentBoxSize =
+                ContainerDimension.newBuilder().setLinearDimension(dp(parentSize)).build();
+        ContainerDimension childBoxSize =
+                ContainerDimension.newBuilder().setLinearDimension(dp(parentSize / 2f)).build();
+
+        Modifiers testModifiers1 =
+                Modifiers.newBuilder()
+                        .setClickable(Clickable.newBuilder().setOnClick(action).setId("foo1"))
+                        .build();
+
+        // Child box has a size smaller than the minimum clickable size, touch delegation is
+        // required.
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(parentBoxSize)
+                                        .setHeight(parentBoxSize)
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setBox(
+                                                                Box.newBuilder()
+                                                                        .setWidth(childBoxSize)
+                                                                        .setHeight(childBoxSize)
+                                                                        .setModifiers(
+                                                                                testModifiers1))))
+                        .build();
+
+        State.Builder receivedState = State.newBuilder();
+        Renderer renderer =
+                renderer(
+                        newRendererConfigBuilder(fingerprintedLayout(root), resourceResolvers())
+                                .setLoadActionListener(receivedState::mergeFrom));
+        FrameLayout rootLayout = renderer.inflate();
+        ViewGroup parent = (ViewGroup) rootLayout.getChildAt(0);
+        // Confirm the touch delegation has happened.
+        assertThat(parent.getTouchDelegate()).isNotNull();
+        // Dispatch a click event to the parent View within the expanded clickable area;
+        // it should trigger the LoadAction...
+        receivedState.clearLastClickableId();
+        dispatchTouchEvent(parent, 5, 5);
+        expect.that(receivedState.getLastClickableId()).isEqualTo("foo1");
+
+        // Produce a new layout with child box specifies its minimum clickable size, NO touch
+        // delegation is required.
+        Modifiers testModifiers2 =
+                Modifiers.newBuilder()
+                        .setClickable(
+                                Clickable.newBuilder()
+                                        .setOnClick(action)
+                                        .setId("foo2")
+                                        .setMinimumClickableWidth(dp(parentSize / 2f))
+                                        .setMinimumClickableHeight(dp(parentSize / 2f)))
+                        .build();
+        LayoutElement root2 =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(parentBoxSize)
+                                        .setHeight(parentBoxSize)
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setBox(
+                                                                Box.newBuilder()
+                                                                        .setWidth(childBoxSize)
+                                                                        .setHeight(childBoxSize)
+                                                                        .setModifiers(
+                                                                                testModifiers2))))
+                        .build();
+
+        // Compute the mutation
+        ViewGroupMutation mutation =
+                renderer.computeMutation(getRenderedMetadata(rootLayout),
+                        fingerprintedLayout(root2));
+        assertThat(mutation).isNotNull();
+        assertThat(mutation.isNoOp()).isFalse();
+
+        // Apply the mutation
+        boolean mutationResult = renderer.applyMutation(rootLayout, mutation);
+        assertThat(mutationResult).isTrue();
+
+        // Verify that the parent removed the touch delegation.
+        // Keep an empty touch delegate composite will lead to failure when calling
+        // {@link TouchDelegateComposite#getTouchDelegateInfo}
+        assertThat(parent.getTouchDelegate()).isNull();
+
+        // Dispatch a click event to the parent View within the expanded clickable area;
+        // it should no longer trigger the LoadAction.
+        receivedState.clearLastClickableId();
+        dispatchTouchEvent(parent, 5, 5);
+        expect.that(receivedState.getLastClickableId()).isEmpty();
+        View box = parent.getChildAt(0);
+        dispatchTouchEvent(box, 1, 1);
+        expect.that(receivedState.getLastClickableId()).isEqualTo("foo2");
+    }
+
+    @Test
     public void inflate_clickable_withoutRippleEffect_rippleDrawableNotAdded() throws IOException {
         final String textContentsWithRipple = "clickable with ripple";
         final String textContentsWithoutRipple = "clickable without ripple";
@@ -1993,15 +2105,15 @@
 
         FrameLayout rootLayout =
                 renderer(
-                        fingerprintedLayout(
-                                LayoutElement.newBuilder()
-                                        .setColumn(
-                                                Column.newBuilder()
-                                                        .addContents(textElementWithRipple)
-                                                        .addContents(
-                                                                textElementWithoutRipple)
-                                                        .build())
-                                        .build()))
+                                fingerprintedLayout(
+                                        LayoutElement.newBuilder()
+                                                .setColumn(
+                                                        Column.newBuilder()
+                                                                .addContents(textElementWithRipple)
+                                                                .addContents(
+                                                                        textElementWithoutRipple)
+                                                                .build())
+                                                .build()))
                         .inflate();
 
         // Column
@@ -2025,6 +2137,49 @@
     }
 
     @Test
+    public void inflate_hiddenModifier_inhibitsClicks() {
+        final String textContents = "I am a clickable";
+
+        Action action = Action.newBuilder().setLoadAction(LoadAction.getDefaultInstance()).build();
+
+        LayoutElement root =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setText(
+                                                                createTextWithVisibility(
+                                                                        textContents,
+                                                                        "back",
+                                                                        action,
+                                                                        true)))
+                                        .addContents(
+                                                LayoutElement.newBuilder()
+                                                        .setText(
+                                                                createTextWithVisibility(
+                                                                        textContents,
+                                                                        "front",
+                                                                        action,
+                                                                        false))))
+                        .build();
+
+        State.Builder receivedState = State.newBuilder();
+        FrameLayout rootLayout =
+                renderer(
+                                newRendererConfigBuilder(
+                                                fingerprintedLayout(root), resourceResolvers())
+                                        .setLoadActionListener(receivedState::mergeFrom))
+                        .inflate();
+
+        // Try to tap the stacked clickables.
+        dispatchTouchEvent(rootLayout, 5f, 5f);
+        shadowOf(Looper.getMainLooper()).idle();
+
+        expect.that(receivedState.getLastClickableId()).isEqualTo("back");
+    }
+
+    @Test
     public void inflate_arc_withLineDrawnWithArcTo() {
         LayoutElement root =
                 LayoutElement.newBuilder()
@@ -2151,8 +2306,8 @@
     @Test
     public void inflate_arc_withText_autoSize_notSet() {
         int lastSize = 12;
-        FontStyle.Builder style = FontStyle.newBuilder()
-                .addAllSize(buildSizesList(new int[]{10, 20, lastSize}));
+        FontStyle.Builder style =
+                FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {10, 20, lastSize}));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setArc(
@@ -2889,12 +3044,13 @@
                         .setFontStyle(FontStyle.newBuilder().addSize(sp(16)))
                         .setMaxLines(Int32Prop.newBuilder().setValue(6))
                         .setOverflow(
-                                TextOverflowProp.newBuilder().setValue(
-                                        TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+                                TextOverflowProp.newBuilder()
+                                        .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
         Layout layout1 =
                 fingerprintedLayout(
                         LayoutElement.newBuilder()
-                                .setBox(buildFixedSizeBoxWIthText(text1)).build());
+                                .setBox(buildFixedSizeBoxWIthText(text1))
+                                .build());
 
         Text.Builder text2 =
                 Text.newBuilder()
@@ -2904,18 +3060,19 @@
                         .setFontStyle(FontStyle.newBuilder().addSize(sp(4)))
                         .setMaxLines(Int32Prop.newBuilder().setValue(6))
                         .setOverflow(
-                                TextOverflowProp.newBuilder().setValue(
-                                        TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
+                                TextOverflowProp.newBuilder()
+                                        .setValue(TextOverflow.TEXT_OVERFLOW_ELLIPSIZE));
         Layout layout2 =
                 fingerprintedLayout(
                         LayoutElement.newBuilder()
-                                .setBox(buildFixedSizeBoxWIthText(text2)).build());
+                                .setBox(buildFixedSizeBoxWIthText(text2))
+                                .build());
 
         // Initial layout.
         Renderer renderer = renderer(layout1);
         ViewGroup inflatedViewParent = renderer.inflate();
-        TextView textView1 = (TextView) ((ViewGroup) inflatedViewParent
-                .getChildAt(0)).getChildAt(0);
+        TextView textView1 =
+                (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
 
         // Apply the mutation.
         ViewGroupMutation mutation =
@@ -2926,8 +3083,8 @@
         assertThat(mutationResult).isTrue();
 
         // This contains layout after the mutation.
-        TextView textView2 = (TextView) ((ViewGroup) inflatedViewParent
-                .getChildAt(0)).getChildAt(0);
+        TextView textView2 =
+                (TextView) ((ViewGroup) inflatedViewParent.getChildAt(0)).getChildAt(0);
 
         expect.that(textView1.getEllipsize()).isEqualTo(TruncateAt.END);
         expect.that(textView1.getMaxLines()).isEqualTo(2);
@@ -3028,7 +3185,7 @@
     @Test
     public void inflate_textView_autosize_set() {
         String text = "Test text";
-        int[] presetSizes = new int[]{12, 20, 10};
+        int[] presetSizes = new int[] {12, 20, 10};
         List<DimensionProto.SpProp> sizes = buildSizesList(presetSizes);
 
         LayoutElement textElement =
@@ -3036,16 +3193,16 @@
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3076,16 +3233,16 @@
                                 Text.newBuilder()
                                         .setText(string(text))
                                         .setMaxLines(Int32Prop.newBuilder().setValue(4))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3099,23 +3256,23 @@
     public void inflate_textView_autosize_notSet() {
         String text = "Test text";
         int size = 24;
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{size});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {size});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ViewGroup firstChild = (ViewGroup) rootLayout.getChildAt(0);
@@ -3129,23 +3286,23 @@
     public void inflate_textView_autosize_setDynamic_noop() {
         String text = "Test text";
         int lastSize = 24;
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{10, 30, lastSize});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {10, 30, lastSize});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(dynamicString(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ArrayList<View> textChildren = new ArrayList<>();
@@ -3159,23 +3316,23 @@
     @Test
     public void inflate_textView_autosize_wrongSizes_noop() {
         String text = "Test text";
-        List<DimensionProto.SpProp> sizes = buildSizesList(new int[]{0, -2, 0});
+        List<DimensionProto.SpProp> sizes = buildSizesList(new int[] {0, -2, 0});
 
         LayoutElement textElement =
                 LayoutElement.newBuilder()
                         .setText(
                                 Text.newBuilder()
                                         .setText(string(text))
-                                        .setFontStyle(
-                                                FontStyle.newBuilder()
-                                                        .addAllSize(sizes)))
+                                        .setFontStyle(FontStyle.newBuilder().addAllSize(sizes)))
                         .build();
         LayoutElement root =
-                LayoutElement.newBuilder().setBox(
-                        Box.newBuilder()
-                                .setWidth(expand())
-                                .setHeight(expand())
-                                .addContents(textElement)).build();
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .setWidth(expand())
+                                        .setHeight(expand())
+                                        .addContents(textElement))
+                        .build();
 
         FrameLayout rootLayout = renderer(fingerprintedLayout(root)).inflate();
         ArrayList<View> textChildren = new ArrayList<>();
@@ -3219,8 +3376,8 @@
     public void inflate_spantext_ignoresMultipleSizes() {
         String text = "Test text";
         int firstSize = 12;
-        FontStyle.Builder style = FontStyle.newBuilder()
-                .addAllSize(buildSizesList(new int[]{firstSize, 10, 20}));
+        FontStyle.Builder style =
+                FontStyle.newBuilder().addAllSize(buildSizesList(new int[] {firstSize, 10, 20}));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setSpannable(
@@ -4780,7 +4937,7 @@
 
         boolean applyMutation(ViewGroup parent, ViewGroupMutation mutation) {
             try {
-                ListenableFuture<Void> applyMutationFuture =
+                ListenableFuture<RenderingArtifact> applyMutationFuture =
                         mRenderer.applyMutation(parent, mutation);
                 shadowOf(Looper.getMainLooper()).idle();
                 applyMutationFuture.get();
@@ -5000,33 +5157,25 @@
 
         LayoutElement image = buildImage(protoResId, 30, 30);
 
-
-        BoolProp.Builder stateBoolPropBuilder = BoolProp
-                .newBuilder()
-                .setValue(
-                        true)
-                .setDynamicValue(
-                        DynamicBool
-                                .newBuilder()
-                                .setStateSource(
-                                        StateBoolSource
-                                                .newBuilder()
-                                                .setSourceKey(
-                                                        boolKey)));
-        LayoutElement.Builder boxBuilder = LayoutElement.newBuilder()
-                .setBox(
-                        Box.newBuilder()
-                                .addContents(image)
-                                .setModifiers(
-                                        Modifiers
-                                                .newBuilder()
-                                                .setHidden(stateBoolPropBuilder)));
+        BoolProp.Builder stateBoolPropBuilder =
+                BoolProp.newBuilder()
+                        .setValue(true)
+                        .setDynamicValue(
+                                DynamicBool.newBuilder()
+                                        .setStateSource(
+                                                StateBoolSource.newBuilder()
+                                                        .setSourceKey(boolKey)));
+        LayoutElement.Builder boxBuilder =
+                LayoutElement.newBuilder()
+                        .setBox(
+                                Box.newBuilder()
+                                        .addContents(image)
+                                        .setModifiers(
+                                                Modifiers.newBuilder()
+                                                        .setHidden(stateBoolPropBuilder)));
         LayoutElement root =
                 LayoutElement.newBuilder()
-                        .setRow(
-                                Row.newBuilder()
-                                        .addContents(boxBuilder)
-                                        .addContents(image))
+                        .setRow(Row.newBuilder().addContents(boxBuilder).addContents(image))
                         .build();
 
         FrameLayout layout = renderer(fingerprintedLayout(root)).inflate();
@@ -5059,7 +5208,8 @@
         assertThat(secondImage.getLeft()).isEqualTo(secondImageLeft);
     }
 
-    @Test   public void inflate_box_withVisibleModifier() {
+    @Test
+    public void inflate_box_withVisibleModifier() {
         final String protoResId = "android";
         final String boolKey = "bool-key";
 
@@ -5226,20 +5376,18 @@
                 ContainerDimension.newBuilder().setLinearDimension(dp(100.f).build()).build();
         ContainerDimension innerBoxSize =
                 ContainerDimension.newBuilder().setLinearDimension(dp(60.f).build()).build();
-        Box.Builder boxBuilder = Box.newBuilder()
-                .setWidth(expand())
-                .setHeight(wrap())
-                .setModifiers(
-                        Modifiers.newBuilder()
-                                .setTransformation(
-                                        transformation)
-                                .build())
-                .addContents(
-                        LayoutElement.newBuilder()
-                                .setBox(
-                                        Box.newBuilder()
-                                                .setWidth(innerBoxSize)
-                                                .setHeight(innerBoxSize)));
+        Box.Builder boxBuilder =
+                Box.newBuilder()
+                        .setWidth(expand())
+                        .setHeight(wrap())
+                        .setModifiers(
+                                Modifiers.newBuilder().setTransformation(transformation).build())
+                        .addContents(
+                                LayoutElement.newBuilder()
+                                        .setBox(
+                                                Box.newBuilder()
+                                                        .setWidth(innerBoxSize)
+                                                        .setHeight(innerBoxSize)));
         LayoutElement root =
                 LayoutElement.newBuilder()
                         .setBox(
@@ -5657,7 +5805,7 @@
                 renderer.computeMutation(
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(textFadeIn("World")));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         // Idle for running code for starting animations.
@@ -5689,7 +5837,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 0)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5715,7 +5863,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5738,7 +5886,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 1)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         assertThat(mDataPipeline.getRunningAnimationsCount()).isEqualTo(0);
@@ -5764,7 +5912,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5792,7 +5940,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
         shadowOf(getMainLooper()).idle();
 
@@ -5821,7 +5969,7 @@
                         fingerprintedLayout(
                                 getMultipleTextElementWithExitAnimation(
                                         Arrays.asList("Hello"), /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5850,7 +5998,7 @@
                         getRenderedMetadata(inflatedViewParent),
                         fingerprintedLayout(
                                 getTextElementWithExitAnimation("World", /* iterations= */ 10)));
-        ListenableFuture<Void> applyMutationFuture =
+        ListenableFuture<RenderingArtifact> applyMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, mutation);
 
         shadowOf(getMainLooper()).idleFor(Duration.ofMillis(100));
@@ -5863,7 +6011,7 @@
                                 getTextElementWithExitAnimation(
                                         "Second mutation", /* iterations= */ 10)));
 
-        ListenableFuture<Void> applySecondMutationFuture =
+        ListenableFuture<RenderingArtifact> applySecondMutationFuture =
                 renderer.mRenderer.applyMutation(inflatedViewParent, secondMutation);
 
         // the previous mutation should be finished
@@ -6222,9 +6370,12 @@
 
     private static Spacer.Builder buildExpandedSpacer(int widthWeight, int heightWeight) {
         return Spacer.newBuilder()
-                .setWidth(SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(widthWeight)))
+                .setWidth(
+                        SpacerDimension.newBuilder()
+                                .setExpandedDimension(expandWithWeight(widthWeight)))
                 .setHeight(
-                        SpacerDimension.newBuilder().setExpandedDimension(expandWithWeight(heightWeight)));
+                        SpacerDimension.newBuilder()
+                                .setExpandedDimension(expandWithWeight(heightWeight)));
     }
 
     private static ExpandedDimensionProp expandWithWeight(int weight) {
@@ -6255,4 +6406,15 @@
                                         .addContents(LayoutElement.newBuilder().setSpacer(spacer)))
                         .build());
     }
+
+    private static Text createTextWithVisibility(
+            String text, String id, Action action, boolean visibility) {
+        return Text.newBuilder()
+                .setText(string(text))
+                .setModifiers(
+                        Modifiers.newBuilder()
+                                .setVisible(BoolProp.newBuilder().setValue(visibility))
+                                .setClickable(Clickable.newBuilder().setId(id).setOnClick(action)))
+                .build();
+    }
 }