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.updates
17
18 import android.content.Context
19 import android.os.Handler
20 import android.os.Trace
21 import android.util.Log
22 import androidx.annotation.FloatRange
23 import androidx.annotation.VisibleForTesting
24 import androidx.core.util.Consumer
25 import com.android.systemui.unfold.compat.INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
26 import com.android.systemui.unfold.config.UnfoldTransitionConfig
27 import com.android.systemui.unfold.dagger.UnfoldMain
28 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate
29 import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener
30 import com.android.systemui.unfold.updates.RotationChangeProvider.RotationListener
31 import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES
32 import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES
33 import com.android.systemui.unfold.updates.hinge.HingeAngleProvider
34 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider
35 import com.android.systemui.unfold.util.CurrentActivityTypeProvider
36 import com.android.systemui.unfold.util.UnfoldKeyguardVisibilityProvider
37 import java.util.concurrent.Executor
38 import javax.inject.Inject
39
40 class DeviceFoldStateProvider
41 @Inject
42 constructor(
43 config: UnfoldTransitionConfig,
44 private val hingeAngleProvider: HingeAngleProvider,
45 private val screenStatusProvider: ScreenStatusProvider,
46 private val foldProvider: FoldProvider,
47 private val activityTypeProvider: CurrentActivityTypeProvider,
48 private val unfoldKeyguardVisibilityProvider: UnfoldKeyguardVisibilityProvider,
49 private val rotationChangeProvider: RotationChangeProvider,
50 private val context: Context,
51 @UnfoldMain private val mainExecutor: Executor,
52 @UnfoldMain private val handler: Handler
53 ) : FoldStateProvider {
54
55 private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf()
56
57 @FoldUpdate private var lastFoldUpdate: Int? = null
58
59 @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f
60 @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngleBeforeTransition: Float = 0f
61
62 private val hingeAngleListener = HingeAngleListener()
63 private val screenListener = ScreenStatusListener()
64 private val foldStateListener = FoldStateListener()
<lambda>null65 private val timeoutRunnable = Runnable { cancelAnimation() }
<lambda>null66 private val rotationListener = RotationListener {
67 if (isTransitionInProgress) cancelAnimation()
68 }
69
70 /**
71 * Time after which [FOLD_UPDATE_FINISH_HALF_OPEN] is emitted following a
72 * [FOLD_UPDATE_START_CLOSING] or [FOLD_UPDATE_START_OPENING] event, if an end state is not
73 * reached.
74 */
75 private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
76
77 private var isFolded = false
78 private var isScreenOn = false
79 private var isUnfoldHandled = true
80
startnull81 override fun start() {
82 foldProvider.registerCallback(foldStateListener, mainExecutor)
83 screenStatusProvider.addCallback(screenListener)
84 hingeAngleProvider.addCallback(hingeAngleListener)
85 rotationChangeProvider.addCallback(rotationListener)
86 activityTypeProvider.init()
87 }
88
stopnull89 override fun stop() {
90 screenStatusProvider.removeCallback(screenListener)
91 foldProvider.unregisterCallback(foldStateListener)
92 hingeAngleProvider.removeCallback(hingeAngleListener)
93 hingeAngleProvider.stop()
94 rotationChangeProvider.removeCallback(rotationListener)
95 activityTypeProvider.uninit()
96 }
97
addCallbacknull98 override fun addCallback(listener: FoldUpdatesListener) {
99 outputListeners.add(listener)
100 }
101
removeCallbacknull102 override fun removeCallback(listener: FoldUpdatesListener) {
103 outputListeners.remove(listener)
104 }
105
106 override val isFinishedOpening: Boolean
107 get() =
108 !isFolded &&
109 (lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN ||
110 lastFoldUpdate == FOLD_UPDATE_FINISH_HALF_OPEN)
111
112 private val isTransitionInProgress: Boolean
113 get() =
114 lastFoldUpdate == FOLD_UPDATE_START_OPENING ||
115 lastFoldUpdate == FOLD_UPDATE_START_CLOSING
116
onHingeAnglenull117 private fun onHingeAngle(angle: Float) {
118 if (DEBUG) {
119 Log.d(
120 TAG,
121 "Hinge angle: $angle, " +
122 "lastHingeAngle: $lastHingeAngle, " +
123 "lastHingeAngleBeforeTransition: $lastHingeAngleBeforeTransition"
124 )
125 Trace.setCounter("hinge_angle", angle.toLong())
126 }
127
128 val currentDirection =
129 if (angle < lastHingeAngle) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
130 if (isTransitionInProgress && currentDirection != lastFoldUpdate) {
131 lastHingeAngleBeforeTransition = lastHingeAngle
132 }
133
134 val isClosing = angle < lastHingeAngleBeforeTransition
135 val transitionUpdate =
136 if (isClosing) FOLD_UPDATE_START_CLOSING else FOLD_UPDATE_START_OPENING
137 val angleChangeSurpassedThreshold =
138 Math.abs(angle - lastHingeAngleBeforeTransition) > HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES
139 val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES
140 val eventNotAlreadyDispatched = lastFoldUpdate != transitionUpdate
141 val screenAvailableEventSent = isUnfoldHandled
142 val isOnLargeScreen = isOnLargeScreen()
143
144 if (
145 angleChangeSurpassedThreshold && // Do not react immediately to small changes in angle
146 eventNotAlreadyDispatched && // we haven't sent transition event already
147 !isFullyOpened && // do not send transition event if we are in fully opened hinge
148 // angle range as closing threshold could overlap this range
149 screenAvailableEventSent && // do not send transition event if we are still in the
150 // process of turning on the inner display
151 isClosingThresholdMet(angle) && // hinge angle is below certain threshold.
152 isOnLargeScreen // Avoids sending closing event when on small screen.
153 // Start event is sent regardless due to hall sensor.
154 ) {
155 notifyFoldUpdate(transitionUpdate, lastHingeAngle)
156 }
157
158 if (isTransitionInProgress) {
159 if (isFullyOpened) {
160 notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN, angle)
161 cancelTimeout()
162 } else {
163 // The timeout will trigger some constant time after the last angle update.
164 rescheduleAbortAnimationTimeout()
165 }
166 }
167
168 lastHingeAngle = angle
169 outputListeners.forEach { it.onHingeAngleUpdate(angle) }
170 }
171
isClosingThresholdMetnull172 private fun isClosingThresholdMet(currentAngle: Float): Boolean {
173 val closingThreshold = getClosingThreshold()
174 return closingThreshold == null || currentAngle < closingThreshold
175 }
176
177 /**
178 * Fold animation should be started only after the threshold returned here.
179 *
180 * This has been introduced because the fold animation might be distracting/unwanted on top of
181 * apps that support table-top/HALF_FOLDED mode. Only for launcher, there is no threshold.
182 */
getClosingThresholdnull183 private fun getClosingThreshold(): Int? {
184 val isHomeActivity = activityTypeProvider.isHomeActivity ?: return null
185 val isKeyguardVisible = unfoldKeyguardVisibilityProvider.isKeyguardVisible == true
186
187 if (DEBUG) {
188 Log.d(TAG, "isHomeActivity=$isHomeActivity, isOnKeyguard=$isKeyguardVisible")
189 }
190
191 return if (isHomeActivity || isKeyguardVisible) {
192 null
193 } else {
194 START_CLOSING_ON_APPS_THRESHOLD_DEGREES
195 }
196 }
197
198 private inner class FoldStateListener : FoldProvider.FoldCallback {
onFoldUpdatednull199 override fun onFoldUpdated(isFolded: Boolean) {
200 this@DeviceFoldStateProvider.isFolded = isFolded
201 lastHingeAngle = FULLY_CLOSED_DEGREES
202
203 if (isFolded) {
204 hingeAngleProvider.stop()
205 notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED, lastHingeAngle)
206 cancelTimeout()
207 isUnfoldHandled = false
208 } else {
209 notifyFoldUpdate(FOLD_UPDATE_START_OPENING, lastHingeAngle)
210 rescheduleAbortAnimationTimeout()
211 hingeAngleProvider.start()
212 }
213 }
214 }
215
notifyFoldUpdatenull216 private fun notifyFoldUpdate(@FoldUpdate update: Int, angle: Float) {
217 if (DEBUG) {
218 Log.d(TAG, update.name())
219 }
220 val previouslyTransitioning = isTransitionInProgress
221
222 outputListeners.forEach { it.onFoldUpdate(update) }
223 lastFoldUpdate = update
224
225 if (previouslyTransitioning != isTransitionInProgress) {
226 lastHingeAngleBeforeTransition = angle
227 }
228 }
229
rescheduleAbortAnimationTimeoutnull230 private fun rescheduleAbortAnimationTimeout() {
231 if (isTransitionInProgress) {
232 cancelTimeout()
233 }
234 handler.postDelayed(timeoutRunnable, halfOpenedTimeoutMillis.toLong())
235 }
236
cancelTimeoutnull237 private fun cancelTimeout() {
238 handler.removeCallbacks(timeoutRunnable)
239 }
240
cancelAnimationnull241 private fun cancelAnimation(): Unit =
242 notifyFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN, lastHingeAngle)
243
244 private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener {
245
246 override fun onScreenTurnedOn() {
247 // Trigger this event only if we are unfolded and this is the first screen
248 // turned on event since unfold started. This prevents running the animation when
249 // turning on the internal display using the power button.
250 // Initially isUnfoldHandled is true so it will be reset to false *only* when we
251 // receive 'folded' event. If SystemUI started when device is already folded it will
252 // still receive 'folded' event on startup.
253 if (!isFolded && !isUnfoldHandled) {
254 outputListeners.forEach { it.onUnfoldedScreenAvailable() }
255 isUnfoldHandled = true
256 }
257 }
258
259 override fun markScreenAsTurnedOn() {
260 if (!isFolded) {
261 isUnfoldHandled = true
262 }
263 }
264
265 override fun onScreenTurningOn() {
266 isScreenOn = true
267 updateHingeAngleProviderState()
268 }
269
270 override fun onScreenTurningOff() {
271 isScreenOn = false
272 updateHingeAngleProviderState()
273 }
274 }
275
isOnLargeScreennull276 private fun isOnLargeScreen(): Boolean {
277 return context.resources.configuration.smallestScreenWidthDp >
278 INNER_SCREEN_SMALLEST_SCREEN_WIDTH_THRESHOLD_DP
279 }
280
281 /** While the screen is off or the device is folded, hinge angle updates are not needed. */
updateHingeAngleProviderStatenull282 private fun updateHingeAngleProviderState() {
283 if (isScreenOn && !isFolded) {
284 hingeAngleProvider.start()
285 } else {
286 hingeAngleProvider.stop()
287 }
288 }
289
290 private inner class HingeAngleListener : Consumer<Float> {
acceptnull291 override fun accept(angle: Float) {
292 onHingeAngle(angle)
293 }
294 }
295 }
296
namenull297 fun @receiver:FoldUpdate Int.name() =
298 when (this) {
299 FOLD_UPDATE_START_OPENING -> "START_OPENING"
300 FOLD_UPDATE_START_CLOSING -> "START_CLOSING"
301 FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN"
302 FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN"
303 FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED"
304 else -> "UNKNOWN"
305 }
306
307 private const val TAG = "DeviceFoldProvider"
308 private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
309
310 /** Threshold after which we consider the device fully unfolded. */
311 @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f
312
313 /** Threshold after which hinge angle updates are considered. This is to eliminate noise. */
314 @VisibleForTesting const val HINGE_ANGLE_CHANGE_THRESHOLD_DEGREES = 7.5f
315
316 /** Fold animation on top of apps only when the angle exceeds this threshold. */
317 @VisibleForTesting const val START_CLOSING_ON_APPS_THRESHOLD_DEGREES = 60
318