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

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

Merge "Adding a SliderDragVelocityProvider for slider haptics" into main

parents 6d1c1fcd 9144c23b
Loading
Loading
Loading
Loading
+14 −41
Original line number Diff line number Diff line
@@ -17,13 +17,11 @@
package com.android.systemui.haptics.slider

import android.os.VibrationEffect
import android.view.VelocityTracker
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.fakeVibratorHelper
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.fakeSystemClock
import kotlin.math.max
import kotlin.test.assertEquals
@@ -31,19 +29,17 @@ import kotlin.test.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
class SliderHapticFeedbackProviderTest : SysuiTestCase() {

    @Mock private lateinit var velocityTracker: VelocityTracker

    private val kosmos = testKosmos()

    private val config = SliderHapticFeedbackConfig()

    private val dragVelocityProvider = SliderDragVelocityProvider { config.maxVelocityToScale }

    private val lowTickDuration = 12 // Mocked duration of a low tick
    private val dragTextureThresholdMillis =
        lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
@@ -52,17 +48,13 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true)
        whenever(velocityTracker.getAxisVelocity(config.velocityAxis))
            .thenReturn(config.maxVelocityToScale)

        vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
            lowTickDuration
        sliderHapticFeedbackProvider =
            SliderHapticFeedbackProvider(
                vibratorHelper,
                velocityTracker,
                dragVelocityProvider,
                config,
                kosmos.fakeSystemClock,
            )
@@ -75,9 +67,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -93,7 +83,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale)
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -110,9 +100,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -128,9 +116,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -146,10 +132,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -222,10 +205,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -234,9 +214,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -250,7 +228,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // THEN there are two bookend vibrations
            assertEquals(
                /* expected= */ 2,
                vibratorHelper.timesVibratedWithEffect(bookendVibration)
                vibratorHelper.timesVibratedWithEffect(bookendVibration),
            )
        }

@@ -260,10 +238,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -272,9 +247,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -288,7 +261,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // THEN there are two bookend vibrations
            assertEquals(
                /* expected= */ 2,
                vibratorHelper.timesVibratedWithEffect(bookendVibration)
                vibratorHelper.timesVibratedWithEffect(bookendVibration),
            )
        }

+14 −1
Original line number Diff line number Diff line
@@ -46,12 +46,24 @@ constructor(

    private val velocityTracker = VelocityTracker.obtain()

    private val dragVelocityProvider = SliderDragVelocityProvider {
        velocityTracker.computeCurrentVelocity(
            UNITS_SECOND,
            sliderHapticFeedbackConfig.maxVelocityToScale,
        )
        if (velocityTracker.isAxisSupported(sliderHapticFeedbackConfig.velocityAxis)) {
            velocityTracker.getAxisVelocity(sliderHapticFeedbackConfig.velocityAxis)
        } else {
            0f
        }
    }

    private val sliderEventProducer = SliderStateProducer()

    private val sliderHapticFeedbackProvider =
        SliderHapticFeedbackProvider(
            vibratorHelper,
            velocityTracker,
            dragVelocityProvider,
            sliderHapticFeedbackConfig,
            systemClock,
        )
@@ -188,5 +200,6 @@ constructor(

    companion object {
        const val KEY_UP_TIMEOUT = 60L
        private const val UNITS_SECOND = 1000
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * 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.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

/** A provider of the velocity at which a slider is being dragged */
fun interface SliderDragVelocityProvider {

    /**
     * Get the velocity of the slider at the time this function is called.
     *
     * @return the velocity of the drag in pixels/sec
     */
    fun getTrackedVelocity(): Float
}
+1 −1
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ data class SliderHapticFeedbackConfig(
    /** Number of low ticks in a drag texture composition. This is not expected to change */
    val numberOfLowTicks: Int = 5,
    /** Maximum velocity allowed for vibration scaling. This is not expected to change. */
    val maxVelocityToScale: Float = 2000f, /* In pixels/sec */
    val maxVelocityToScale: Float = 2000f, /* In units/sec. The default units are pixels */
    /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */
    val velocityAxis: Int = MotionEvent.AXIS_X,
    /** Vibration scale at the upper bookend of the slider */
+7 −16
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ import kotlin.math.pow
 */
class SliderHapticFeedbackProvider(
    private val vibratorHelper: VibratorHelper,
    private val velocityTracker: VelocityTracker,
    private val velocityProvider: SliderDragVelocityProvider,
    private val config: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
    private val clock: com.android.systemui.util.time.SystemClock,
) : SliderStateListener {
@@ -50,6 +50,7 @@ class SliderHapticFeedbackProvider(
    private var dragTextureLastTime = clock.elapsedRealtime()
    var dragTextureLastProgress = -1f
        private set

    private val lowTickDurationMs =
        vibratorHelper.getPrimitiveDurations(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0]
    private var hasVibratedAtLowerBookend = false
@@ -99,7 +100,7 @@ class SliderHapticFeedbackProvider(
     */
    private fun vibrateDragTexture(
        absoluteVelocity: Float,
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
    ) {
        // Check if its time to vibrate
        val currentTime = clock.elapsedRealtime()
@@ -132,7 +133,7 @@ class SliderHapticFeedbackProvider(
    @VisibleForTesting
    fun scaleOnDragTexture(
        absoluteVelocity: Float,
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
    ): Float {
        val velocityInterpolated =
            velocityAccelerateInterpolator.getInterpolation(
@@ -162,33 +163,24 @@ class SliderHapticFeedbackProvider(

    override fun onLowerBookend() {
        if (!hasVibratedAtLowerBookend) {
            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
            vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
            hasVibratedAtLowerBookend = true
        }
    }

    override fun onUpperBookend() {
        if (!hasVibratedAtUpperBookend) {
            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
            vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
            hasVibratedAtUpperBookend = true
        }
    }

    override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
        vibrateDragTexture(abs(getTrackedVelocity()), progress)
        vibrateDragTexture(abs(velocityProvider.getTrackedVelocity()), progress)
        hasVibratedAtUpperBookend = false
        hasVibratedAtLowerBookend = false
    }

    private fun getTrackedVelocity(): Float {
        velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
        return if (velocityTracker.isAxisSupported(config.velocityAxis)) {
            velocityTracker.getAxisVelocity(config.velocityAxis)
        } else {
            0f
        }
    }

    override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}

    override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
@@ -199,6 +191,5 @@ class SliderHapticFeedbackProvider(
                .setUsage(VibrationAttributes.USAGE_TOUCH)
                .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
                .build()
        private const val UNITS_SECOND = 1000
    }
}