1 package com.android.systemui.unfold.progress
2 
3 import android.os.Trace
4 import android.util.Log
5 import androidx.dynamicanimation.animation.FloatPropertyCompat
6 import androidx.dynamicanimation.animation.SpringAnimation
7 import androidx.dynamicanimation.animation.SpringForce
8 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
9 
10 /**
11  * Makes progress received from other processes resilient to jank.
12  *
13  * Sender and receiver processes might have different frame-rates. If the sending process is
14  * dropping a frame due to jank (or generally because it's main thread is too busy), we don't want
15  * the receiving process to drop progress frames as well. For this reason, a spring animator pass
16  * (with very high stiffness) is applied to the incoming progress. This adds a small delay to the
17  * progress (~30ms), but guarantees an always smooth animation on the receiving end.
18  */
19 class UnfoldRemoteFilter(
20     private val listener: UnfoldTransitionProgressProvider.TransitionProgressListener
21 ) : UnfoldTransitionProgressProvider.TransitionProgressListener {
22 
23     private val springAnimation =
24         SpringAnimation(this, AnimationProgressProperty).apply {
25             spring =
26                 SpringForce().apply {
27                     dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
28                     stiffness = 100_000f
29                     finalPosition = 1.0f
30                 }
31             setMinValue(0f)
32             setMaxValue(1f)
33             minimumVisibleChange = 0.001f
34         }
35 
36     private var inProgress = false
37     private var receivedProgressEvent = false
38 
39     private var processedProgress: Float = 1.0f
40         set(newProgress) {
41             if (inProgress) {
42                 logCounter({ "$TAG#filtered_progress" }, newProgress)
43                 listener.onTransitionProgress(newProgress)
44             } else {
45                 Log.e(TAG, "Filtered progress received received while animation not in progress.")
46             }
47             field = newProgress
48         }
49 
50     override fun onTransitionStarted() {
51         listener.onTransitionStarted()
52         inProgress = true
53     }
54 
55     override fun onTransitionProgress(progress: Float) {
56         logCounter({ "$TAG#plain_remote_progress" }, progress)
57         if (inProgress) {
58             if (receivedProgressEvent) {
59                 // We have received at least one progress event, animate from the previous
60                 // progress to the current
61                 springAnimation.animateToFinalPosition(progress)
62             } else {
63                 // This is the first progress event after starting the animation, send it
64                 // straightaway and set the spring value without animating it
65                 processedProgress = progress
66                 receivedProgressEvent = true
67             }
68         } else {
69             Log.e(TAG, "Progress received while not in progress.")
70         }
71     }
72 
73     override fun onTransitionFinished() {
74         inProgress = false
75         receivedProgressEvent = false
76         listener.onTransitionFinished()
77     }
78 
79     private object AnimationProgressProperty :
80         FloatPropertyCompat<UnfoldRemoteFilter>("UnfoldRemoteFilter") {
81 
82         override fun setValue(provider: UnfoldRemoteFilter, value: Float) {
83             provider.processedProgress = value
84         }
85 
86         override fun getValue(provider: UnfoldRemoteFilter): Float = provider.processedProgress
87     }
88     private fun logCounter(name: () -> String, progress: Float) {
89         if (DEBUG) {
90             Trace.setCounter(name(), (progress * 100).toLong())
91         }
92     }
93 }
94 
95 private val TAG = "UnfoldRemoteFilter"
96 private val DEBUG = false
97