Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 24df7560 authored by Beth Thibodeau's avatar Beth Thibodeau
Browse files

Update media progress bar content description

Test: verify with TalkBack
Test: atest SystemUITests:SeekBarObserverTest
Fixes: 392039095
Flag: EXEMPT bugfix
Change-Id: I1058629bd97c54fbc7751e79b90c6cb99b6e413f
parent aaa454ed
Loading
Loading
Loading
Loading
+28 −7
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder

import android.animation.Animator
import android.animation.ObjectAnimator
import android.icu.text.MeasureFormat
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import android.testing.TestableLooper
import android.view.View
import android.widget.SeekBar
@@ -30,6 +33,7 @@ import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -61,11 +65,11 @@ class SeekBarObserverTest : SysuiTestCase() {
    fun setUp() {
        context.orCreateTestableResources.addOverride(
            R.dimen.qs_media_enabled_seekbar_height,
            enabledHeight
            enabledHeight,
        )
        context.orCreateTestableResources.addOverride(
            R.dimen.qs_media_disabled_seekbar_height,
            disabledHeight
            disabledHeight,
        )

        seekBarView = SeekBar(context)
@@ -110,14 +114,31 @@ class SeekBarObserverTest : SysuiTestCase() {

    @Test
    fun seekBarProgress() {
        val elapsedTime = 3000
        val duration = (1.5 * 60 * 60 * 1000).toInt()
        // WHEN part of the track has been played
        val data = SeekBarViewModel.Progress(true, true, true, false, 3000, 120000, true)
        val data = SeekBarViewModel.Progress(true, true, true, false, elapsedTime, duration, true)
        observer.onChanged(data)
        // THEN seek bar shows the progress
        assertThat(seekBarView.progress).isEqualTo(3000)
        assertThat(seekBarView.max).isEqualTo(120000)

        val desc = context.getString(R.string.controls_media_seekbar_description, "00:03", "02:00")
        assertThat(seekBarView.progress).isEqualTo(elapsedTime)
        assertThat(seekBarView.max).isEqualTo(duration)

        val expectedProgress =
            MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
                .formatMeasures(Measure(3, MeasureUnit.SECOND))
        val expectedDuration =
            MeasureFormat.getInstance(Locale.getDefault(), MeasureFormat.FormatWidth.WIDE)
                .formatMeasures(
                    Measure(1, MeasureUnit.HOUR),
                    Measure(30, MeasureUnit.MINUTE),
                    Measure(0, MeasureUnit.SECOND),
                )
        val desc =
            context.getString(
                R.string.controls_media_seekbar_description,
                expectedProgress,
                expectedDuration,
            )
        assertThat(seekBarView.contentDescription).isEqualTo(desc)
    }

+2 −2
Original line number Diff line number Diff line
@@ -3167,8 +3167,8 @@
    <string name="controls_media_settings_button">Settings</string>
    <!-- Description for media control's playing media item, including information for the media's title, the artist, and source app [CHAR LIMIT=NONE]-->
    <string name="controls_media_playing_item_description"><xliff:g id="song_name" example="Daily mix">%1$s</xliff:g> by <xliff:g id="artist_name" example="Various artists">%2$s</xliff:g> is playing from <xliff:g id="app_label" example="Spotify">%3$s</xliff:g></string>
    <!-- Content description for media cotnrols progress bar [CHAR_LIMIT=NONE] -->
    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1:30">%1$s</xliff:g> of <xliff:g id="total_time" example="3:00">%2$s</xliff:g></string>
    <!-- Content description for media controls progress bar [CHAR_LIMIT=NONE] -->
    <string name="controls_media_seekbar_description"><xliff:g id="elapsed_time" example="1 hour 2 minutes 30 seconds">%1$s</xliff:g> of <xliff:g id="total_time" example="4 hours 5 seconds">%2$s</xliff:g></string>
    <!-- Placeholder title to inform user that an app has posted media controls [CHAR_LIMIT=NONE] -->
    <string name="controls_media_empty_title"><xliff:g id="app_name" example="Foo Music App">%1$s</xliff:g> is running</string>

+55 −8
Original line number Diff line number Diff line
@@ -18,6 +18,9 @@ package com.android.systemui.media.controls.ui.binder

import android.animation.Animator
import android.animation.ObjectAnimator
import android.icu.text.MeasureFormat
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import android.text.format.DateUtils
import androidx.annotation.UiThread
import androidx.lifecycle.Observer
@@ -28,8 +31,11 @@ import com.android.systemui.media.controls.ui.drawable.SquigglyProgress
import com.android.systemui.media.controls.ui.view.MediaViewHolder
import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel
import com.android.systemui.res.R
import java.util.Locale

private const val TAG = "SeekBarObserver"
private const val MIN_IN_SEC = 60
private const val HOUR_IN_SEC = MIN_IN_SEC * 60

/**
 * Observer for changes from SeekBarViewModel.
@@ -127,10 +133,9 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
        }

        holder.seekBar.setMax(data.duration)
        val totalTimeString =
            DateUtils.formatElapsedTime(data.duration / DateUtils.SECOND_IN_MILLIS)
        val totalTimeDescription = formatTimeContentDescription(data.duration)
        if (data.scrubbing) {
            holder.scrubbingTotalTimeView.text = totalTimeString
            holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
        }

        data.elapsedTime?.let {
@@ -148,20 +153,62 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
                }
            }

            val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
            val elapsedTimeDescription = formatTimeContentDescription(it)
            if (data.scrubbing) {
                holder.scrubbingElapsedTimeView.text = elapsedTimeString
                holder.scrubbingElapsedTimeView.text = formatTimeLabel(it)
            }

            holder.seekBar.contentDescription =
                holder.seekBar.context.getString(
                    R.string.controls_media_seekbar_description,
                    elapsedTimeString,
                    totalTimeString
                    elapsedTimeDescription,
                    totalTimeDescription,
                )
        }
    }

    /** Returns a time string suitable for display, e.g. "12:34" */
    private fun formatTimeLabel(milliseconds: Int): CharSequence {
        return DateUtils.formatElapsedTime(milliseconds / DateUtils.SECOND_IN_MILLIS)
    }

    /**
     * Returns a time string suitable for content description, e.g. "12 minutes 34 seconds"
     *
     * Follows same logic as Chronometer#formatDuration
     */
    private fun formatTimeContentDescription(milliseconds: Int): CharSequence {
        var seconds = milliseconds / DateUtils.SECOND_IN_MILLIS

        val hours =
            if (seconds >= HOUR_IN_SEC) {
                seconds / HOUR_IN_SEC
            } else {
                0
            }
        seconds -= hours * HOUR_IN_SEC

        val minutes =
            if (seconds >= MIN_IN_SEC) {
                seconds / MIN_IN_SEC
            } else {
                0
            }
        seconds -= minutes * MIN_IN_SEC

        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())
    }

    @VisibleForTesting
    open fun buildResetAnimator(targetTime: Int): Animator {
        val animator =
@@ -169,7 +216,7 @@ open class SeekBarObserver(private val holder: MediaViewHolder) :
                holder.seekBar,
                "progress",
                holder.seekBar.progress,
                targetTime + RESET_ANIMATION_DURATION_MS
                targetTime + RESET_ANIMATION_DURATION_MS,
            )
        animator.setAutoCancel(true)
        animator.duration = RESET_ANIMATION_DURATION_MS.toLong()