1 /*
2  * Copyright (C) 2021 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 package com.android.systemui.unfold.progress
17 
18 import android.util.Log
19 import android.util.MathUtils.saturate
20 import androidx.dynamicanimation.animation.DynamicAnimation
21 import androidx.dynamicanimation.animation.FloatPropertyCompat
22 import androidx.dynamicanimation.animation.SpringAnimation
23 import androidx.dynamicanimation.animation.SpringForce
24 import com.android.systemui.unfold.UnfoldTransitionProgressProvider
25 import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
26 import com.android.systemui.unfold.updates.FOLD_UPDATE_ABORTED
27 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED
28 import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING
29 import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN
30 import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE
31 import com.android.systemui.unfold.updates.FoldStateProvider
32 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
33 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
34 
35 /**
36  * Maps fold updates to unfold transition progress using DynamicAnimation.
37  *
38  * TODO(b/193793338) Current limitations:
39  *  - doesn't handle postures
40  */
41 internal class PhysicsBasedUnfoldTransitionProgressProvider(
42     private val foldStateProvider: FoldStateProvider
43 ) :
44     UnfoldTransitionProgressProvider,
45     FoldUpdatesListener,
46     DynamicAnimation.OnAnimationEndListener {
47 
48     private val springAnimation = SpringAnimation(this, AnimationProgressProperty)
49         .apply {
50             addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
51         }
52 
53     private var isTransitionRunning = false
54     private var isAnimatedCancelRunning = false
55 
56     private var transitionProgress: Float = 0.0f
57         set(value) {
58             if (isTransitionRunning) {
59                 listeners.forEach { it.onTransitionProgress(value) }
60             }
61             field = value
62         }
63 
64     private val listeners: MutableList<TransitionProgressListener> = mutableListOf()
65 
66     init {
67         foldStateProvider.addCallback(this)
68         foldStateProvider.start()
69     }
70 
71     override fun destroy() {
72         foldStateProvider.stop()
73     }
74 
75     override fun onHingeAngleUpdate(angle: Float) {
76         if (!isTransitionRunning || isAnimatedCancelRunning) return
77         val progress = saturate(angle / FINAL_HINGE_ANGLE_POSITION)
78         springAnimation.animateToFinalPosition(progress)
79     }
80 
81     override fun onFoldUpdate(@FoldUpdate update: Int) {
82         when (update) {
83             FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> {
84                 startTransition(startValue = 0f)
85 
86                 // Stop the animation if the device has already opened by the time when
87                 // the display is available as we won't receive the full open event anymore
88                 if (foldStateProvider.isFullyOpened) {
89                     cancelTransition(endValue = 1f, animate = true)
90                 }
91             }
92             FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_ABORTED -> {
93                 // Do not cancel if we haven't started the transition yet.
94                 // This could happen when we fully unfolded the device before the screen
95                 // became available. In this case we start and immediately cancel the animation
96                 // in FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE event handler, so we don't need to
97                 // cancel it here.
98                 if (isTransitionRunning) {
99                     cancelTransition(endValue = 1f, animate = true)
100                 }
101             }
102             FOLD_UPDATE_FINISH_CLOSED -> {
103                 cancelTransition(endValue = 0f, animate = false)
104             }
105             FOLD_UPDATE_START_CLOSING -> {
106                 // The transition might be already running as the device might start closing several
107                 // times before reaching an end state.
108                 if (!isTransitionRunning) {
109                     startTransition(startValue = 1f)
110                 }
111             }
112         }
113 
114         if (DEBUG) {
115             Log.d(TAG, "onFoldUpdate = $update")
116         }
117     }
118 
119     private fun cancelTransition(endValue: Float, animate: Boolean) {
120         if (isTransitionRunning && animate) {
121             isAnimatedCancelRunning = true
122             springAnimation.animateToFinalPosition(endValue)
123         } else {
124             transitionProgress = endValue
125             isAnimatedCancelRunning = false
126             isTransitionRunning = false
127             springAnimation.cancel()
128 
129             listeners.forEach {
130                 it.onTransitionFinished()
131             }
132 
133             if (DEBUG) {
134                 Log.d(TAG, "onTransitionFinished")
135             }
136         }
137     }
138 
139     override fun onAnimationEnd(
140         animation: DynamicAnimation<out DynamicAnimation<*>>,
141         canceled: Boolean,
142         value: Float,
143         velocity: Float
144     ) {
145         if (isAnimatedCancelRunning) {
146             cancelTransition(value, animate = false)
147         }
148     }
149 
150     private fun onStartTransition() {
151         listeners.forEach {
152             it.onTransitionStarted()
153         }
154         isTransitionRunning = true
155 
156         if (DEBUG) {
157             Log.d(TAG, "onTransitionStarted")
158         }
159     }
160 
161     private fun startTransition(startValue: Float) {
162         if (!isTransitionRunning) onStartTransition()
163 
164         springAnimation.apply {
165             spring = SpringForce().apply {
166                 finalPosition = startValue
167                 dampingRatio = SpringForce.DAMPING_RATIO_NO_BOUNCY
168                 stiffness = SPRING_STIFFNESS
169             }
170             minimumVisibleChange = MINIMAL_VISIBLE_CHANGE
171             setStartValue(startValue)
172             setMinValue(0f)
173             setMaxValue(1f)
174         }
175 
176         springAnimation.start()
177     }
178 
179     override fun addCallback(listener: TransitionProgressListener) {
180         listeners.add(listener)
181     }
182 
183     override fun removeCallback(listener: TransitionProgressListener) {
184         listeners.remove(listener)
185     }
186 
187     private object AnimationProgressProperty :
188         FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") {
189 
190         override fun setValue(
191             provider: PhysicsBasedUnfoldTransitionProgressProvider,
192             value: Float
193         ) {
194             provider.transitionProgress = value
195         }
196 
197         override fun getValue(provider: PhysicsBasedUnfoldTransitionProgressProvider): Float =
198             provider.transitionProgress
199     }
200 }
201 
202 private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider"
203 private const val DEBUG = true
204 
205 private const val SPRING_STIFFNESS = 200.0f
206 private const val MINIMAL_VISIBLE_CHANGE = 0.001f
207 private const val FINAL_HINGE_ANGLE_POSITION = 165f
208