summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt160
-rw-r--r--packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateListener.kt64
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java21
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt247
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt12
6 files changed, 547 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
new file mode 100644
index 000000000000..20d99d1e75fb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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
+
+import androidx.annotation.FloatRange
+
+/** Configuration parameters of a [SliderHapticFeedbackProvider] */
+data class SliderHapticFeedbackConfig(
+ /** Interpolator factor for velocity-based vibration scale interpolations. Must be positive */
+ val velocityInterpolatorFactor: Float = 1f,
+ /** Interpolator factor for progress-based vibration scale interpolations. Must be positive */
+ val progressInterpolatorFactor: Float = 1f,
+ /** Minimum vibration scale for vibrations based on slider progress */
+ @FloatRange(from = 0.0, to = 1.0) val progressBasedDragMinScale: Float = 0f,
+ /** Maximum vibration scale for vibrations based on slider progress */
+ @FloatRange(from = 0.0, to = 1.0) val progressBasedDragMaxScale: Float = 0.2f,
+ /** Additional vibration scaling due to velocity */
+ @FloatRange(from = 0.0, to = 1.0) val additionalVelocityMaxBump: Float = 0.15f,
+ /** Additional time delta to wait between drag texture vibrations */
+ @FloatRange(from = 0.0) val deltaMillisForDragInterval: Float = 0f,
+ /** 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 */
+ /** Vibration scale at the upper bookend of the slider */
+ @FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f,
+ /** Vibration scale at the lower bookend of the slider */
+ @FloatRange(from = 0.0, to = 1.0) val lowerBookendScale: Float = 0.05f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
new file mode 100644
index 000000000000..e6de156de0c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.os.VibrationAttributes
+import android.os.VibrationEffect
+import android.view.VelocityTracker
+import android.view.animation.AccelerateInterpolator
+import androidx.annotation.FloatRange
+import com.android.systemui.statusbar.VibratorHelper
+import kotlin.math.abs
+import kotlin.math.min
+
+/**
+ * Listener of slider events that triggers haptic feedback.
+ *
+ * @property[vibratorHelper] Singleton instance of the [VibratorHelper] to deliver haptics.
+ * @property[velocityTracker] Instance of a [VelocityTracker] that tracks slider dragging velocity.
+ * @property[config] Configuration parameters for vibration encapsulated as a
+ * [SliderHapticFeedbackConfig].
+ * @property[clock] Clock to obtain elapsed real time values.
+ */
+class SliderHapticFeedbackProvider(
+ private val vibratorHelper: VibratorHelper,
+ private val velocityTracker: VelocityTracker,
+ private val config: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
+ private val clock: com.android.systemui.util.time.SystemClock,
+) : SliderStateListener {
+
+ private val velocityAccelerateInterpolator =
+ AccelerateInterpolator(config.velocityInterpolatorFactor)
+ private val positionAccelerateInterpolator =
+ AccelerateInterpolator(config.progressInterpolatorFactor)
+ private var dragTextureLastTime = clock.elapsedRealtime()
+ private val lowTickDurationMs =
+ vibratorHelper.getPrimitiveDurations(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0]
+ private var hasVibratedAtLowerBookend = false
+ private var hasVibratedAtUpperBookend = false
+
+ /** Time threshold to wait before making new API call. */
+ private val thresholdUntilNextDragCallMillis =
+ lowTickDurationMs * config.numberOfLowTicks + config.deltaMillisForDragInterval
+
+ /**
+ * Vibrate when the handle reaches either bookend with a certain velocity.
+ *
+ * @param[absoluteVelocity] Velocity of the handle when it reached the bookend.
+ */
+ private fun vibrateOnEdgeCollision(absoluteVelocity: Float) {
+ val velocityInterpolated =
+ velocityAccelerateInterpolator.getInterpolation(
+ min(absoluteVelocity / config.maxVelocityToScale, 1f)
+ )
+ val bookendScaleRange = config.upperBookendScale - config.lowerBookendScale
+ val bookendsHitScale = bookendScaleRange * velocityInterpolated + config.lowerBookendScale
+
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, bookendsHitScale)
+ .compose()
+ vibratorHelper.vibrate(vibration, VIBRATION_ATTRIBUTES_PIPELINING)
+ }
+
+ /**
+ * Create a drag texture vibration based on velocity and slider progress.
+ *
+ * @param[absoluteVelocity] Absolute velocity of the handle.
+ * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from
+ * 0F to 1F (inclusive).
+ */
+ private fun vibrateDragTexture(
+ absoluteVelocity: Float,
+ @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
+ ) {
+ // Check if its time to vibrate
+ val currentTime = clock.elapsedRealtime()
+ val elapsedSinceLastDrag = currentTime - dragTextureLastTime
+ if (elapsedSinceLastDrag < thresholdUntilNextDragCallMillis) return
+
+ val velocityInterpolated =
+ velocityAccelerateInterpolator.getInterpolation(
+ min(absoluteVelocity / config.maxVelocityToScale, 1f)
+ )
+
+ // Scaling of vibration due to the position of the slider
+ val positionScaleRange = config.progressBasedDragMaxScale - config.progressBasedDragMinScale
+ val sliderProgressInterpolated =
+ positionAccelerateInterpolator.getInterpolation(normalizedSliderProgress)
+ val positionBasedScale =
+ positionScaleRange * sliderProgressInterpolated + config.progressBasedDragMinScale
+
+ // Scaling bump due to velocity
+ val velocityBasedScale = velocityInterpolated * config.additionalVelocityMaxBump
+
+ // Total scale
+ val scale = positionBasedScale + velocityBasedScale
+
+ // Trigger the vibration composition
+ val composition = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale)
+ }
+ vibratorHelper.vibrate(composition.compose(), VIBRATION_ATTRIBUTES_PIPELINING)
+ dragTextureLastTime = currentTime
+ }
+
+ override fun onHandleAcquiredByTouch() {}
+
+ override fun onHandleReleasedFromTouch() {}
+
+ override fun onLowerBookend() {
+ if (!hasVibratedAtLowerBookend) {
+ velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+ vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ hasVibratedAtLowerBookend = true
+ }
+ }
+
+ override fun onUpperBookend() {
+ if (!hasVibratedAtUpperBookend) {
+ velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+ vibrateOnEdgeCollision(abs(velocityTracker.xVelocity))
+ hasVibratedAtUpperBookend = true
+ }
+ }
+
+ override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+ velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
+ vibrateDragTexture(abs(velocityTracker.xVelocity), progress)
+ hasVibratedAtUpperBookend = false
+ hasVibratedAtLowerBookend = false
+ }
+
+ override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
+
+ override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
+
+ private companion object {
+ private val VIBRATION_ATTRIBUTES_PIPELINING =
+ VibrationAttributes.Builder()
+ .setUsage(VibrationAttributes.USAGE_TOUCH)
+ .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
+ .build()
+ private const val UNITS_SECOND = 1000
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateListener.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateListener.kt
new file mode 100644
index 000000000000..9c99c90bb910
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderStateListener.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2023 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
+
+import androidx.annotation.FloatRange
+
+/** Listener of events from a slider (such as [android.widget.SeekBar]) */
+interface SliderStateListener {
+
+ /** Notification that the handle is acquired by touch */
+ fun onHandleAcquiredByTouch()
+
+ /** Notification that the handle was released from touch */
+ fun onHandleReleasedFromTouch()
+
+ /** Notification that the handle reached the lower bookend */
+ fun onLowerBookend()
+
+ /** Notification that the handle reached the upper bookend */
+ fun onUpperBookend()
+
+ /**
+ * Notification that the slider reached a certain progress on the slider track.
+ *
+ * This method is called in all intermediate steps of a continuous progress change as the slider
+ * moves through the slider track.
+ *
+ * @param[progress] The progress of the slider in the range from 0F to 1F (inclusive).
+ */
+ fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float)
+
+ /**
+ * Notification that the slider handle jumped to a selected progress on the slider track.
+ *
+ * This method is specific to the case when the handle performed a single jump to a position on
+ * the slider track and reached the corresponding progress. In this case, [onProgress] is not
+ * called and the new progress reached is represented by the [progress] parameter.
+ *
+ * @param[progress] The selected progress on the slider track that the handle jumps to. The
+ * progress is in the range from 0F to 1F (inclusive).
+ */
+ fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float)
+
+ /**
+ * Notification that the slider handle was moved by a button press.
+ *
+ * @param[progress] The progress of the slider in the range from 0F to 1F (inclusive).
+ */
+ fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 645595c1f7bf..d089252759c7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -118,6 +118,16 @@ public class VibratorHelper {
}
/**
+ * @see Vibrator#vibrate(VibrationEffect, VibrationAttributes)
+ */
+ public void vibrate(@NonNull VibrationEffect effect, @NonNull VibrationAttributes attributes) {
+ if (!hasVibrator()) {
+ return;
+ }
+ mExecutor.execute(() -> mVibrator.vibrate(effect, attributes));
+ }
+
+ /**
* @see Vibrator#hasVibrator()
*/
public boolean hasVibrator() {
@@ -154,6 +164,17 @@ public class VibratorHelper {
}
/**
+ * @see Vibrator#getPrimitiveDurations(int...)
+ */
+ public int[] getPrimitiveDurations(int... primitiveIds) {
+ if (!hasVibrator()) {
+ return new int[]{0};
+ } else {
+ return mVibrator.getPrimitiveDurations(primitiveIds);
+ }
+ }
+
+ /**
* Perform a vibration using a view and the one-way API with flags
* @see View#performHapticFeedback(int feedbackConstant, int flags)
*/
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
new file mode 100644
index 000000000000..0ee348e0d23d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.os.VibrationAttributes
+import android.os.VibrationEffect
+import android.view.VelocityTracker
+import android.view.animation.AccelerateInterpolator
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SliderHapticFeedbackProviderTest : SysuiTestCase() {
+
+ @Mock private lateinit var velocityTracker: VelocityTracker
+ @Mock private lateinit var vibratorHelper: VibratorHelper
+
+ private val config = SliderHapticFeedbackConfig()
+ private val clock = FakeSystemClock()
+
+ private val lowTickDuration = 12 // Mocked duration of a low tick
+ private val dragTextureThresholdMillis =
+ lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
+ private val progressInterpolator = AccelerateInterpolator(config.progressInterpolatorFactor)
+ private val velocityInterpolator = AccelerateInterpolator(config.velocityInterpolatorFactor)
+ private lateinit var sliderHapticFeedbackProvider: SliderHapticFeedbackProvider
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ whenever(vibratorHelper.getPrimitiveDurations(any()))
+ .thenReturn(intArrayOf(lowTickDuration))
+ whenever(velocityTracker.xVelocity).thenReturn(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider =
+ SliderHapticFeedbackProvider(vibratorHelper, velocityTracker, config, clock)
+ }
+
+ @Test
+ fun playHapticAtLowerBookend_playsClick() {
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ sliderHapticFeedbackProvider.onLowerBookend()
+
+ verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtLowerBookend_twoTimes_playsClickOnlyOnce() {
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ sliderHapticFeedbackProvider.onLowerBookend()
+ sliderHapticFeedbackProvider.onLowerBookend()
+
+ verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtUpperBookend_playsClick() {
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ sliderHapticFeedbackProvider.onUpperBookend()
+
+ verify(vibratorHelper).vibrate(eq(vibration), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtUpperBookend_twoTimes_playsClickOnlyOnce() {
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ sliderHapticFeedbackProvider.onUpperBookend()
+ sliderHapticFeedbackProvider.onUpperBookend()
+
+ verify(vibratorHelper, times(1))
+ .vibrate(eq(vibration), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() {
+ // GIVEN max velocity and slider progress
+ val progress = 1f
+ val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val ticks = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
+ }
+
+ // GIVEN system running for 1s
+ clock.advanceTime(1000)
+
+ // WHEN two calls to play occur immediately
+ sliderHapticFeedbackProvider.onProgress(progress)
+ sliderHapticFeedbackProvider.onProgress(progress)
+
+ // THEN the correct composition only plays once
+ verify(vibratorHelper, times(1))
+ .vibrate(eq(ticks.compose()), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtProgress_afterNextDragThreshold_playsLowTicksTwice() {
+ // GIVEN max velocity and slider progress
+ val progress = 1f
+ val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val ticks = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
+ }
+
+ // GIVEN system running for 1s
+ clock.advanceTime(1000)
+
+ // WHEN two calls to play occur with the required threshold separation
+ sliderHapticFeedbackProvider.onProgress(progress)
+ clock.advanceTime(dragTextureThresholdMillis.toLong())
+ sliderHapticFeedbackProvider.onProgress(progress)
+
+ // THEN the correct composition plays two times
+ verify(vibratorHelper, times(2))
+ .vibrate(eq(ticks.compose()), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() {
+ // GIVEN max velocity and slider progress
+ val progress = 1f
+ val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val ticks = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
+ }
+ val bookendVibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ // GIVEN a vibration at the lower bookend followed by a request to vibrate at progress
+ sliderHapticFeedbackProvider.onLowerBookend()
+ sliderHapticFeedbackProvider.onProgress(progress)
+
+ // WHEN a vibration is to trigger again at the lower bookend
+ sliderHapticFeedbackProvider.onLowerBookend()
+
+ // THEN there are two bookend vibrations
+ verify(vibratorHelper, times(2))
+ .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java))
+ }
+
+ @Test
+ fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() {
+ // GIVEN max velocity and slider progress
+ val progress = 1f
+ val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val ticks = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
+ }
+ val bookendVibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(
+ VibrationEffect.Composition.PRIMITIVE_CLICK,
+ scaleAtBookends(config.maxVelocityToScale)
+ )
+ .compose()
+
+ // GIVEN a vibration at the upper bookend followed by a request to vibrate at progress
+ sliderHapticFeedbackProvider.onUpperBookend()
+ sliderHapticFeedbackProvider.onProgress(progress)
+
+ // WHEN a vibration is to trigger again at the upper bookend
+ sliderHapticFeedbackProvider.onUpperBookend()
+
+ // THEN there are two bookend vibrations
+ verify(vibratorHelper, times(2))
+ .vibrate(eq(bookendVibration), any(VibrationAttributes::class.java))
+ }
+
+ private fun scaleAtBookends(velocity: Float): Float {
+ val range = config.upperBookendScale - config.lowerBookendScale
+ val interpolatedVelocity =
+ velocityInterpolator.getInterpolation(velocity / config.maxVelocityToScale)
+ return interpolatedVelocity * range + config.lowerBookendScale
+ }
+
+ private fun scaleAtProgressChange(velocity: Float, progress: Float): Float {
+ val range = config.progressBasedDragMaxScale - config.progressBasedDragMinScale
+ val interpolatedVelocity =
+ velocityInterpolator.getInterpolation(velocity / config.maxVelocityToScale)
+ val interpolatedProgress = progressInterpolator.getInterpolation(progress)
+ val bump = interpolatedVelocity * config.additionalVelocityMaxBump
+ return interpolatedProgress * range + config.progressBasedDragMinScale + bump
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
index aab4bc361d7b..b90582575970 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt
@@ -75,6 +75,18 @@ class VibratorHelperTest : SysuiTestCase() {
}
@Test
+ fun testVibrate5() {
+ vibratorHelper.vibrate(
+ mock(VibrationEffect::class.java),
+ mock(VibrationAttributes::class.java)
+ )
+ verifyAsync().vibrate(
+ any(VibrationEffect::class.java),
+ any(VibrationAttributes::class.java)
+ )
+ }
+
+ @Test
fun testPerformHapticFeedback() {
val constant = HapticFeedbackConstants.CONFIRM
vibratorHelper.performHapticFeedback(view, constant)