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

Commit 47ee5328 authored by Beth Thibodeau's avatar Beth Thibodeau Committed by Android (Google) Code Review
Browse files

Merge "Update seekbar content description and process in background" into main

parents 4cd71367 6ff97d3b
Loading
Loading
Loading
Loading
+2 −5
Original line number Diff line number Diff line
@@ -61,11 +61,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)
@@ -116,9 +116,6 @@ class SeekBarObserverTest : SysuiTestCase() {
        // 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.contentDescription).isEqualTo(desc)
    }

    @Test
+12 −8
Original line number Diff line number Diff line
@@ -127,10 +127,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 = data.durationDescription
        if (data.scrubbing) {
            holder.scrubbingTotalTimeView.text = totalTimeString
            holder.scrubbingTotalTimeView.text = formatTimeLabel(data.duration)
        }

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

            val elapsedTimeString = DateUtils.formatElapsedTime(it / DateUtils.SECOND_IN_MILLIS)
            val elapsedTimeDescription = data.elapsedTimeDescription
            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)
    }

    @VisibleForTesting
    open fun buildResetAnimator(targetTime: Int): Animator {
        val animator =
@@ -169,7 +173,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()
+66 −5
Original line number Diff line number Diff line
@@ -16,11 +16,15 @@

package com.android.systemui.media.controls.ui.viewmodel

import android.icu.text.MeasureFormat
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.PlaybackState
import android.os.SystemClock
import android.os.Trace
import android.text.format.DateUtils
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
@@ -38,11 +42,14 @@ import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.util.concurrency.RepeatableExecutor
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs

private const val POSITION_UPDATE_INTERVAL_MILLIS = 500L
private const val MIN_FLING_VELOCITY_SCALE_FACTOR = 10
private const val MIN_IN_SEC = 60
private const val HOUR_IN_SEC = MIN_IN_SEC * 60

private const val TRACE_POSITION_NAME = "SeekBarPollingPosition"

@@ -97,11 +104,20 @@ constructor(
        )
        set(value) {
            val enabledChanged = value.enabled != field.enabled
            field = value
            if (enabledChanged) {
                enabledChangeListener?.onEnabledChanged(value.enabled)
            }
            _progress.postValue(value)
            bgExecutor.execute {
                val durationDescription = formatTimeContentDescription(value.duration)
                val elapsedDescription =
                    value.elapsedTime?.let { formatTimeContentDescription(it) } ?: ""
                field =
                    value.copy(
                        durationDescription = durationDescription,
                        elapsedTimeDescription = elapsedDescription,
                    )
                _progress.postValue(field)
            }
        }

    private val _progress = MutableLiveData<Progress>().apply { postValue(_data) }
@@ -253,7 +269,8 @@ constructor(
                playbackState?.state ?: PlaybackState.STATE_NONE
            )
        _data = Progress(enabled, seekAvailable, playing, scrubbing, position, duration, listening)
        checkIfPollingNeeded()
        // No need to update since we just set the progress info
        checkIfPollingNeeded(requireUpdate = false)
    }

    /**
@@ -311,8 +328,13 @@ constructor(
        }
    }

    /**
     * Begin polling if needed given the current seekbar state
     *
     * @param requireUpdate If true, update the playback position without beginning polling
     */
    @WorkerThread
    private fun checkIfPollingNeeded() {
    private fun checkIfPollingNeeded(requireUpdate: Boolean = true) {
        val needed = listening && !scrubbing && playbackState?.isInMotion() ?: false
        val traceCookie = controller?.sessionToken.hashCode()
        if (needed) {
@@ -329,7 +351,7 @@ constructor(
                    Trace.endAsyncSection(TRACE_POSITION_NAME, traceCookie)
                }
            }
        } else {
        } else if (requireUpdate) {
            checkPlaybackPosition()
            cancel?.run()
            cancel = null
@@ -399,6 +421,43 @@ constructor(
            abs(firstMotionEvent!!.y - lastMotionEvent!!.y)
    }

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

    /** Listener interface to be notified when the user starts or stops scrubbing. */
    interface ScrubbingChangeListener {
        fun onScrubbingChanged(scrubbing: Boolean)
@@ -580,5 +639,7 @@ constructor(
        val duration: Int,
        /** whether seekBar is listening to progress updates */
        val listening: Boolean,
        val elapsedTimeDescription: CharSequence = "",
        val durationDescription: CharSequence = "",
    )
}
+56 −0
Original line number Diff line number Diff line
@@ -16,6 +16,9 @@

package com.android.systemui.media.controls.ui.viewmodel

import android.icu.text.MeasureFormat
import android.icu.util.Measure
import android.icu.util.MeasureUnit
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSession
@@ -34,6 +37,7 @@ import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.concurrency.FakeRepeatableExecutor
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Locale
import org.junit.After
import org.junit.Before
import org.junit.Ignore
@@ -155,6 +159,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN the duration is extracted
        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
        assertThat(viewModel.progress.value!!.enabled).isTrue()
@@ -173,6 +178,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getMetadata()).thenReturn(metadata)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN the duration is extracted
        assertThat(viewModel.progress.value!!.duration).isEqualTo(duration)
        assertThat(viewModel.progress.value!!.enabled).isFalse()
@@ -197,6 +203,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN the seek bar is disabled
        assertThat(viewModel.progress.value!!.enabled).isFalse()
    }
@@ -220,6 +227,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN the seek bar is disabled
        assertThat(viewModel.progress.value!!.enabled).isFalse()
    }
@@ -238,6 +246,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN the seek bar is disabled
        assertThat(viewModel.progress.value!!.enabled).isFalse()
    }
@@ -254,6 +263,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN elapsed time is captured
        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(200.toInt())
    }
@@ -536,6 +546,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN the controller is updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN a task is queued
        assertThat(fakeExecutor.numPending()).isEqualTo(1)
    }
@@ -551,6 +562,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN an update task is not queued
        assertThat(fakeExecutor.numPending()).isEqualTo(0)
    }
@@ -572,6 +584,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN an update task is queued
        assertThat(fakeExecutor.numPending()).isEqualTo(1)
    }
@@ -593,6 +606,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.getPlaybackState()).thenReturn(state)
        // WHEN updated
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // THEN an update task is not queued
        assertThat(fakeExecutor.numPending()).isEqualTo(0)
    }
@@ -719,6 +733,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
            }
        whenever(mockController.getPlaybackState()).thenReturn(state)
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        // WHEN start listening
        viewModel.listening = true
        // THEN an update task is queued
@@ -820,6 +835,7 @@ public class SeekBarViewModelTest : SysuiTestCase() {
        whenever(mockController.playbackState).thenReturn(state)
        val captor = ArgumentCaptor.forClass(MediaController.Callback::class.java)
        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()
        verify(mockController).registerCallback(captor.capture())
        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(firstPosition.toInt())

@@ -831,8 +847,48 @@ public class SeekBarViewModelTest : SysuiTestCase() {
                build()
            }
        captor.value.onPlaybackStateChanged(secondState)
        fakeExecutor.runNextReady()

        // THEN then elapsed time should be updated
        assertThat(viewModel.progress.value!!.elapsedTime).isEqualTo(secondPosition.toInt())
    }

    @Test
    fun contentDescriptionUpdated() {
        // When there is a duration and position
        val duration = (1.5 * 60 * 60 * 1000).toLong()
        val metadata =
            MediaMetadata.Builder().run {
                putLong(MediaMetadata.METADATA_KEY_DURATION, duration)
                build()
            }
        whenever(mockController.getMetadata()).thenReturn(metadata)

        val elapsedTime = 3000L
        val state =
            PlaybackState.Builder().run {
                setState(PlaybackState.STATE_PLAYING, elapsedTime, 1f)
                build()
            }
        whenever(mockController.getPlaybackState()).thenReturn(state)

        viewModel.updateController(mockController)
        fakeExecutor.runNextReady()

        // Then the content description is set
        val result = viewModel.progress.value!!

        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),
                )
        assertThat(result.durationDescription).isEqualTo(expectedDuration)
        assertThat(result.elapsedTimeDescription).isEqualTo(expectedProgress)
    }
}