1 package com.android.systemui.unfold 2 3 import android.os.SystemProperties 4 import android.os.VibrationAttributes 5 import android.os.VibrationEffect 6 import android.os.Vibrator 7 import com.android.systemui.dagger.qualifiers.Main 8 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener 9 import com.android.systemui.unfold.updates.FoldProvider 10 import com.android.systemui.unfold.updates.FoldProvider.FoldCallback 11 import java.util.concurrent.Executor 12 import javax.inject.Inject 13 14 /** Class that plays a haptics effect during unfolding a foldable device */ 15 @SysUIUnfoldScope 16 class UnfoldHapticsPlayer 17 @Inject 18 constructor( 19 unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider, 20 foldProvider: FoldProvider, 21 @Main private val mainExecutor: Executor, 22 private val vibrator: Vibrator? 23 ) : TransitionProgressListener { 24 25 private var isFirstAnimationAfterUnfold = false 26 private val touchVibrationAttributes = 27 VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK) 28 29 init { 30 if (vibrator != null) { 31 // We don't need to remove the callback because we should listen to it 32 // the whole time when SystemUI process is alive 33 unfoldTransitionProgressProvider.addCallback(this) 34 } 35 36 foldProvider.registerCallback( 37 object : FoldCallback { 38 override fun onFoldUpdated(isFolded: Boolean) { 39 if (isFolded) { 40 isFirstAnimationAfterUnfold = true 41 } 42 } 43 }, 44 mainExecutor 45 ) 46 } 47 48 private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN 49 50 override fun onTransitionStarted() { 51 lastTransitionProgress = TRANSITION_PROGRESS_CLOSED 52 } 53 54 override fun onTransitionProgress(progress: Float) { 55 lastTransitionProgress = progress 56 } 57 58 override fun onTransitionFinishing() { 59 // Run haptics only when unfolding the device (first animation after unfolding) 60 if (!isFirstAnimationAfterUnfold) { 61 return 62 } 63 64 isFirstAnimationAfterUnfold = false 65 66 // Run haptics only if the animation is long enough to notice 67 if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) { 68 playHaptics() 69 } 70 } 71 72 override fun onTransitionFinished() { 73 lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN 74 } 75 76 private fun playHaptics() { 77 vibrator?.vibrate(effect, touchVibrationAttributes) 78 } 79 80 private val hapticsScale: Float 81 get() { 82 val intensityString = SystemProperties.get("persist.unfold.haptics_scale", "0.5") 83 return intensityString.toFloatOrNull() ?: 0.5f 84 } 85 86 private val hapticsScaleTick: Float 87 get() { 88 val intensityString = 89 SystemProperties.get("persist.unfold.haptics_scale_end_tick", "1.0") 90 return intensityString.toFloatOrNull() ?: 1.0f 91 } 92 93 private val primitivesCount: Int 94 get() { 95 val count = SystemProperties.get("persist.unfold.primitives_count", "18") 96 return count.toIntOrNull() ?: 18 97 } 98 99 private val effect: VibrationEffect by lazy { 100 val composition = 101 VibrationEffect.startComposition() 102 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0F, 0) 103 104 repeat(primitivesCount) { 105 composition.addPrimitive( 106 VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 107 hapticsScale, 108 0 109 ) 110 } 111 112 composition 113 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, hapticsScaleTick) 114 .compose() 115 } 116 } 117 118 private const val TRANSITION_PROGRESS_CLOSED = 0f 119 private const val TRANSITION_PROGRESS_FULL_OPEN = 1f 120 private const val TRANSITION_NOTICEABLE_THRESHOLD = 0.9f 121