• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.haptics.slider
18 
19 import android.os.VibrationAttributes
20 import android.os.VibrationEffect
21 import android.view.VelocityTracker
22 import android.view.animation.AccelerateInterpolator
23 import androidx.annotation.FloatRange
24 import androidx.annotation.VisibleForTesting
25 import com.android.systemui.Flags
26 import com.android.systemui.statusbar.VibratorHelper
27 import com.google.android.msdl.data.model.MSDLToken
28 import com.google.android.msdl.domain.InteractionProperties
29 import com.google.android.msdl.domain.MSDLPlayer
30 import kotlin.math.abs
31 import kotlin.math.min
32 import kotlin.math.pow
33 import kotlin.math.round
34 
35 /**
36  * Listener of slider events that triggers haptic feedback.
37  *
38  * @property[vibratorHelper] Singleton instance of the [VibratorHelper] to deliver haptics.
39  * @property[velocityTracker] Instance of a [VelocityTracker] that tracks slider dragging velocity.
40  * @property[config] Configuration parameters for vibration encapsulated as a
41  *   [SliderHapticFeedbackConfig].
42  * @property[clock] Clock to obtain elapsed real time values.
43  */
44 class SliderHapticFeedbackProvider(
45     private val vibratorHelper: VibratorHelper,
46     private val msdlPlayer: MSDLPlayer,
47     private val velocityProvider: SliderDragVelocityProvider,
48     private val config: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
49     private val clock: com.android.systemui.util.time.SystemClock,
50 ) : SliderStateListener {
51 
52     private val velocityAccelerateInterpolator =
53         AccelerateInterpolator(config.velocityInterpolatorFactor)
54     private val positionAccelerateInterpolator =
55         AccelerateInterpolator(config.progressInterpolatorFactor)
56     private var dragTextureLastTime = clock.elapsedRealtime()
57     var dragTextureLastProgress = -1f
58         private set
59 
60     private val lowTickDurationMs =
61         vibratorHelper.getPrimitiveDurations(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0]
62     private var hasVibratedAtLowerBookend = false
63     private var hasVibratedAtUpperBookend = false
64 
65     /** Time threshold to wait before making new API call. */
66     private val thresholdUntilNextDragCallMillis =
67         lowTickDurationMs * config.numberOfLowTicks + config.deltaMillisForDragInterval
68 
69     /**
70      * Vibrate when the handle reaches either bookend with a certain velocity.
71      *
72      * @param[absoluteVelocity] Velocity of the handle when it reached the bookend.
73      */
vibrateOnEdgeCollisionnull74     private fun vibrateOnEdgeCollision(absoluteVelocity: Float) {
75         val powerScale = scaleOnEdgeCollision(absoluteVelocity)
76         if (Flags.msdlFeedback()) {
77             val properties =
78                 InteractionProperties.DynamicVibrationScale(
79                     powerScale,
80                     VIBRATION_ATTRIBUTES_PIPELINING,
81                 )
82             msdlPlayer.playToken(MSDLToken.DRAG_THRESHOLD_INDICATOR_LIMIT, properties)
83         } else {
84             val vibration =
85                 VibrationEffect.startComposition()
86                     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, powerScale)
87                     .compose()
88             vibratorHelper.vibrate(vibration, VIBRATION_ATTRIBUTES_PIPELINING)
89         }
90     }
91 
92     /**
93      * Get the velocity-based scale at the bookends
94      *
95      * @param[absoluteVelocity] Velocity of the handle when it reached the bookend.
96      * @return The power scale for the vibration.
97      */
98     @VisibleForTesting
scaleOnEdgeCollisionnull99     fun scaleOnEdgeCollision(absoluteVelocity: Float): Float {
100         val velocityInterpolated =
101             velocityAccelerateInterpolator.getInterpolation(
102                 min(absoluteVelocity / config.maxVelocityToScale, 1f)
103             )
104         val bookendScaleRange = config.upperBookendScale - config.lowerBookendScale
105         val bookendsHitScale = bookendScaleRange * velocityInterpolated + config.lowerBookendScale
106         return bookendsHitScale.pow(config.exponent)
107     }
108 
109     /**
110      * Create a drag texture vibration based on velocity and slider progress.
111      *
112      * @param[absoluteVelocity] Absolute velocity of the handle.
113      * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from
114      *   0F to 1F (inclusive).
115      */
vibrateDragTexturenull116     private fun vibrateDragTexture(
117         absoluteVelocity: Float,
118         @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
119     ) {
120         // Check if its time to vibrate
121         val currentTime = clock.elapsedRealtime()
122         val elapsedSinceLastDrag = currentTime - dragTextureLastTime
123         if (elapsedSinceLastDrag < thresholdUntilNextDragCallMillis) return
124 
125         val deltaProgress = abs(normalizedSliderProgress - dragTextureLastProgress)
126         if (deltaProgress < config.deltaProgressForDragThreshold) return
127 
128         // Check if the progress is a discrete step so haptics can be delivered
129         if (
130             config.sliderStepSize > 0 &&
131                 !normalizedSliderProgress.isDiscreteStep(config.sliderStepSize)
132         ) {
133             return
134         }
135 
136         val powerScale = scaleOnDragTexture(absoluteVelocity, normalizedSliderProgress)
137 
138         // Deliver haptic feedback
139         when {
140             config.sliderStepSize == 0f -> performContinuousSliderDragVibration(powerScale)
141             config.sliderStepSize > 0f -> performDiscreteSliderDragVibration(powerScale)
142         }
143         dragTextureLastTime = currentTime
144         dragTextureLastProgress = normalizedSliderProgress
145     }
146 
Floatnull147     private fun Float.isDiscreteStep(stepSize: Float, epsilon: Float = 0.001f): Boolean {
148         if (stepSize <= 0f) return false
149         val division = this / stepSize
150         return abs(division - round(division)) < epsilon
151     }
152 
performDiscreteSliderDragVibrationnull153     private fun performDiscreteSliderDragVibration(scale: Float) {
154         if (Flags.msdlFeedback()) {
155             val properties =
156                 InteractionProperties.DynamicVibrationScale(scale, VIBRATION_ATTRIBUTES_PIPELINING)
157             msdlPlayer.playToken(MSDLToken.DRAG_INDICATOR_DISCRETE, properties)
158         } else {
159             val effect =
160                 VibrationEffect.startComposition()
161                     .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, scale)
162                     .compose()
163             vibratorHelper.vibrate(effect, VIBRATION_ATTRIBUTES_PIPELINING)
164         }
165     }
166 
performContinuousSliderDragVibrationnull167     private fun performContinuousSliderDragVibration(scale: Float) {
168         if (Flags.msdlFeedback()) {
169             val properties =
170                 InteractionProperties.DynamicVibrationScale(scale, VIBRATION_ATTRIBUTES_PIPELINING)
171             msdlPlayer.playToken(MSDLToken.DRAG_INDICATOR_CONTINUOUS, properties)
172         } else {
173             val composition = VibrationEffect.startComposition()
174             repeat(config.numberOfLowTicks) {
175                 composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale)
176             }
177             vibratorHelper.vibrate(composition.compose(), VIBRATION_ATTRIBUTES_PIPELINING)
178         }
179     }
180 
181     /**
182      * Get the scale of the drag texture vibration.
183      *
184      * @param[absoluteVelocity] Absolute velocity of the handle.
185      * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from
186      *   0F to 1F (inclusive).
187      *     @return the scale of the vibration.
188      */
189     @VisibleForTesting
scaleOnDragTexturenull190     fun scaleOnDragTexture(
191         absoluteVelocity: Float,
192         @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
193     ): Float {
194         val velocityInterpolated =
195             velocityAccelerateInterpolator.getInterpolation(
196                 min(absoluteVelocity / config.maxVelocityToScale, 1f)
197             )
198 
199         // Scaling of vibration due to the position of the slider
200         val positionScaleRange = config.progressBasedDragMaxScale - config.progressBasedDragMinScale
201         val sliderProgressInterpolated =
202             positionAccelerateInterpolator.getInterpolation(normalizedSliderProgress)
203         val positionBasedScale =
204             positionScaleRange * sliderProgressInterpolated + config.progressBasedDragMinScale
205 
206         // Scaling bump due to velocity
207         val velocityBasedScale = velocityInterpolated * config.additionalVelocityMaxBump
208 
209         // Total scale
210         val scale = positionBasedScale + velocityBasedScale
211         return scale.pow(config.exponent)
212     }
213 
onHandleAcquiredByTouchnull214     override fun onHandleAcquiredByTouch() {}
215 
onHandleReleasedFromTouchnull216     override fun onHandleReleasedFromTouch() {
217         dragTextureLastProgress = -1f
218     }
219 
onLowerBookendnull220     override fun onLowerBookend() {
221         if (!hasVibratedAtLowerBookend && config.filter.vibrateOnLowerBookend) {
222             vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
223             hasVibratedAtLowerBookend = true
224         }
225     }
226 
onUpperBookendnull227     override fun onUpperBookend() {
228         if (!hasVibratedAtUpperBookend && config.filter.vibrateOnUpperBookend) {
229             vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
230             hasVibratedAtUpperBookend = true
231         }
232     }
233 
onProgressnull234     override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
235         vibrateDragTexture(abs(velocityProvider.getTrackedVelocity()), progress)
236         hasVibratedAtUpperBookend = false
237         hasVibratedAtLowerBookend = false
238     }
239 
onProgressJumpnull240     override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
241 
onSelectAndArrownull242     override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
243 
244     private companion object {
245         private val VIBRATION_ATTRIBUTES_PIPELINING =
246             VibrationAttributes.Builder()
247                 .setUsage(VibrationAttributes.USAGE_TOUCH)
248                 .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
249                 .build()
250     }
251 }
252