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