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.statusbar.VibratorHelper 26 import kotlin.math.abs 27 import kotlin.math.min 28 import kotlin.math.pow 29 30 /** 31 * Listener of slider events that triggers haptic feedback. 32 * 33 * @property[vibratorHelper] Singleton instance of the [VibratorHelper] to deliver haptics. 34 * @property[velocityTracker] Instance of a [VelocityTracker] that tracks slider dragging velocity. 35 * @property[config] Configuration parameters for vibration encapsulated as a 36 * [SliderHapticFeedbackConfig]. 37 * @property[clock] Clock to obtain elapsed real time values. 38 */ 39 class SliderHapticFeedbackProvider( 40 private val vibratorHelper: VibratorHelper, 41 private val velocityTracker: VelocityTracker, 42 private val config: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(), 43 private val clock: com.android.systemui.util.time.SystemClock, 44 ) : SliderStateListener { 45 46 private val velocityAccelerateInterpolator = 47 AccelerateInterpolator(config.velocityInterpolatorFactor) 48 private val positionAccelerateInterpolator = 49 AccelerateInterpolator(config.progressInterpolatorFactor) 50 private var dragTextureLastTime = clock.elapsedRealtime() 51 var dragTextureLastProgress = -1f 52 private set 53 private val lowTickDurationMs = 54 vibratorHelper.getPrimitiveDurations(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0] 55 private var hasVibratedAtLowerBookend = false 56 private var hasVibratedAtUpperBookend = false 57 58 /** Time threshold to wait before making new API call. */ 59 private val thresholdUntilNextDragCallMillis = 60 lowTickDurationMs * config.numberOfLowTicks + config.deltaMillisForDragInterval 61 62 /** 63 * Vibrate when the handle reaches either bookend with a certain velocity. 64 * 65 * @param[absoluteVelocity] Velocity of the handle when it reached the bookend. 66 */ vibrateOnEdgeCollisionnull67 private fun vibrateOnEdgeCollision(absoluteVelocity: Float) { 68 val powerScale = scaleOnEdgeCollision(absoluteVelocity) 69 val vibration = 70 VibrationEffect.startComposition() 71 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, powerScale) 72 .compose() 73 vibratorHelper.vibrate(vibration, VIBRATION_ATTRIBUTES_PIPELINING) 74 } 75 76 /** 77 * Get the velocity-based scale at the bookends 78 * 79 * @param[absoluteVelocity] Velocity of the handle when it reached the bookend. 80 * @return The power scale for the vibration. 81 */ 82 @VisibleForTesting scaleOnEdgeCollisionnull83 fun scaleOnEdgeCollision(absoluteVelocity: Float): Float { 84 val velocityInterpolated = 85 velocityAccelerateInterpolator.getInterpolation( 86 min(absoluteVelocity / config.maxVelocityToScale, 1f) 87 ) 88 val bookendScaleRange = config.upperBookendScale - config.lowerBookendScale 89 val bookendsHitScale = bookendScaleRange * velocityInterpolated + config.lowerBookendScale 90 return bookendsHitScale.pow(config.exponent) 91 } 92 93 /** 94 * Create a drag texture vibration based on velocity and slider progress. 95 * 96 * @param[absoluteVelocity] Absolute velocity of the handle. 97 * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from 98 * 0F to 1F (inclusive). 99 */ vibrateDragTexturenull100 private fun vibrateDragTexture( 101 absoluteVelocity: Float, 102 @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float 103 ) { 104 // Check if its time to vibrate 105 val currentTime = clock.elapsedRealtime() 106 val elapsedSinceLastDrag = currentTime - dragTextureLastTime 107 if (elapsedSinceLastDrag < thresholdUntilNextDragCallMillis) return 108 109 val deltaProgress = abs(normalizedSliderProgress - dragTextureLastProgress) 110 if (deltaProgress < config.deltaProgressForDragThreshold) return 111 112 val powerScale = scaleOnDragTexture(absoluteVelocity, normalizedSliderProgress) 113 114 // Trigger the vibration composition 115 val composition = VibrationEffect.startComposition() 116 repeat(config.numberOfLowTicks) { 117 composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, powerScale) 118 } 119 vibratorHelper.vibrate(composition.compose(), VIBRATION_ATTRIBUTES_PIPELINING) 120 dragTextureLastTime = currentTime 121 dragTextureLastProgress = normalizedSliderProgress 122 } 123 124 /** 125 * Get the scale of the drag texture vibration. 126 * 127 * @param[absoluteVelocity] Absolute velocity of the handle. 128 * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from 129 * 0F to 1F (inclusive). 130 * @return the scale of the vibration. 131 */ 132 @VisibleForTesting scaleOnDragTexturenull133 fun scaleOnDragTexture( 134 absoluteVelocity: Float, 135 @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float 136 ): Float { 137 val velocityInterpolated = 138 velocityAccelerateInterpolator.getInterpolation( 139 min(absoluteVelocity / config.maxVelocityToScale, 1f) 140 ) 141 142 // Scaling of vibration due to the position of the slider 143 val positionScaleRange = config.progressBasedDragMaxScale - config.progressBasedDragMinScale 144 val sliderProgressInterpolated = 145 positionAccelerateInterpolator.getInterpolation(normalizedSliderProgress) 146 val positionBasedScale = 147 positionScaleRange * sliderProgressInterpolated + config.progressBasedDragMinScale 148 149 // Scaling bump due to velocity 150 val velocityBasedScale = velocityInterpolated * config.additionalVelocityMaxBump 151 152 // Total scale 153 val scale = positionBasedScale + velocityBasedScale 154 return scale.pow(config.exponent) 155 } 156 onHandleAcquiredByTouchnull157 override fun onHandleAcquiredByTouch() {} 158 onHandleReleasedFromTouchnull159 override fun onHandleReleasedFromTouch() { 160 dragTextureLastProgress = -1f 161 } 162 onLowerBookendnull163 override fun onLowerBookend() { 164 if (!hasVibratedAtLowerBookend) { 165 vibrateOnEdgeCollision(abs(getTrackedVelocity())) 166 hasVibratedAtLowerBookend = true 167 } 168 } 169 onUpperBookendnull170 override fun onUpperBookend() { 171 if (!hasVibratedAtUpperBookend) { 172 vibrateOnEdgeCollision(abs(getTrackedVelocity())) 173 hasVibratedAtUpperBookend = true 174 } 175 } 176 onProgressnull177 override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) { 178 vibrateDragTexture(abs(getTrackedVelocity()), progress) 179 hasVibratedAtUpperBookend = false 180 hasVibratedAtLowerBookend = false 181 } 182 getTrackedVelocitynull183 private fun getTrackedVelocity(): Float { 184 velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale) 185 return if (velocityTracker.isAxisSupported(config.velocityAxis)) { 186 velocityTracker.getAxisVelocity(config.velocityAxis) 187 } else { 188 0f 189 } 190 } 191 onProgressJumpnull192 override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} 193 onSelectAndArrownull194 override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {} 195 196 private companion object { 197 private val VIBRATION_ATTRIBUTES_PIPELINING = 198 VibrationAttributes.Builder() 199 .setUsage(VibrationAttributes.USAGE_TOUCH) 200 .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT) 201 .build() 202 private const val UNITS_SECOND = 1000 203 } 204 } 205