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
+ }
}