Merge "Add circular progree indicator fallback implementation with older renderer" into androidx-main
diff --git a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
index 2aa2b52..9887718 100644
--- a/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
+++ b/wear/protolayout/protolayout-material3/src/androidTest/java/androidx/wear/protolayout/material3/TestCasesGenerator.kt
@@ -27,6 +27,7 @@
 import androidx.wear.protolayout.ModifiersBuilders.Background
 import androidx.wear.protolayout.ModifiersBuilders.Corner
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
 import androidx.wear.protolayout.material3.AppCardStyle.Companion.largeAppCardStyle
 import androidx.wear.protolayout.material3.ButtonDefaults.filledButtonColors
 import androidx.wear.protolayout.material3.ButtonDefaults.filledTonalButtonColors
@@ -76,6 +77,8 @@
                 .setScreenDensity(displayMetrics.density)
                 .setFontScale(1f)
                 .setScreenShape(DeviceParametersBuilders.SCREEN_SHAPE_RECT)
+                // testing with the latest renderer version
+                .setRendererSchemaVersion(VersionInfo.Builder().setMajor(99).setMinor(999).build())
                 .build()
         val clickable = clickable(id = "action_id")
         val testCases: HashMap<String, LayoutElementBuilders.LayoutElement> = HashMap()
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
index c8a3022..c029f51 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicator.kt
@@ -23,13 +23,14 @@
 import androidx.wear.protolayout.DimensionBuilders.AngularLayoutConstraint
 import androidx.wear.protolayout.DimensionBuilders.ContainerDimension
 import androidx.wear.protolayout.DimensionBuilders.DegreesProp
+import androidx.wear.protolayout.DimensionBuilders.DpProp
 import androidx.wear.protolayout.DimensionBuilders.ExpandedDimensionProp
 import androidx.wear.protolayout.DimensionBuilders.WrappedDimensionProp
 import androidx.wear.protolayout.DimensionBuilders.degrees
-import androidx.wear.protolayout.DimensionBuilders.dp
 import androidx.wear.protolayout.DimensionBuilders.expand
 import androidx.wear.protolayout.LayoutElementBuilders
 import androidx.wear.protolayout.LayoutElementBuilders.Arc
+import androidx.wear.protolayout.LayoutElementBuilders.ArcLine
 import androidx.wear.protolayout.LayoutElementBuilders.ArcSpacer
 import androidx.wear.protolayout.LayoutElementBuilders.Box
 import androidx.wear.protolayout.LayoutElementBuilders.DashedArcLine
@@ -38,6 +39,8 @@
 import androidx.wear.protolayout.ModifiersBuilders.Modifiers
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicColor
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
+import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.CPI_DEFAULT_DP_SIZE
 import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.INDICATOR_STROKE_WIDTH_INCREMENT_PX
 import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.LARGE_STROKE_WIDTH
 import androidx.wear.protolayout.material3.CircularProgressIndicatorDefaults.METADATA_TAG
@@ -49,11 +52,17 @@
 import androidx.wear.protolayout.modifiers.padding
 import androidx.wear.protolayout.modifiers.tag
 import androidx.wear.protolayout.modifiers.toProtoLayoutModifiers
+import androidx.wear.protolayout.types.dp
 import kotlin.math.min
 
 /**
  * Protolayout Material3 design circular progress indicator.
  *
+ * Note that, the proper implementation of this component requires a ProtoLayout renderer with
+ * version equal to or above 1.403. When the renderer is lower than 1.403, this component will
+ * automatically fallback to an implementation with reduced features, without support for expandable
+ * size, overflow, and start/end transition.
+ *
  * @param staticProgress The static progress of this progress indicator where 0 represent no
  *   progress and 1 represents completion. Progress above 1 is also allowed. If [dynamicProgress] is
  *   also set, this static value will only be used when the dynamic value cannot be evaluated. By
@@ -91,8 +100,8 @@
     modifier: LayoutModifier = LayoutModifier,
     startAngleDegrees: Float = 0F,
     endAngleDegrees: Float = startAngleDegrees + 360F,
-    @Dimension(unit = DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
-    @Dimension(unit = DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
+    @Dimension(DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
+    @Dimension(DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
     colors: ProgressIndicatorColors = filledProgressIndicatorColors(),
     size: ContainerDimension = expand(),
 ): LayoutElement {
@@ -100,19 +109,39 @@
     verifySize(size)
 
     val modifiers = (LayoutModifier.tag(METADATA_TAG) then modifier).toProtoLayoutModifiers()
+    val hasDashedArcLineSupport =
+        deviceConfiguration.rendererSchemaVersion.hasDashedArcLineSupport()
+    // With the fallback implementation, expandable size is not supported, fallback to dp size.
+    val containerSize =
+        if (hasDashedArcLineSupport || size is DpProp) size else CPI_DEFAULT_DP_SIZE.dp
+    val boxBuilder =
+        if (hasDashedArcLineSupport)
+            singleSegmentImpl(
+                startAngleDegrees = startAngleDegrees,
+                endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+                staticProgress = staticProgress,
+                dynamicProgress = dynamicProgress,
+                strokeWidth = strokeWidth,
+                gapSize = gapSize,
+                colors = colors
+            )
+        else
+            circularProgressIndicatorFallbackImpl(
+                // Without DashedArcLine support, container size fell back to dp size.
+                arcContainerSize = (containerSize as DpProp).value,
+                startAngleDegrees = startAngleDegrees,
+                endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+                staticProgress = staticProgress,
+                dynamicProgress = dynamicProgress,
+                strokeWidth = strokeWidth,
+                gapSize = gapSize,
+                colors = colors
+            )
 
-    return singleSegmentImpl(
-            startAngleDegrees = startAngleDegrees,
-            endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
-            staticProgress = staticProgress,
-            dynamicProgress = dynamicProgress,
-            strokeWidth = strokeWidth,
-            gapSize = gapSize,
-            colors = colors
-        )
+    return boxBuilder
         .setModifiers(modifiers)
-        .setWidth(size)
-        .setHeight(size)
+        .setWidth(containerSize)
+        .setHeight(containerSize)
         .build()
 }
 
@@ -121,6 +150,11 @@
  *
  * A segmented variant of [circularProgressIndicator] that is divided into equally sized segments.
  *
+ * Note that, the proper implementation of this component requires a ProtoLayout renderer with
+ * version equal to or above 1.403. When the renderer is lower than 1.403, this component will
+ * automatically fallback to an implementation with reduced features, without support for multiple
+ * segments, expandable size, overflow, and start/end transition.
+ *
  * @param segmentCount Number of equal segments that the progress indicator should be divided into.
  *   Has to be a number greater than or equal to 1.
  * @param staticProgress The static progress of this progress indicator where 0 represent no
@@ -161,8 +195,8 @@
     modifier: LayoutModifier = LayoutModifier,
     startAngleDegrees: Float = 0F,
     endAngleDegrees: Float = startAngleDegrees + 360F,
-    @Dimension(unit = DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
-    @Dimension(unit = DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
+    @Dimension(DP) strokeWidth: Float = LARGE_STROKE_WIDTH,
+    @Dimension(DP) gapSize: Float = calculateRecommendedGapSize(strokeWidth),
     colors: ProgressIndicatorColors = filledProgressIndicatorColors(),
     size: ContainerDimension = expand(),
 ): LayoutElement {
@@ -170,20 +204,40 @@
     verifySize(size)
 
     val modifiers = (LayoutModifier.tag(METADATA_TAG) then modifier).toProtoLayoutModifiers()
+    val hasDashedArcLineSupport =
+        deviceConfiguration.rendererSchemaVersion.hasDashedArcLineSupport()
+    // Without using DashedArcLine, expandable size is not supported, fallback to dp size.
+    val containerSize =
+        if (hasDashedArcLineSupport || size is DpProp) size else CPI_DEFAULT_DP_SIZE.dp
+    val boxBuilder =
+        if (hasDashedArcLineSupport)
+            multipleSegmentsImpl(
+                segmentCount = segmentCount,
+                startAngleDegrees = startAngleDegrees,
+                endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+                staticProgress = staticProgress,
+                dynamicProgress = dynamicProgress,
+                strokeWidth = strokeWidth,
+                gapSize = gapSize,
+                colors = colors
+            )
+        else
+            circularProgressIndicatorFallbackImpl(
+                // Without DashedArcLine support, container size fell back to dp size.
+                arcContainerSize = (containerSize as DpProp).value,
+                startAngleDegrees = startAngleDegrees,
+                endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
+                staticProgress = staticProgress,
+                dynamicProgress = dynamicProgress,
+                strokeWidth = strokeWidth,
+                gapSize = gapSize,
+                colors = colors
+            )
 
-    return multipleSegmentsImpl(
-            segmentCount = segmentCount,
-            startAngleDegrees = startAngleDegrees,
-            endAngleDegrees = checkAndAdjustEndAngle(startAngleDegrees, endAngleDegrees),
-            staticProgress = staticProgress,
-            dynamicProgress = dynamicProgress,
-            strokeWidth = strokeWidth,
-            gapSize = gapSize,
-            colors = colors
-        )
+    return boxBuilder
         .setModifiers(modifiers)
-        .setWidth(size)
-        .setHeight(size)
+        .setWidth(containerSize)
+        .setHeight(containerSize)
         .build()
 }
 
@@ -197,8 +251,8 @@
     endAngleDegrees: Float,
     staticProgress: Float,
     dynamicProgress: DynamicFloat?,
-    @Dimension(unit = DP) strokeWidth: Float,
-    @Dimension(unit = DP) gapSize: Float,
+    @Dimension(DP) strokeWidth: Float,
+    @Dimension(DP) gapSize: Float,
     colors: ProgressIndicatorColors
 ): Box.Builder {
     val sweepAngle = endAngleDegrees - startAngleDegrees
@@ -225,7 +279,7 @@
             .setGapLocations(0f)
             .build()
     val spacer =
-        ArcSpacer.Builder().setAngularLength(dp(gapSize / 2)).setThickness(dp(strokeWidth)).build()
+        ArcSpacer.Builder().setAngularLength((gapSize / 2F).dp).setThickness(strokeWidth.dp).build()
     return Box.Builder()
         .addContent( // the track
             createArc(
@@ -266,8 +320,8 @@
     endAngleDegrees: Float,
     staticProgress: Float,
     dynamicProgress: DynamicFloat?,
-    @Dimension(unit = DP) strokeWidth: Float,
-    @Dimension(unit = DP) gapSize: Float,
+    @Dimension(DP) strokeWidth: Float,
+    @Dimension(DP) gapSize: Float,
     colors: ProgressIndicatorColors
 ): Box.Builder {
     val sweepAngle = endAngleDegrees - startAngleDegrees
@@ -333,6 +387,12 @@
     }
 }
 
+/**
+ * For renderer with version lower than 1.403, there is no [DashedArcLine] support. In this case,
+ * the progress indicator will fallback to use [ArcLine]
+ */
+private fun VersionInfo.hasDashedArcLineSupport() = major > 1 || (major == 1 && minor >= 403)
+
 /*
  * Check the endAngle is valid with the provided startAngle, and adjust the sweep angle to be 360
  * maximum.
@@ -394,11 +454,11 @@
     anchorType: Int,
     arcLength: DegreesProp,
     arcColor: ColorProp,
-    @Dimension(unit = DP) strokeWidth: Float,
+    @Dimension(DP) strokeWidth: Float,
     linePattern: DashedLinePattern,
     arcDirection: Int
-): Arc.Builder {
-    return Arc.Builder()
+): Arc.Builder =
+    Arc.Builder()
         .setAnchorAngle(anchorAngle)
         .setAnchorType(anchorType)
         .setArcDirection(arcDirection)
@@ -415,4 +475,3 @@
                 )
                 .build()
         )
-}
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
index 7ca7208..bd9c49c 100644
--- a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorDefaults.kt
@@ -84,18 +84,18 @@
             .build()
 
     /** Large stroke width for circular progress indicator. */
-    @Dimension(unit = DP) public const val LARGE_STROKE_WIDTH: Float = 8F
+    @Dimension(DP) public const val LARGE_STROKE_WIDTH: Float = 8F
 
     /** Small stroke width for circular progress indicator. */
-    @Dimension(unit = DP) public const val SMALL_STROKE_WIDTH: Float = 4F
+    @Dimension(DP) public const val SMALL_STROKE_WIDTH: Float = 4F
 
     /**
      * Returns recommended size of the gap based on [strokeWidth].
      *
      * The absolute value can be customized with `gapSize` parameter on [circularProgressIndicator].
      */
-    @Dimension(unit = DP)
-    public fun calculateRecommendedGapSize(@Dimension(unit = DP) strokeWidth: Float): Float =
+    @Dimension(DP)
+    public fun calculateRecommendedGapSize(@Dimension(DP) strokeWidth: Float): Float =
         strokeWidth / 3F
 
     internal const val METADATA_TAG: String = "M3CPI"
@@ -110,4 +110,7 @@
      * top of the track arc where there are multiple segments.
      */
     internal const val INDICATOR_STROKE_WIDTH_INCREMENT_PX: Float = 1.5f
+
+    /** Default size for the fallback implementation. */
+    @Dimension(DP) internal const val CPI_DEFAULT_DP_SIZE: Float = 52F
 }
diff --git a/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt
new file mode 100644
index 0000000..00dc0e2
--- /dev/null
+++ b/wear/protolayout/protolayout-material3/src/main/java/androidx/wear/protolayout/material3/CircularProgressIndicatorFallback.kt
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2025 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.material3
+
+import androidx.annotation.Dimension
+import androidx.annotation.Dimension.Companion.DP
+import androidx.wear.protolayout.ColorBuilders.ColorProp
+import androidx.wear.protolayout.DimensionBuilders.AngularLayoutConstraint
+import androidx.wear.protolayout.DimensionBuilders.DegreesProp
+import androidx.wear.protolayout.DimensionBuilders.degrees
+import androidx.wear.protolayout.LayoutElementBuilders
+import androidx.wear.protolayout.LayoutElementBuilders.Arc
+import androidx.wear.protolayout.LayoutElementBuilders.ArcLine
+import androidx.wear.protolayout.LayoutElementBuilders.Box
+import androidx.wear.protolayout.LayoutElementBuilders.DashedArcLine
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.types.dp
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * This method provides the fallback content layout for [circularProgressIndicator] and
+ * [segmentedCircularProgressIndicator] using [ArcLine] when the renderer version is lower than
+ * 1.403 where [DashedArcLine] is not available.
+ *
+ * Note that we require valid start and end angles for calling this method.
+ */
+internal fun MaterialScope.circularProgressIndicatorFallbackImpl(
+    @Dimension(unit = DP) arcContainerSize: Float,
+    startAngleDegrees: Float,
+    endAngleDegrees: Float,
+    staticProgress: Float,
+    dynamicProgress: DynamicFloat?,
+    @Dimension(unit = DP) strokeWidth: Float,
+    @Dimension(unit = DP) gapSize: Float,
+    colors: ProgressIndicatorColors
+): Box.Builder {
+    // Offset the anchor to make space for the cap and half gap.
+    val anchorOffsetDegrees =
+        (strokeWidth + gapSize).dpToDegree(radius = (arcContainerSize - strokeWidth) / 2.0) / 2
+
+    // Visually, the progress arcline and track arcline never overlaps. But from the layout point
+    // of view, their layout size are both from start angle to end angle, and overlaps completely.
+    // Thus, we put each arcline in an arc container, and stacks these two arc containers inside a
+    // box.
+    return Box.Builder()
+        .addContent(
+            createArc(
+                anchorDegree = endAngleDegrees - anchorOffsetDegrees,
+                anchorType = LayoutElementBuilders.ARC_ANCHOR_END,
+                arcLength =
+                    trackInDegrees(
+                        sweepAngle = endAngleDegrees - startAngleDegrees,
+                        staticProgress = staticProgress,
+                        dynamicProgress = dynamicProgress,
+                        lengthAdjustment = anchorOffsetDegrees * 2F
+                    ),
+                color = colors.trackColor.prop,
+                strokeWidth = strokeWidth
+            )
+        )
+        .addContent(
+            createArc(
+                anchorDegree = startAngleDegrees + anchorOffsetDegrees,
+                anchorType = LayoutElementBuilders.ARC_ANCHOR_START,
+                arcLength =
+                    progressInDegrees(
+                        sweepAngle = endAngleDegrees - startAngleDegrees,
+                        staticProgress = staticProgress,
+                        dynamicProgress = dynamicProgress,
+                        lengthAdjustment = anchorOffsetDegrees * 2F
+                    ),
+                color = colors.indicatorColor.prop,
+                strokeWidth = strokeWidth
+            )
+        )
+}
+
+/**
+ * A small offset to make the progress dot remaining when the progress is 0, and the track dot
+ * remaining when the progress is 1.
+ */
+private const val ARC_OFFSET_IN_DEGREES = 0.05f
+
+private fun createArc(
+    anchorDegree: Float,
+    anchorType: Int,
+    arcLength: DegreesProp,
+    color: ColorProp,
+    @Dimension(unit = DP) strokeWidth: Float
+): Arc =
+    Arc.Builder()
+        .setAnchorAngle(degrees(anchorDegree))
+        .setAnchorType(anchorType)
+        .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE)
+        .addContent(
+            ArcLine.Builder()
+                .setColor(color)
+                .setArcDirection(LayoutElementBuilders.ARC_DIRECTION_CLOCKWISE)
+                .setLength(arcLength)
+                .setLayoutConstraintsForDynamicLength(
+                    // We use one Arc container to put one arcline, so it is fine to put 360 here
+                    // as layout constraint.
+                    AngularLayoutConstraint.Builder(360F).setAngularAlignment(anchorType).build()
+                )
+                .setThickness(strokeWidth.dp)
+                .build()
+        )
+        .build()
+
+private fun Float.dpToDegree(radius: Double): Float =
+    // radianAngle = arcLength / radius
+    Math.toDegrees(this / radius).toFloat()
+
+private fun progressInDegrees(
+    sweepAngle: Float,
+    staticProgress: Float,
+    dynamicProgress: DynamicFloat?,
+    lengthAdjustment: Float
+): DegreesProp =
+    arcInDegrees(
+        sweepAngle = sweepAngle,
+        staticRatio = staticProgress,
+        dynamicRatio = dynamicProgress,
+        lengthAdjustment = lengthAdjustment
+    )
+
+private fun trackInDegrees(
+    sweepAngle: Float,
+    staticProgress: Float,
+    dynamicProgress: DynamicFloat?,
+    lengthAdjustment: Float
+): DegreesProp =
+    arcInDegrees(
+        sweepAngle = sweepAngle,
+        staticRatio = 1F - staticProgress,
+        dynamicRatio =
+            if (dynamicProgress == null) null else DynamicFloat.constant(1F).minus(dynamicProgress),
+        lengthAdjustment = lengthAdjustment
+    )
+
+private fun arcInDegrees(
+    sweepAngle: Float,
+    staticRatio: Float,
+    dynamicRatio: DynamicFloat?,
+    lengthAdjustment: Float
+): DegreesProp {
+    val staticValue =
+        getCorrectStaticArcLength(
+            sweepAngle = sweepAngle,
+            ratio = staticRatio,
+            lengthAdjustment = lengthAdjustment
+        )
+
+    if (dynamicRatio == null) { // static value
+        return degrees(staticValue)
+    }
+
+    return DegreesProp.Builder(staticValue)
+        .setDynamicValue(
+            getApproximateDynamicArcLength(
+                sweepAngle = sweepAngle,
+                ratio = dynamicRatio,
+                lengthAdjustment = lengthAdjustment
+            )
+        )
+        .build()
+}
+
+/**
+ * When drawing the progress arc line, we need to adjust its Length to make space for two caps and
+ * gap. Note that, event the progress arc is 0, we still leave the above space, for a good
+ * transition to a non-zero progress. Similar for track arc line. The arc length calculation is as
+ * follows:
+ * ```
+ * sweepAngle = endAngle-startAngle
+ * perArcLengthAdjustment = cap * 2 - gap
+ * maxTotalLength = endAngle - startAngle- 2 * perArcLengthAdjustment
+ * ProgressArcLength = clamp(sweepAngle * progress - perArcLengthAdjustment, 0, maxTotalLength)
+ * trackArcLength = clamp(sweepAngle*(1-progress) - perArcLengthAdjustment, 0, maxTotalLength)
+ * ```
+ */
+private fun getCorrectStaticArcLength(
+    sweepAngle: Float,
+    ratio: Float,
+    lengthAdjustment: Float
+): Float {
+    val length = (sweepAngle) * ratio - lengthAdjustment
+    val maxLength = sweepAngle - 2F * lengthAdjustment
+    return (max(min(length, maxLength), 0F) + ARC_OFFSET_IN_DEGREES)
+}
+
+/**
+ * When the progress is static, we calculate the arc lengths as {@link #getCorrectStaticArcLength}.
+ * However, the clamp operation adds two extra animation quota per arc with animated dynamic
+ * progress which is not acceptable. We thus use an approximation calculation for dynamic values,
+ * Which is not very precise, but a good approximation. As follows:
+ * ```
+ * sweepAngle = endAngle-startAngle
+ * perArcLengthAdjustment = cap * 2 - gap
+ * maxTotalLength = endAngle-startAngle - 2 * perArcLengthAdjustment
+ * progressArcLength = maxTotalLength * progress
+ * trackArcLength = maxTotalLength * (1-progress)
+ * ```
+ */
+private fun getApproximateDynamicArcLength(
+    sweepAngle: Float,
+    ratio: DynamicFloat,
+    lengthAdjustment: Float
+) = ratio.times(sweepAngle - lengthAdjustment * 2).plus(ARC_OFFSET_IN_DEGREES)
diff --git a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
index 07973b83..6d7e763 100644
--- a/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
+++ b/wear/protolayout/protolayout-material3/src/test/java/androidx/wear/protolayout/material3/CircularProgressIndicatorTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.wear.protolayout.DeviceParametersBuilders
 import androidx.wear.protolayout.DimensionBuilders.expand
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
 import androidx.wear.protolayout.testing.LayoutElementAssertionsProvider
 import androidx.wear.protolayout.testing.hasHeight
 import androidx.wear.protolayout.testing.hasWidth
@@ -61,6 +62,7 @@
             DeviceParametersBuilders.DeviceParameters.Builder()
                 .setScreenWidthDp(192)
                 .setScreenHeightDp(192)
+                .setRendererSchemaVersion(VersionInfo.Builder().setMajor(1).setMinor(403).build())
                 .build()
 
         private const val STATIC_PROGRESS = 0.5F
diff --git a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
index ef30cd7..cd0f098 100644
--- a/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
+++ b/wear/protolayout/protolayout/src/main/java/androidx/wear/protolayout/types/Helpers.kt
@@ -16,6 +16,8 @@
 
 package androidx.wear.protolayout.types
 
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
 import androidx.wear.protolayout.DimensionBuilders.DpProp
 import androidx.wear.protolayout.DimensionBuilders.EmProp
 import androidx.wear.protolayout.DimensionBuilders.SpProp
@@ -32,7 +34,8 @@
 internal val Boolean.prop: BoolProp
     get() = BoolProp.Builder(this).build()
 
-internal val Float.dp: DpProp
+@get:RestrictTo(Scope.LIBRARY_GROUP)
+val Float.dp: DpProp
     get() = DpProp.Builder(this).build()
 
 @RequiresSchemaVersion(major = 1, minor = 400)
diff --git a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
index f3f97b9..183279b 100644
--- a/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
+++ b/wear/tiles/tiles-samples/src/main/java/androidx/wear/tiles/samples/tile/PlaygroundTileService.kt
@@ -17,6 +17,8 @@
 package androidx.wear.tiles.samples.tile
 
 import android.content.Context
+import androidx.wear.protolayout.DeviceParametersBuilders
+import androidx.wear.protolayout.DimensionBuilders.dp
 import androidx.wear.protolayout.DimensionBuilders.expand
 import androidx.wear.protolayout.DimensionBuilders.weight
 import androidx.wear.protolayout.LayoutElementBuilders
@@ -25,6 +27,7 @@
 import androidx.wear.protolayout.ResourceBuilders.ImageResource
 import androidx.wear.protolayout.TimelineBuilders
 import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.VersionBuilders.VersionInfo
 import androidx.wear.protolayout.material3.ButtonDefaults.filledVariantButtonColors
 import androidx.wear.protolayout.material3.CardColors
 import androidx.wear.protolayout.material3.CardDefaults.filledTonalCardColors
@@ -250,6 +253,43 @@
         }
     )
 
+private fun MaterialScope.graphicDataCardSampleWithFallbackProgressIndicator(context: Context) =
+    graphicDataCard(
+        onClick = clickable(),
+        modifier = LayoutModifier.contentDescription("Graphic Data Card"),
+        height = expand(),
+        horizontalAlignment = LayoutElementBuilders.HORIZONTAL_ALIGN_END,
+        title = {
+            text(
+                "1,234!".layoutString,
+            )
+        },
+        content = {
+            text(
+                "steps".layoutString,
+            )
+        },
+        graphic = {
+            materialScope(
+                context = context,
+                deviceConfiguration =
+                    DeviceParametersBuilders.DeviceParameters.Builder()
+                        .setRendererSchemaVersion(
+                            VersionInfo.Builder().setMajor(1).setMinor(402).build()
+                        )
+                        .build()
+            ) {
+                segmentedCircularProgressIndicator(
+                    segmentCount = 6,
+                    startAngleDegrees = 200F,
+                    endAngleDegrees = 520F,
+                    dynamicProgress = DynamicFloat.animate(0.0F, 1.0F, recommendedAnimationSpec),
+                    size = dp(75F)
+                )
+            }
+        }
+    )
+
 private fun MaterialScope.dataCards() = buttonGroup {
     buttonGroupItem {
         textDataCard(