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

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

Merge "Refactoring the quick settings long-press effect" into main

parents e3ff4ab9 83c620d9
Loading
Loading
Loading
Loading
+113 −65
Original line number Diff line number Diff line
@@ -26,39 +26,34 @@ import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.AnimatorTestRule
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.backgroundCoroutineContext
import com.android.systemui.kosmos.testScope
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule

@SmallTest
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
@RunWithLooper(setAsMainLooper = true)
class QSLongPressEffectTest : SysuiTestCase() {

    @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
    @Mock private lateinit var vibratorHelper: VibratorHelper
    @Mock private lateinit var testView: View
    @get:Rule val animatorTestRule = AnimatorTestRule(this)
    private val kosmos = testKosmos()
    private val vibratorHelper = kosmos.vibratorHelper

    private val effectDuration = 400
    private val lowTickDuration = 12
@@ -68,19 +63,71 @@ class QSLongPressEffectTest : SysuiTestCase() {

    @Before
    fun setup() {
        whenever(
                vibratorHelper.getPrimitiveDurations(
                    VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                    VibrationEffect.Composition.PRIMITIVE_SPIN,
                )
            )
            .thenReturn(intArrayOf(lowTickDuration, spinDuration))
        vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
            lowTickDuration
        vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_SPIN] = spinDuration

        kosmos.fakeKeyguardRepository.setKeyguardDismissible(true)

        longPressEffect =
            QSLongPressEffect(
                vibratorHelper,
                effectDuration,
                kosmos.keyguardInteractor,
                CoroutineScope(kosmos.backgroundCoroutineContext),
            )
        longPressEffect.initializeEffect(effectDuration)
    }

    @Test
    fun onReset_whileIdle_resetsEffect() = testWithScope {
        // GIVEN a call to reset
        longPressEffect.resetEffect()

        // THEN the effect remains idle and has not been initialized
        val state by collectLastValue(longPressEffect.state)
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(longPressEffect.hasInitialized).isFalse()
    }

    @Test
    fun onReset_whileRunning_resetsEffect() = testWhileRunning {
        // GIVEN a call to reset
        longPressEffect.resetEffect()

        // THEN the effect remains idle and has not been initialized
        val state by collectLastValue(longPressEffect.state)
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(longPressEffect.hasInitialized).isFalse()
    }

    @Test
    fun onInitialize_withNegativeDuration_doesNotInitialize() = testWithScope {
        // GIVEN an effect that has reset
        longPressEffect.resetEffect()

        // WHEN attempting to initialize with a negative duration
        val couldInitialize = longPressEffect.initializeEffect(-1)

        // THEN the effect can't initialized and remains reset
        val state by collectLastValue(longPressEffect.state)
        assertThat(couldInitialize).isFalse()
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(longPressEffect.hasInitialized).isFalse()
    }

    @Test
    fun onInitialize_withPositiveDuration_initializes() = testWithScope {
        // GIVEN an effect that has reset
        longPressEffect.resetEffect()

        // WHEN attempting to initialize with a positive duration
        val couldInitialize = longPressEffect.initializeEffect(effectDuration)

        // THEN the effect is initialized
        val state by collectLastValue(longPressEffect.state)
        assertThat(couldInitialize).isTrue()
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(longPressEffect.hasInitialized).isTrue()
    }

    @Test
@@ -90,7 +137,8 @@ class QSLongPressEffectTest : SysuiTestCase() {
        longPressEffect.onTouch(testView, downEvent)

        // THEN the effect moves to the TIMEOUT_WAIT state
        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
        val state by collectLastValue(longPressEffect.state)
        assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
    }

    @Test
@@ -100,7 +148,8 @@ class QSLongPressEffectTest : SysuiTestCase() {
        longPressEffect.onTouch(testView, cancelEvent)

        // THEN the effect goes back to idle and does not start
        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
        val state by collectLastValue(longPressEffect.state)
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertEffectDidNotStart()
    }

@@ -121,7 +170,7 @@ class QSLongPressEffectTest : SysuiTestCase() {
    @Test
    fun onWaitComplete_whileWaiting_beginsEffect() = testWhileWaiting {
        // GIVEN the pressed timeout is complete
        advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
        longPressEffect.handleTimeoutComplete()

        // THEN the effect starts
        assertEffectStarted()
@@ -154,12 +203,25 @@ class QSLongPressEffectTest : SysuiTestCase() {
    }

    @Test
    fun onAnimationComplete_effectEnds() = testWhileRunning {
    fun onAnimationComplete_keyguardDismissible_effectEndsWithLongPress() = testWhileRunning {
        // GIVEN that the animation completes
        animatorTestRule.advanceTimeBy(effectDuration + 10L)

        // THEN the long-press effect completes
        assertEffectCompleted()
        // THEN the long-press effect completes with a LONG_PRESS
        assertEffectCompleted(QSLongPressEffect.ActionType.LONG_PRESS)
    }

    @Test
    fun onAnimationComplete_keyguardNotDismissible_effectEndsWithResetAndLongPress() =
        testWhileRunning {
            // GIVEN that the keyguard is not dismissible
            kosmos.fakeKeyguardRepository.setKeyguardDismissible(false)

            // GIVEN that the animation completes
            animatorTestRule.advanceTimeBy(effectDuration + 10L)

            // THEN the long-press effect completes with RESET_AND_LONG_PRESS
            assertEffectCompleted(QSLongPressEffect.ActionType.RESET_AND_LONG_PRESS)
        }

    @Test
@@ -192,33 +254,21 @@ class QSLongPressEffectTest : SysuiTestCase() {
        animatorTestRule.advanceTimeBy(effectDuration.toLong())

        // THEN the state goes to [QSLongPressEffect.State.IDLE]
        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
        val state by collectLastValue(longPressEffect.state)
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
    }

    private fun buildMotionEvent(action: Int): MotionEvent =
        MotionEventBuilder.newBuilder().setAction(action).build()

    private fun testWithScope(test: suspend TestScope.() -> Unit) =
        with(kosmos) {
            testScope.runTest {
                // GIVEN an effect with a testing scope
                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))

                // THEN run the test
                test()
            }
        }
        with(kosmos) { testScope.runTest { test() } }

    private fun testWhileWaiting(test: suspend TestScope.() -> Unit) =
        with(kosmos) {
            testScope.runTest {
                // GIVEN an effect with a testing scope
                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))

                // GIVEN the TIMEOUT_WAIT state is entered
                val downEvent =
                    MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
                longPressEffect.onTouch(testView, downEvent)
                longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT)

                // THEN run the test
                test()
@@ -228,16 +278,9 @@ class QSLongPressEffectTest : SysuiTestCase() {
    private fun testWhileRunning(test: suspend TestScope.() -> Unit) =
        with(kosmos) {
            testScope.runTest {
                // GIVEN an effect with a testing scope
                longPressEffect.scope = CoroutineScope(UnconfinedTestDispatcher(testScheduler))

                // GIVEN the down event that enters the TIMEOUT_WAIT state
                val downEvent =
                    MotionEventBuilder.newBuilder().setAction(MotionEvent.ACTION_DOWN).build()
                longPressEffect.onTouch(testView, downEvent)

                // GIVEN that the timeout completes and the effect starts
                advanceTimeBy(QSLongPressEffect.PRESSED_TIMEOUT + 10L)
                // GIVEN that the effect starts after the tap timeout is complete
                longPressEffect.setState(QSLongPressEffect.State.TIMEOUT_WAIT)
                longPressEffect.handleTimeoutComplete()

                // THEN run the test
                test()
@@ -252,6 +295,7 @@ class QSLongPressEffectTest : SysuiTestCase() {
     */
    private fun TestScope.assertEffectStarted() {
        val effectProgress by collectLastValue(longPressEffect.effectProgress)
        val state by collectLastValue(longPressEffect.state)
        val longPressHint =
            LongPressHapticBuilder.createLongPressHint(
                lowTickDuration,
@@ -259,10 +303,10 @@ class QSLongPressEffectTest : SysuiTestCase() {
                effectDuration,
            )

        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
        assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
        assertThat(effectProgress).isEqualTo(0f)
        assertThat(longPressHint).isNotNull()
        verify(vibratorHelper).vibrate(longPressHint!!)
        assertThat(vibratorHelper.hasVibratedWithEffects(longPressHint!!)).isTrue()
    }

    /**
@@ -274,11 +318,12 @@ class QSLongPressEffectTest : SysuiTestCase() {
     */
    private fun TestScope.assertEffectDidNotStart() {
        val effectProgress by collectLastValue(longPressEffect.effectProgress)
        val state by collectLastValue(longPressEffect.state)

        assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
        assertThat(longPressEffect.state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
        assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_FORWARD)
        assertThat(state).isNotEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
        assertThat(effectProgress).isNull()
        verify(vibratorHelper, never()).vibrate(any(/* type= */ VibrationEffect::class.java))
        assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
    }

    /**
@@ -286,18 +331,19 @@ class QSLongPressEffectTest : SysuiTestCase() {
     * 1. The progress is null
     * 2. The final snap haptics are played
     * 3. The internal state goes back to [QSLongPressEffect.State.IDLE]
     * 4. The action to perform on the tile is the long-press action
     * 4. The action to perform on the tile is the action given as a parameter
     */
    private fun TestScope.assertEffectCompleted() {
    private fun TestScope.assertEffectCompleted(expectedAction: QSLongPressEffect.ActionType) {
        val action by collectLastValue(longPressEffect.actionType)
        val effectProgress by collectLastValue(longPressEffect.effectProgress)
        val snapEffect = LongPressHapticBuilder.createSnapEffect()
        val state by collectLastValue(longPressEffect.state)

        assertThat(effectProgress).isNull()
        assertThat(snapEffect).isNotNull()
        verify(vibratorHelper).vibrate(snapEffect!!)
        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(action).isEqualTo(QSLongPressEffect.ActionType.LONG_PRESS)
        assertThat(vibratorHelper.hasVibratedWithEffects(snapEffect!!)).isTrue()
        assertThat(state).isEqualTo(QSLongPressEffect.State.IDLE)
        assertThat(action).isEqualTo(expectedAction)
    }

    /**
@@ -305,17 +351,18 @@ class QSLongPressEffectTest : SysuiTestCase() {
     * 1. The internal state is [QSLongPressEffect.State.RUNNING_BACKWARDS]
     * 2. The reverse haptics plays at the point where the animation was paused
     */
    private fun assertEffectReverses(pausedProgress: Float) {
    private fun TestScope.assertEffectReverses(pausedProgress: Float) {
        val reverseHaptics =
            LongPressHapticBuilder.createReversedEffect(
                pausedProgress,
                lowTickDuration,
                effectDuration,
            )
        val state by collectLastValue(longPressEffect.state)

        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
        assertThat(state).isEqualTo(QSLongPressEffect.State.RUNNING_BACKWARDS)
        assertThat(reverseHaptics).isNotNull()
        verify(vibratorHelper).vibrate(reverseHaptics!!)
        assertThat(vibratorHelper.hasVibratedWithEffects(reverseHaptics!!)).isTrue()
    }

    /**
@@ -325,8 +372,9 @@ class QSLongPressEffectTest : SysuiTestCase() {
     */
    private fun TestScope.assertEffectResets() {
        val effectProgress by collectLastValue(longPressEffect.effectProgress)
        assertThat(effectProgress).isEqualTo(0f)
        val state by collectLastValue(longPressEffect.state)

        assertThat(longPressEffect.state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
        assertThat(effectProgress).isNull()
        assertThat(state).isEqualTo(QSLongPressEffect.State.TIMEOUT_WAIT)
    }
}
+114 −87
Original line number Diff line number Diff line
@@ -27,40 +27,65 @@ import androidx.annotation.VisibleForTesting
import androidx.core.animation.doOnCancel
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.statusbar.VibratorHelper
import kotlinx.coroutines.CancellationException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
 * A class that handles the long press visuo-haptic effect for a QS tile.
 *
 * The class is also a [View.OnTouchListener] to handle the touch events, clicks and long-press
 * gestures of the tile. The class also provides a [State] that can be used to determine the current
 * gestures of the tile. The class also provides a [State] tha can be used to determine the current
 * state of the long press effect.
 *
 * @property[vibratorHelper] The [VibratorHelper] to deliver haptic effects.
 * @property[effectDuration] The duration of the effect in ms.
 */
class QSLongPressEffect(
// TODO(b/332902869): In addition from being injectable, we can consider making it a singleton
class QSLongPressEffect
@Inject
constructor(
    private val vibratorHelper: VibratorHelper?,
    private val effectDuration: Int,
    val keyguardInteractor: KeyguardInteractor,
    @Background bgScope: CoroutineScope,
) : View.OnTouchListener {

    private var effectDuration = 0

    /** Current state */
    var state = State.IDLE
        @VisibleForTesting set
    private var _state = MutableStateFlow(State.IDLE)
    val state = _state.stateIn(bgScope, SharingStarted.Lazily, State.IDLE)

    /** Flows for view control and action */
    private val _effectProgress = MutableStateFlow<Float?>(null)
    val effectProgress = _effectProgress.asStateFlow()
    val effectProgress = _effectProgress.stateIn(bgScope, SharingStarted.Lazily, null)

    // Actions to perform
    private val _postedActionType = MutableStateFlow<ActionType?>(null)
    val actionType: StateFlow<ActionType?> =
        combine(
                _postedActionType,
                keyguardInteractor.isKeyguardDismissible,
            ) { action, isDismissible ->
                if (!isDismissible && action == ActionType.LONG_PRESS) {
                    ActionType.RESET_AND_LONG_PRESS
                } else {
                    action
                }
            }
            .stateIn(bgScope, SharingStarted.Lazily, null)

    private val _actionType = MutableStateFlow<ActionType?>(null)
    val actionType = _actionType.asStateFlow()
    // Should a tap timeout countdown begin
    val shouldWaitForTapTimeout: Flow<Boolean> = state.map { it == State.TIMEOUT_WAIT }

    /** Haptic effects */
    private val durations =
@@ -69,32 +94,23 @@ class QSLongPressEffect(
            VibrationEffect.Composition.PRIMITIVE_SPIN
        )

    private val longPressHint =
        LongPressHapticBuilder.createLongPressHint(
            durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
            durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
            effectDuration
        )
    private var longPressHint: VibrationEffect? = null

    private val snapEffect = LongPressHapticBuilder.createSnapEffect()

    /* A coroutine scope and a timer job that waits for the pressedTimeout */
    var scope: CoroutineScope? = null
    private var waitJob: Job? = null
    private var effectAnimator: ValueAnimator? = null

    private val effectAnimator =
        ValueAnimator.ofFloat(0f, 1f).apply {
            duration = effectDuration.toLong()
            interpolator = AccelerateDecelerateInterpolator()
    val hasInitialized: Boolean
        get() = longPressHint != null && effectAnimator != null

            doOnStart { handleAnimationStart() }
            addUpdateListener { _effectProgress.value = animatedValue as Float }
            doOnEnd { handleAnimationComplete() }
            doOnCancel { handleAnimationCancel() }
    @VisibleForTesting
    fun setState(state: State) {
        _state.value = state
    }

    private fun reverse() {
        val pausedProgress = effectAnimator.animatedFraction
        effectAnimator?.let {
            val pausedProgress = it.animatedFraction
            val effect =
                LongPressHapticBuilder.createReversedEffect(
                    pausedProgress,
@@ -103,7 +119,8 @@ class QSLongPressEffect(
                )
            vibratorHelper?.cancel()
            vibrate(effect)
        effectAnimator.reverse()
            it.reverse()
        }
    }

    private fun vibrate(effect: VibrationEffect?) {
@@ -129,52 +146,37 @@ class QSLongPressEffect(
    }

    private fun handleActionDown() {
        when (state) {
        when (_state.value) {
            State.IDLE -> {
                startPressedTimeoutWait()
                state = State.TIMEOUT_WAIT
                setState(State.TIMEOUT_WAIT)
            }
            State.RUNNING_BACKWARDS -> effectAnimator.cancel()
            State.RUNNING_BACKWARDS -> effectAnimator?.cancel()
            else -> {}
        }
    }

    private fun startPressedTimeoutWait() {
        waitJob =
            scope?.launch {
                try {
                    delay(PRESSED_TIMEOUT)
                    handleTimeoutComplete()
                } catch (_: CancellationException) {
                    state = State.IDLE
                }
            }
    }

    private fun handleActionUp() {
        when (state) {
        when (_state.value) {
            State.TIMEOUT_WAIT -> {
                waitJob?.cancel()
                _actionType.value = ActionType.CLICK
                state = State.IDLE
                _postedActionType.value = ActionType.CLICK
                setState(State.IDLE)
            }
            State.RUNNING_FORWARD -> {
                reverse()
                state = State.RUNNING_BACKWARDS
                setState(State.RUNNING_BACKWARDS)
            }
            else -> {}
        }
    }

    private fun handleActionCancel() {
        when (state) {
        when (_state.value) {
            State.TIMEOUT_WAIT -> {
                waitJob?.cancel()
                state = State.IDLE
                setState(State.IDLE)
            }
            State.RUNNING_FORWARD -> {
                reverse()
                state = State.RUNNING_BACKWARDS
                setState(State.RUNNING_BACKWARDS)
            }
            else -> {}
        }
@@ -182,54 +184,78 @@ class QSLongPressEffect(

    private fun handleAnimationStart() {
        vibrate(longPressHint)
        state = State.RUNNING_FORWARD
        setState(State.RUNNING_FORWARD)
    }

    /** This function is called both when an animator completes or gets cancelled */
    private fun handleAnimationComplete() {
        if (state == State.RUNNING_FORWARD) {
        if (_state.value == State.RUNNING_FORWARD) {
            vibrate(snapEffect)
            _actionType.value = ActionType.LONG_PRESS
            _postedActionType.value = ActionType.LONG_PRESS
            _effectProgress.value = null
        }
        if (state != State.TIMEOUT_WAIT) {
        if (_state.value != State.TIMEOUT_WAIT) {
            // This will happen if the animator did not finish by being cancelled
            state = State.IDLE
            setState(State.IDLE)
        }
    }

    private fun handleAnimationCancel() {
        _effectProgress.value = 0f
        startPressedTimeoutWait()
        state = State.TIMEOUT_WAIT
        _effectProgress.value = null
        setState(State.TIMEOUT_WAIT)
    }

    private fun handleTimeoutComplete() {
        if (state == State.TIMEOUT_WAIT && !effectAnimator.isRunning) {
            effectAnimator.start()
    fun handleTimeoutComplete() {
        if (_state.value == State.TIMEOUT_WAIT && effectAnimator?.isRunning == false) {
            effectAnimator?.start()
        }
    }

    fun clearActionType() {
        _actionType.value = null
        _postedActionType.value = null
    }

    /** Reset the effect by going back to a default [IDLE] state */
    fun resetEffect() {
        if (effectAnimator?.isRunning == true) {
            effectAnimator?.cancel()
        }
        longPressHint = null
        effectAnimator = null
        _effectProgress.value = null
        _postedActionType.value = null
        setState(State.IDLE)
    }

    /**
     * Reset the effect with a new effect duration.
     *
     * The effect will go back to an [IDLE] state where it can begin its logic with a new duration.
     *
     * @param[duration] New duration for the long-press effect
     * @return true if the effect initialized correctly
     */
    fun resetWithDuration(duration: Int) {
    fun initializeEffect(duration: Int): Boolean {
        // The effect can't reset if it is running
        if (effectAnimator.isRunning) return
        if (duration <= 0) return false

        effectAnimator.duration = duration.toLong()
        _effectProgress.value = 0f
        _actionType.value = null
        waitJob?.cancel()
        state = State.IDLE
        resetEffect()
        effectDuration = duration
        effectAnimator =
            ValueAnimator.ofFloat(0f, 1f).apply {
                this.duration = effectDuration.toLong()
                interpolator = AccelerateDecelerateInterpolator()

                doOnStart { handleAnimationStart() }
                addUpdateListener { _effectProgress.value = animatedValue as Float }
                doOnEnd { handleAnimationComplete() }
                doOnCancel { handleAnimationCancel() }
            }
        longPressHint =
            LongPressHapticBuilder.createLongPressHint(
                durations?.get(0) ?: LongPressHapticBuilder.INVALID_DURATION,
                durations?.get(1) ?: LongPressHapticBuilder.INVALID_DURATION,
                effectDuration
            )
        return true
    }

    enum class State {
@@ -243,6 +269,7 @@ class QSLongPressEffect(
    enum class ActionType {
        CLICK,
        LONG_PRESS,
        RESET_AND_LONG_PRESS,
    }

    companion object {
+46 −34

File changed.

Preview size limit exceeded, changes collapsed.

+4 −3

File changed.

Preview size limit exceeded, changes collapsed.

+14 −5

File changed.

Preview size limit exceeded, changes collapsed.

Loading