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

Commit 8bbfc25c authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez Committed by Android (Google) Code Review
Browse files

Merge "Refactor of the SeekableSliderEventProducer and SeekableSliderTracker." into main

parents ecb650dd f00fd445
Loading
Loading
Loading
Loading
+3 −3
Original line number Diff line number Diff line
@@ -44,14 +44,14 @@ import org.mockito.junit.MockitoRule
@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class SeekableSliderHapticPluginTest : SysuiTestCase() {
class SeekbarHapticPluginTest : SysuiTestCase() {

    private val kosmos = Kosmos()

    @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
    @Mock private lateinit var vibratorHelper: VibratorHelper
    private val seekBar = SeekBar(mContext)
    private lateinit var plugin: SeekableSliderHapticPlugin
    private lateinit var plugin: SeekbarHapticPlugin

    @Before
    fun setup() {
@@ -142,7 +142,7 @@ class SeekableSliderHapticPluginTest : SysuiTestCase() {

    private fun createPlugin() {
        plugin =
            SeekableSliderHapticPlugin(
            SeekbarHapticPlugin(
                vibratorHelper,
                kosmos.fakeSystemClock,
            )
+134 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 * Copyright (C) 2024 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.
@@ -16,7 +16,6 @@

package com.android.systemui.haptics.slider

import android.widget.SeekBar
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -28,17 +27,16 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SeekableSliderEventProducerTest : SysuiTestCase() {
class SliderStateProducerTest : SysuiTestCase() {

    private val seekBar = SeekBar(mContext)
    private val eventProducer = SeekableSliderEventProducer()
    private val eventProducer = SliderStateProducer()
    private val eventFlow = eventProducer.produceEvents()

    @Test
    fun onStartTrackingTouch_noProgress_trackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

        eventProducer.onStartTrackingTouch(seekBar)
        eventProducer.onStartTracking(/*fromUser =*/ true)

        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0F), latest)
    }
@@ -47,101 +45,90 @@ class SeekableSliderEventProducerTest : SysuiTestCase() {
    fun onStopTrackingTouch_noProgress_StoppedTrackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

        eventProducer.onStopTrackingTouch(seekBar)
        eventProducer.onStopTracking(/*fromUser =*/ true)

        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0F), latest)
    }

    @Test
    fun onProgressChangeByUser_changeByUserEventProduced_withNormalizedProgress() = runTest {
        val progress = 50
    fun onStartTrackingProgram_noProgress_trackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

        eventProducer.onProgressChanged(seekBar, progress, true)
        eventProducer.onStartTracking(/*fromUser =*/ false)

        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 0.5F), latest)
        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, 0F), latest)
    }

    @Test
    fun onProgressChangeByUser_zeroWidthSlider_changeByUserEventProduced_withMaxProgress() =
        runTest {
            // No-width slider where the min and max values are the same
            seekBar.min = 100
            seekBar.max = 100
            val progress = 50
    fun onStopTrackingProgram_noProgress_StoppedTrackingTouchEventProduced() = runTest {
        val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)
        eventProducer.onStopTracking(/*fromUser =*/ false)

            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, 1.0F), latest)
        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, 0F), latest)
    }

    @Test
    fun onProgressChangeByProgram_changeByProgramEventProduced_withNormalizedProgress() = runTest {
        val progress = 50
    fun onProgressChangeByUser_changeByUserEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

        eventProducer.onProgressChanged(seekBar, progress, false)
        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)

        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 0.5F), latest)
        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_USER, progress), latest)
    }

    @Test
    fun onProgressChangeByProgram_zeroWidthSlider_changeByProgramEventProduced_withMaxProgress() =
        runTest {
            // No-width slider where the min and max values are the same
            seekBar.min = 100
            seekBar.max = 100
            val progress = 50
    fun onProgressChangeByProgram_changeByProgramEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, false)
        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)

            assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, 1.0F), latest)
        assertEquals(SliderEvent(SliderEventType.PROGRESS_CHANGE_BY_PROGRAM, progress), latest)
    }

    @Test
    fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced_withNormalizedProgress() =
        runTest {
            val progress = 50
    fun onStartTrackingTouch_afterProgress_trackingTouchEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)
            eventProducer.onStartTrackingTouch(seekBar)
        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
        eventProducer.onStartTracking(/*fromUser =*/ true)

            assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, 0.5F), latest)
        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_TOUCH, progress), latest)
    }

    @Test
    fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced_withNormalizedProgress() =
        runTest {
            val progress = 50
    fun onStopTrackingTouch_afterProgress_stopTrackingTouchEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

            eventProducer.onProgressChanged(seekBar, progress, true)
            eventProducer.onStopTrackingTouch(seekBar)
        eventProducer.onProgressChanged(/*fromUser =*/ true, progress)
        eventProducer.onStopTracking(/*fromUser =*/ true)

            assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, 0.5F), latest)
        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_TOUCH, progress), latest)
    }

    @Test
    fun onArrowUp_afterStartTrackingTouch_ArrowUpProduced() = runTest {
    fun onStartTrackingProgram_afterProgress_trackingProgramEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

        eventProducer.onStartTrackingTouch(seekBar)
        eventProducer.onArrowUp()
        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
        eventProducer.onStartTracking(/*fromUser =*/ false)

        assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0f), latest)
        assertEquals(SliderEvent(SliderEventType.STARTED_TRACKING_PROGRAM, progress), latest)
    }

    @Test
    fun onArrowUp_afterChangeByProgram_ArrowUpProduced_withProgress() = runTest {
        val progress = 50
    fun onStopTrackingProgram_afterProgress_stopTrackingProgramEventProduced() = runTest {
        val progress = 0.5f
        val latest by collectLastValue(eventFlow)

        eventProducer.onProgressChanged(seekBar, progress, false)
        eventProducer.onArrowUp()
        eventProducer.onProgressChanged(/*fromUser =*/ false, progress)
        eventProducer.onStopTracking(/*fromUser =*/ false)

        assertEquals(SliderEvent(SliderEventType.ARROW_UP, 0.5f), latest)
        assertEquals(SliderEvent(SliderEventType.STOPPED_TRACKING_PROGRAM, progress), latest)
    }
}
+2 −2
Original line number Diff line number Diff line
@@ -23,11 +23,11 @@ import kotlinx.coroutines.awaitCancellation

object HapticSliderViewBinder {
    /**
     * Binds a [SeekableSliderHapticPlugin] to a [View]. The binded view should be a
     * Binds a [SeekbarHapticPlugin] to a [View]. The binded view should be a
     * [android.widget.SeekBar] or a container of a [android.widget.SeekBar]
     */
    @JvmStatic
    fun bind(view: View?, plugin: SeekableSliderHapticPlugin) {
    fun bind(view: View?, plugin: SeekbarHapticPlugin) {
        view?.repeatWhenAttached {
            plugin.startInScope(lifecycleScope)
            try {
+38 −14
Original line number Diff line number Diff line
@@ -30,12 +30,12 @@ import kotlinx.coroutines.launch
/**
 * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
 *
 * A [SeekableSliderEventProducer] is used as the producer of slider events, a
 * A [SliderStateProducer] is used as the producer of slider events, a
 * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
 * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that
 * depending on the state, and a [SliderStateTracker] is used as the state machine handler that
 * tracks and manipulates the slider state.
 */
class SeekableSliderHapticPlugin
class SeekbarHapticPlugin
@JvmOverloads
constructor(
    vibratorHelper: VibratorHelper,
@@ -46,7 +46,7 @@ constructor(

    private val velocityTracker = VelocityTracker.obtain()

    private val sliderEventProducer = SeekableSliderEventProducer()
    private val sliderEventProducer = SliderStateProducer()

    private val sliderHapticFeedbackProvider =
        SliderHapticFeedbackProvider(
@@ -56,7 +56,7 @@ constructor(
            systemClock,
        )

    private var sliderTracker: SeekableSliderTracker? = null
    private var sliderTracker: SliderStateTracker? = null

    private var pluginScope: CoroutineScope? = null

@@ -86,7 +86,7 @@ constructor(
    fun startInScope(scope: CoroutineScope) {
        if (sliderTracker != null) stop()
        sliderTracker =
            SeekableSliderTracker(
            SliderStateTracker(
                sliderHapticFeedbackProvider,
                sliderEventProducer,
                scope,
@@ -116,28 +116,52 @@ constructor(
    /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
    fun onStartTrackingTouch(seekBar: SeekBar) {
        if (isTracking) {
            sliderEventProducer.onStartTrackingTouch(seekBar)
            sliderEventProducer.onStartTracking(true)
        }
    }

    /** onProgressChanged event from the slider's [android.widget.SeekBar] */
    fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
        if (isTracking) {
            sliderEventProducer.onProgressChanged(seekBar, progress, fromUser)
            if (sliderTracker?.currentState == SliderState.IDLE && !fromUser) {
                // This case translates to the slider starting to track program changes
                sliderEventProducer.resetWithProgress(normalizeProgress(seekBar, progress))
                sliderEventProducer.onStartTracking(false)
            } else {
                sliderEventProducer.onProgressChanged(
                    fromUser,
                    normalizeProgress(seekBar, progress),
                )
            }
        }
    }

    /**
     * Normalize the integer progress of a SeekBar to the range from 0F to 1F.
     *
     * @param[seekBar] The SeekBar that reports a progress.
     * @param[progress] The integer progress of the SeekBar within its min and max values.
     * @return The progress in the range from 0F to 1F.
     */
    private fun normalizeProgress(seekBar: SeekBar, progress: Int): Float {
        if (seekBar.max == seekBar.min) {
            return 1.0f
        }
        val range = seekBar.max - seekBar.min
        return (progress - seekBar.min) / range.toFloat()
    }

    /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
    fun onStopTrackingTouch(seekBar: SeekBar) {
        if (isTracking) {
            sliderEventProducer.onStopTrackingTouch(seekBar)
            sliderEventProducer.onStopTracking(true)
        }
    }

    /** onArrowUp event recorded */
    fun onArrowUp() {
    /** Programmatic changes have stopped */
    private fun onStoppedTrackingProgram() {
        if (isTracking) {
            sliderEventProducer.onArrowUp()
            sliderEventProducer.onStopTracking(false)
        }
    }

@@ -146,7 +170,7 @@ constructor(
     *
     * This event is used to estimate the key-up event based on a running a timer as a waiting
     * coroutine in the [pluginScope]. A key-up event in a slider corresponds to an onArrowUp event.
     * Therefore, [onArrowUp] must be called after the timeout.
     * Therefore, [onStoppedTrackingProgram] must be called after the timeout.
     */
    fun onKeyDown() {
        if (!isTracking) return
@@ -158,7 +182,7 @@ constructor(
        keyUpJob =
            pluginScope?.launch {
                delay(KEY_UP_TIMEOUT)
                onArrowUp()
                onStoppedTrackingProgram()
            }
    }

+3 −1
Original line number Diff line number Diff line
@@ -22,6 +22,8 @@ enum class SliderEventType {
    NOTHING,
    /* The slider has captured a touch input and is tracking touch events. */
    STARTED_TRACKING_TOUCH,
    /* The slider started tracking programmatic value changes */
    STARTED_TRACKING_PROGRAM,
    /* The slider progress is changing due to user touch input. */
    PROGRESS_CHANGE_BY_USER,
    /* The slider progress is changing programmatically. */
@@ -29,5 +31,5 @@ enum class SliderEventType {
    /* The slider has stopped tracking touch events. */
    STOPPED_TRACKING_TOUCH,
    /* The external (not touch) stimulus that was modifying the slider progress has stopped. */
    ARROW_UP,
    STOPPED_TRACKING_PROGRAM,
}
Loading