Merge "[Media] A11y string for seekbar." into main
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
index 3c18637..8751aa3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt
@@ -98,6 +98,8 @@
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.semantics.clearAndSetSemantics
+import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Dp
@@ -678,7 +680,10 @@
                                     modifier = Modifier.fillMaxWidth(),
                                 )
                             },
-                            modifier = Modifier.fillMaxWidth(),
+                            modifier =
+                                Modifier.fillMaxWidth().clearAndSetSemantics {
+                                    contentDescription = viewModel.contentDescription
+                                },
                         )
                     }
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
index c34c733..ca73343 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaNavigationViewModel.kt
@@ -59,6 +59,8 @@
          * the seek bar). The position/progress should be committed.
          */
         val onScrubFinished: () -> Unit,
+        /** Accessibility string to attach to the seekbar UI element. */
+        val contentDescription: String,
     ) : MediaNavigationViewModel
 
     /** The seek bar should be hidden. */
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
index 61444e5..1595116 100644
--- a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaViewModel.kt
@@ -17,6 +17,9 @@
 package com.android.systemui.media.remedia.ui.viewmodel
 
 import android.content.Context
+import android.icu.text.MeasureFormat
+import android.icu.util.Measure
+import android.icu.util.MeasureUnit
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
@@ -38,7 +41,9 @@
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import java.util.Locale
 import kotlin.math.roundToLong
+import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.awaitCancellation
 
 /** Models UI state for a media element. */
@@ -118,6 +123,12 @@
                                     }
                                     isScrubbing = false
                                 },
+                                contentDescription =
+                                    context.getString(
+                                        R.string.controls_media_seekbar_description,
+                                        formatTimeContentDescription(session.positionMs),
+                                        formatTimeContentDescription(session.durationMs),
+                                    ),
                             )
                         } else {
                             MediaNavigationViewModel.Hidden
@@ -298,6 +309,43 @@
         }
     }
 
+    /**
+     * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
+     *
+     * Follows same logic as Chronometer#formatDuration
+     */
+    private fun formatTimeContentDescription(milliseconds: Long): String {
+        var seconds = milliseconds.milliseconds.inWholeSeconds
+
+        val hours =
+            if (seconds >= OneHourInSec) {
+                seconds / OneHourInSec
+            } else {
+                0
+            }
+        seconds -= hours * OneHourInSec
+
+        val minutes =
+            if (seconds >= OneMinuteInSec) {
+                seconds / OneMinuteInSec
+            } else {
+                0
+            }
+        seconds -= minutes * OneMinuteInSec
+
+        val measures = arrayListOf<Measure>()
+        if (hours > 0) {
+            measures.add(Measure(hours, MeasureUnit.HOUR))
+        }
+        if (minutes > 0) {
+            measures.add(Measure(minutes, MeasureUnit.MINUTE))
+        }
+        measures.add(Measure(seconds, MeasureUnit.SECOND))
+
+        return MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
+            .formatMeasures(*measures.toTypedArray())
+    }
+
     interface FalsingSystem {
         fun runIfNotFalseTap(@FalsingManager.Penalty penalty: Int, block: () -> Unit)
 
@@ -308,4 +356,9 @@
     interface Factory {
         fun create(context: Context, carouselVisibility: MediaCarouselVisibility): MediaViewModel
     }
+
+    companion object {
+        private const val OneMinuteInSec = 60
+        private const val OneHourInSec = OneMinuteInSec * 60
+    }
 }