diff options
5 files changed, 130 insertions, 3 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt index 3467382df4da..75fd56658d9d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt @@ -47,7 +47,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val config = SliderHapticFeedbackConfig() + private var config = SliderHapticFeedbackConfig() private val dragVelocityProvider = SliderDragVelocityProvider { config.maxVelocityToScale } @@ -227,6 +227,72 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() { } @Test + @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playHapticAtProgress_forDiscreteSlider_playsTick() = + with(kosmos) { + config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f) + sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + msdlPlayer, + dragVelocityProvider, + config, + kosmos.fakeSystemClock, + ) + + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress) + val tick = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, expectedScale) + .compose() + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN called to play haptics + sliderHapticFeedbackProvider.onProgress(progress) + + // THEN the correct composition only plays once + assertEquals(expected = 1, vibratorHelper.timesVibratedWithEffect(tick)) + } + + @Test + @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) + fun playHapticAtProgress_forDiscreteSlider_playsDiscreteSliderToken() = + with(kosmos) { + config = SliderHapticFeedbackConfig(sliderStepSize = 0.2f) + sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + msdlPlayer, + dragVelocityProvider, + config, + kosmos.fakeSystemClock, + ) + + // GIVEN max velocity and slider progress + val progress = 1f + val expectedScale = + sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress) + val expectedProperties = + InteractionProperties.DynamicVibrationScale(expectedScale, pipeliningAttributes) + + // GIVEN system running for 1s + fakeSystemClock.advanceTime(1000) + + // WHEN called to play haptics + sliderHapticFeedbackProvider.onProgress(progress) + + // THEN the correct token plays once + assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.DRAG_INDICATOR_DISCRETE) + assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(expectedProperties) + assertThat(msdlPlayer.getHistory().size).isEqualTo(1) + } + + @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playHapticAtProgress_onQuickSuccession_playsContinuousDragTokenOnce() = with(kosmos) { diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt index 24dd04dbadd2..da124de358a8 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt @@ -47,4 +47,6 @@ data class SliderHapticFeedbackConfig( @FloatRange(from = 0.0, to = 1.0) val lowerBookendScale: Float = 0.05f, /** Exponent for power function compensation */ @FloatRange(from = 0.0, fromInclusive = false) val exponent: Float = 1f / 0.89f, + /** The step-size that defines the slider quantization. Zero represents a continuous slider */ + @FloatRange(from = 0.0) val sliderStepSize: Float = 0f, ) diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt index bc4f531b8b81..de6697bd8fea 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt @@ -30,6 +30,7 @@ import com.google.android.msdl.domain.MSDLPlayer import kotlin.math.abs import kotlin.math.min import kotlin.math.pow +import kotlin.math.round /** * Listener of slider events that triggers haptic feedback. @@ -124,14 +125,45 @@ class SliderHapticFeedbackProvider( val deltaProgress = abs(normalizedSliderProgress - dragTextureLastProgress) if (deltaProgress < config.deltaProgressForDragThreshold) return + // Check if the progress is a discrete step so haptics can be delivered + if ( + config.sliderStepSize > 0 && + !normalizedSliderProgress.isDiscreteStep(config.sliderStepSize) + ) { + return + } + val powerScale = scaleOnDragTexture(absoluteVelocity, normalizedSliderProgress) // Deliver haptic feedback - performContinuousSliderDragVibration(powerScale) + when { + config.sliderStepSize == 0f -> performContinuousSliderDragVibration(powerScale) + config.sliderStepSize > 0f -> performDiscreteSliderDragVibration(powerScale) + } dragTextureLastTime = currentTime dragTextureLastProgress = normalizedSliderProgress } + private fun Float.isDiscreteStep(stepSize: Float, epsilon: Float = 0.001f): Boolean { + if (stepSize <= 0f) return false + val division = this / stepSize + return abs(division - round(division)) < epsilon + } + + private fun performDiscreteSliderDragVibration(scale: Float) { + if (Flags.msdlFeedback()) { + val properties = + InteractionProperties.DynamicVibrationScale(scale, VIBRATION_ATTRIBUTES_PIPELINING) + msdlPlayer.playToken(MSDLToken.DRAG_INDICATOR_DISCRETE, properties) + } else { + val effect = + VibrationEffect.startComposition() + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, scale) + .compose() + vibratorHelper.vibrate(effect, VIBRATION_ATTRIBUTES_PIPELINING) + } + } + private fun performContinuousSliderDragVibration(scale: Float) { if (Flags.msdlFeedback()) { val properties = diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt new file mode 100644 index 000000000000..033d55cc9b61 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderQuantization.kt @@ -0,0 +1,26 @@ +/* + * 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 + +interface SliderQuantization { + /** What is the step size between discrete steps of the slider */ + val stepSize: Float + + data class Continuous(override val stepSize: Float = Float.MIN_VALUE) : SliderQuantization + + data class Discrete(override val stepSize: Float) : SliderQuantization +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 07509e6368fb..e74b0f3cc7db 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -2697,7 +2697,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, /* velocityAxis= */ MotionEvent.AXIS_Y, /* upperBookendScale= */ 1f, /* lowerBookendScale= */ 0.05f, - /* exponent= */ 1f / 0.89f); + /* exponent= */ 1f / 0.89f, + /* sliderStepSize = */ 0f); private static final SeekableSliderTrackerConfig sSliderTrackerConfig = new SeekableSliderTrackerConfig( /* waitTimeMillis= */100, |