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